Skip to content

Commit 798fcfa

Browse files
author
privapps
committed
Enhance GitHub Copilot Service Proxy with performance optimizations and timeout configurations
- Gzip the built binary for release assets in GitHub Actions workflow. - Update README.md to reflect project name change and add performance features. - Introduce timeout configurations in config.go and config.example.json. - Implement circuit breaker and request coalescing in proxy.go for improved reliability. - Add profiling endpoints for monitoring and performance analysis. - Refactor HTTP client initialization to use shared client with configurable timeouts. - Enhance worker pool management for concurrent request processing. - Implement graceful shutdown for worker pool during server termination.
1 parent 9414f25 commit 798fcfa

File tree

10 files changed

+670
-168
lines changed

10 files changed

+670
-168
lines changed

.github/workflows/release.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,19 @@ jobs:
128128
# Make the binary executable (important for Unix systems)
129129
chmod +x "$BINARY_NAME"
130130
131-
echo "Built binary: $BINARY_NAME"
132-
ls -la "$BINARY_NAME"
131+
# Gzip the binary
132+
GZ_BINARY_NAME="$BINARY_NAME.gz"
133+
gzip -c "$BINARY_NAME" > "$GZ_BINARY_NAME"
134+
135+
echo "Built and gzipped binary: $GZ_BINARY_NAME"
136+
ls -la "$GZ_BINARY_NAME"
133137
134138
- name: Upload Release Asset
135139
uses: actions/upload-release-asset@v1
136140
env:
137141
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138142
with:
139143
upload_url: ${{ needs.release.outputs.upload_url }}
140-
asset_path: ./github-copilot-svcs-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}
141-
asset_name: github-copilot-svcs-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}
142-
asset_content_type: application/octet-stream
144+
asset_path: ./github-copilot-svcs-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}.gz
145+
asset_name: github-copilot-svcs-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}.gz
146+
asset_content_type: application/gzip

README.md

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# GitHub Copilot SVCS Proxy
1+
# GitHub Copilot Service Proxy
22

33
This project provides a reverse proxy for GitHub Copilot, exposing OpenAI-compatible endpoints for use with tools and clients that expect the OpenAI API. It follows the authentication and token management approach used by [OpenCode](https://github.com/sst/opencode).
44

@@ -21,6 +21,8 @@ This project provides a reverse proxy for GitHub Copilot, exposing OpenAI-compat
2121
- **Graceful Shutdown**: Proper signal handling and graceful server shutdown
2222
- **Comprehensive Logging**: Request/response logging for debugging and monitoring
2323
- **Enhanced CLI Commands**: Status monitoring, manual token refresh, and detailed configuration display
24+
- **Production-Ready Performance**: HTTP connection pooling, circuit breaker, request coalescing, and memory optimization
25+
- **Monitoring & Profiling**: Built-in pprof endpoints for memory, CPU, and goroutine analysis
2426

2527
## Downloads
2628

@@ -38,6 +40,38 @@ Releases are automatically created when code is merged to the `main` branch:
3840
- Cross-platform binaries are built and attached to each release
3941
- Release notes include download links for all supported platforms
4042

43+
## Performance & Production Features
44+
45+
This service includes enterprise-grade performance optimizations:
46+
47+
### 🚀 HTTP Server Optimizations
48+
- **Connection Pooling**: Shared HTTP client with configurable connection limits (100 max idle, 20 per host)
49+
- **Configurable Timeouts**: Fully customizable timeout settings via `config.json` for all server operations
50+
- **Streaming Support**: Read (30s), Write (300s), and Idle (120s) timeouts optimized for AI chat streaming
51+
- **Long Response Handling**: HTTP client and proxy context timeouts support up to 300s (5 minutes) for extended AI conversations
52+
- **Request Limits**: 5MB request body size limit to prevent memory exhaustion
53+
- **Advanced Transport**: Configurable dial timeout (10s), TLS handshake timeout (10s), keep-alive (30s)
54+
55+
### 🔄 Reliability & Concurrency
56+
- **Circuit Breaker**: Automatic failure detection and recovery (5 failure threshold, 30s timeout)
57+
- **Context Propagation**: Request contexts with 25s timeout and proper cancellation
58+
- **Request Coalescing**: Deduplicates identical concurrent requests to models endpoint
59+
- **Exponential Backoff**: Enhanced retry logic with circuit breaker integration
60+
- **Worker Pool**: Concurrent request processing with dedicated worker goroutines (CPU*2 workers)
61+
62+
### 💾 Resource Management
63+
- **Buffer Pooling**: sync.Pool for request/response buffer reuse to reduce GC pressure
64+
- **Memory Optimization**: Streaming support with 32KB buffers for large responses
65+
- **Graceful Shutdown**: Proper resource cleanup and coordinated shutdown with worker pool termination
66+
- **Shared Clients**: Centralized HTTP client eliminates resource duplication
67+
- **Worker Pool Management**: Automatic worker lifecycle management with graceful termination
68+
69+
### 📊 Monitoring & Observability
70+
- **Profiling Endpoints**: `/debug/pprof/*` for memory, CPU, and goroutine analysis
71+
- **Enhanced Logging**: Circuit breaker state, request coalescing, worker pool metrics, and performance data
72+
- **Health Monitoring**: Detailed `/health` endpoint for load balancer integration
73+
- **Production Metrics**: Built-in support for operational monitoring and worker pool status
74+
4175
## Quickstart with Makefile
4276

4377
If you have `make` installed, you can build, run, and test the project easily:
@@ -60,14 +94,21 @@ make build
6094
go build -o github-copilot-svcs
6195
```
6296

63-
### 2. First Time Setup & Authentication
97+
### 2. Optional: Configure Timeouts
98+
```bash
99+
# Copy example config and customize timeout values
100+
cp config.example.json ~/.local/share/github-copilot-svcs/config.json
101+
# Edit the timeouts section as needed
102+
```
103+
104+
### 3. First Time Setup & Authentication
64105
```bash
65106
make auth
66107
# or manually:
67108
./github-copilot-svcs auth
68109
```
69110

70-
### 3. Start the Proxy Server
111+
### 4. Start the Proxy Server
71112
```bash
72113
make run
73114
# or manually:
@@ -139,6 +180,15 @@ GET http://localhost:8081/v1/models
139180
GET http://localhost:8081/health
140181
```
141182

183+
### Profiling Endpoints (Production Monitoring)
184+
```bash
185+
GET http://localhost:8081/debug/pprof/ # Overview of available profiles
186+
GET http://localhost:8081/debug/pprof/heap # Memory heap profile
187+
GET http://localhost:8081/debug/pprof/goroutine # Goroutine profile
188+
GET http://localhost:8081/debug/pprof/profile # CPU profile (30s sampling)
189+
GET http://localhost:8081/debug/pprof/trace # Execution trace
190+
```
191+
142192
## Reliability & Error Handling
143193

144194
### Automatic Token Management
@@ -182,7 +232,19 @@ The configuration is stored in `~/.local/share/github-copilot-svcs/config.json`:
182232
"github_token": "gho_...",
183233
"copilot_token": "ghu_...",
184234
"expires_at": 1720000000,
185-
"refresh_in": 1500
235+
"refresh_in": 1500,
236+
"timeouts": {
237+
"http_client": 300,
238+
"server_read": 30,
239+
"server_write": 300,
240+
"server_idle": 120,
241+
"proxy_context": 300,
242+
"circuit_breaker": 30,
243+
"keep_alive": 30,
244+
"tls_handshake": 10,
245+
"dial_timeout": 10,
246+
"idle_conn_timeout": 90
247+
}
186248
}
187249
```
188250

@@ -194,6 +256,32 @@ The configuration is stored in `~/.local/share/github-copilot-svcs/config.json`:
194256
- `expires_at`: Unix timestamp when the Copilot token expires
195257
- `refresh_in`: Seconds until token should be refreshed (typically 1500 = 25 minutes)
196258

259+
### Timeout Configuration
260+
261+
All timeout values are specified in seconds and have sensible defaults:
262+
263+
| Field | Default | Description |
264+
|-------|---------|-------------|
265+
| `http_client` | 300 | HTTP client timeout for outbound requests to GitHub Copilot API |
266+
| `server_read` | 30 | Server timeout for reading incoming requests |
267+
| `server_write` | 300 | Server timeout for writing responses (increased for streaming) |
268+
| `server_idle` | 120 | Server timeout for idle connections |
269+
| `proxy_context` | 300 | Request context timeout for proxy operations |
270+
| `circuit_breaker` | 30 | Circuit breaker recovery timeout when API is failing |
271+
| `keep_alive` | 30 | TCP keep-alive timeout for HTTP connections |
272+
| `tls_handshake` | 10 | TLS handshake timeout |
273+
| `dial_timeout` | 10 | Connection dial timeout |
274+
| `idle_conn_timeout` | 90 | Idle connection timeout in connection pool |
275+
276+
**Streaming Support**: The service is optimized for long-running streaming chat completions with timeouts up to 300 seconds (5 minutes) to support extended AI conversations.
277+
278+
**Custom Configuration**: You can copy `config.example.json` as a starting point and modify timeout values based on your environment:
279+
280+
```bash
281+
cp config.example.json ~/.local/share/github-copilot-svcs/config.json
282+
# Edit the timeouts section as needed
283+
```
284+
197285
## Authentication Flow
198286

199287
The authentication follows GitHub Copilot's OAuth device flow:

auth.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ const (
1717
copilotAPIKeyURL = "https://api.github.com/copilot_internal/v2/token"
1818
copilotClientID = "Iv1.b507a08c87ecfe98"
1919
copilotScope = "read:user"
20-
userAgent = "GitHubCopilotChat/0.26.7"
21-
20+
userAgent = "GitHubCopilotChat/0.26.7"
21+
2222
// Retry configuration
2323
maxRefreshRetries = 3
24-
baseRetryDelay = 2 // seconds
24+
baseRetryDelay = 2 // seconds
2525
)
2626

2727
type deviceCodeResponse struct {
@@ -53,7 +53,7 @@ func authenticate(cfg *Config) error {
5353
log.Printf("Token still valid: expires in %d seconds", cfg.ExpiresAt-now)
5454
return nil // Already authenticated
5555
}
56-
56+
5757
if cfg.CopilotToken != "" {
5858
log.Printf("Token expired or expiring soon: expires in %d seconds, triggering re-auth", cfg.ExpiresAt-now)
5959
} else {
@@ -72,8 +72,7 @@ func authenticate(cfg *Config) error {
7272
body := fmt.Sprintf(`{"client_id":"%s","scope":"%s"}`, copilotClientID, copilotScope)
7373
req.Body = io.NopCloser(strings.NewReader(body))
7474

75-
client := &http.Client{}
76-
resp, err := client.Do(req)
75+
resp, err := sharedHTTPClient.Do(req)
7776
if err != nil {
7877
return err
7978
}
@@ -127,8 +126,7 @@ func pollForGitHubToken(deviceCode string, interval int) (string, error) {
127126
copilotClientID, deviceCode)
128127
req.Body = io.NopCloser(strings.NewReader(body))
129128

130-
client := &http.Client{}
131-
resp, err := client.Do(req)
129+
resp, err := sharedHTTPClient.Do(req)
132130
if err != nil {
133131
continue
134132
}
@@ -160,8 +158,7 @@ func getCopilotToken(githubToken string) (string, int64, int64, error) {
160158
req.Header.Set("Authorization", "token "+githubToken)
161159
req.Header.Set("User-Agent", userAgent)
162160

163-
client := &http.Client{}
164-
resp, err := client.Do(req)
161+
resp, err := sharedHTTPClient.Do(req)
165162
if err != nil {
166163
return "", 0, 0, err
167164
}
@@ -188,14 +185,14 @@ func refreshToken(cfg *Config) error {
188185
// Retry with exponential backoff
189186
for attempt := 1; attempt <= maxRefreshRetries; attempt++ {
190187
log.Printf("Attempting to refresh Copilot token (attempt %d/%d)", attempt, maxRefreshRetries)
191-
188+
192189
copilotToken, expiresAt, refreshIn, err := getCopilotToken(cfg.GitHubToken)
193190
if err != nil {
194191
if attempt == maxRefreshRetries {
195192
log.Printf("Token refresh failed after %d attempts: %v", maxRefreshRetries, err)
196193
return err
197194
}
198-
195+
199196
// Wait before retry with exponential backoff
200197
waitTime := time.Duration(baseRetryDelay*attempt*attempt) * time.Second
201198
log.Printf("Token refresh failed (attempt %d), retrying in %v: %v", attempt, waitTime, err)
@@ -210,6 +207,6 @@ func refreshToken(cfg *Config) error {
210207

211208
return saveConfig(cfg)
212209
}
213-
210+
214211
return errors.New("maximum retry attempts exceeded")
215212
}

cli.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"flag"
55
"fmt"
66
"net/http"
7+
_ "net/http/pprof"
78
"os"
89
"time"
910
)
@@ -27,6 +28,9 @@ func handleAuth() error {
2728
return fmt.Errorf("failed to load config: %v", err)
2829
}
2930

31+
// Initialize timeout configurations before any HTTP operations
32+
initializeTimeouts(cfg)
33+
3034
fmt.Println("Starting GitHub Copilot authentication...")
3135
if err := authenticate(cfg); err != nil {
3236
return fmt.Errorf("authentication failed: %v", err)
@@ -51,13 +55,13 @@ func handleStatus() error {
5155
now := getCurrentTime()
5256
if cfg.CopilotToken != "" {
5357
fmt.Printf("Authentication: ✓ Authenticated\n")
54-
58+
5559
timeUntilExpiry := cfg.ExpiresAt - now
5660
if timeUntilExpiry > 0 {
5761
minutes := timeUntilExpiry / 60
5862
seconds := timeUntilExpiry % 60
5963
fmt.Printf("Token expires: in %dm %ds (%d seconds)\n", minutes, seconds, timeUntilExpiry)
60-
64+
6165
// Show refresh timing
6266
if cfg.RefreshIn > 0 {
6367
refreshThreshold := cfg.RefreshIn / 5 // 20%
@@ -74,7 +78,7 @@ func handleStatus() error {
7478
fmt.Printf("Token expires: ⚠️ EXPIRED (%d seconds ago)\n", -timeUntilExpiry)
7579
fmt.Printf("Status: ❌ Token needs refresh\n")
7680
}
77-
81+
7882
fmt.Printf("Has GitHub token: %t\n", cfg.GitHubToken != "")
7983
if cfg.RefreshIn > 0 {
8084
fmt.Printf("Refresh interval: %d seconds\n", cfg.RefreshIn)
@@ -115,6 +119,9 @@ func handleRun() error {
115119
return fmt.Errorf("failed to load config: %v", err)
116120
}
117121

122+
// Initialize timeout configurations before any HTTP operations
123+
initializeTimeouts(cfg)
124+
118125
// Ensure we're authenticated
119126
if err := ensureValidToken(cfg); err != nil {
120127
return fmt.Errorf("authentication failed: %v", err)
@@ -126,15 +133,24 @@ func handleRun() error {
126133
mux.HandleFunc("/v1/models", modelsHandler(cfg))
127134
mux.HandleFunc("/v1/chat/completions", proxyHandler(cfg))
128135
mux.HandleFunc("/health", healthHandler)
136+
// Add pprof endpoints for profiling
137+
mux.HandleFunc("/debug/pprof/", http.DefaultServeMux.ServeHTTP)
138+
mux.HandleFunc("/debug/pprof/cmdline", http.DefaultServeMux.ServeHTTP)
139+
mux.HandleFunc("/debug/pprof/profile", http.DefaultServeMux.ServeHTTP)
140+
mux.HandleFunc("/debug/pprof/symbol", http.DefaultServeMux.ServeHTTP)
141+
mux.HandleFunc("/debug/pprof/trace", http.DefaultServeMux.ServeHTTP)
129142

130143
port := cfg.Port
131144
if port == 0 {
132145
port = 8081
133146
}
134147

135148
server := &http.Server{
136-
Addr: fmt.Sprintf(":%d", port),
137-
Handler: mux,
149+
Addr: fmt.Sprintf(":%d", port),
150+
Handler: mux,
151+
ReadTimeout: time.Duration(cfg.Timeouts.ServerRead) * time.Second,
152+
WriteTimeout: time.Duration(cfg.Timeouts.ServerWrite) * time.Second,
153+
IdleTimeout: time.Duration(cfg.Timeouts.ServerIdle) * time.Second,
138154
}
139155

140156
setupGracefulShutdown(server)
@@ -158,6 +174,9 @@ func handleModels() error {
158174
return fmt.Errorf("failed to load config: %v", err)
159175
}
160176

177+
// Initialize timeout configurations before any HTTP operations
178+
initializeTimeouts(cfg)
179+
161180
// Ensure we're authenticated
162181
if err := ensureValidToken(cfg); err != nil {
163182
return fmt.Errorf("authentication failed: %v", err)
@@ -189,6 +208,9 @@ func handleRefresh() error {
189208
return fmt.Errorf("failed to load config: %v", err)
190209
}
191210

211+
// Initialize timeout configurations before any HTTP operations
212+
initializeTimeouts(cfg)
213+
192214
if cfg.CopilotToken == "" {
193215
return fmt.Errorf("no token to refresh - run 'auth' command first")
194216
}
@@ -199,13 +221,13 @@ func handleRefresh() error {
199221
}
200222

201223
fmt.Printf("✅ Token refresh successful!\n")
202-
224+
203225
// Show new expiration time
204226
now := getCurrentTime()
205227
timeUntilExpiry := cfg.ExpiresAt - now
206228
minutes := timeUntilExpiry / 60
207229
seconds := timeUntilExpiry % 60
208230
fmt.Printf("New token expires in: %dm %ds\n", minutes, seconds)
209-
231+
210232
return nil
211233
}

config.example.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"port": 8081,
3+
"timeouts": {
4+
"http_client": 300,
5+
"server_read": 30,
6+
"server_write": 300,
7+
"server_idle": 120,
8+
"proxy_context": 300,
9+
"circuit_breaker": 30,
10+
"keep_alive": 30,
11+
"tls_handshake": 10,
12+
"dial_timeout": 10,
13+
"idle_conn_timeout": 90
14+
}
15+
}

0 commit comments

Comments
 (0)