Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/target
**/*.rs.bk
Cargo.lock.orig

# Python bytecode from scripts/
__pycache__/
*.py[cod]
339 changes: 339 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ name = "ghc-proxy"
path = "src/main.rs"

[dependencies]
axum = { version = "0.8", features = ["json"] }
axum = { version = "0.8", features = ["json", "ws"] }
tokio = { version = "1", features = ["full"] }
# Upstream transport for the catalog's `ws:/responses` surface. rustls with the
# platform trust store, to match reqwest and keep enterprise CAs working.
tokio-tungstenite = { version = "0.28", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.7", features = ["fs"] }
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "stream", "form"] }
Expand All @@ -38,3 +41,6 @@ self_update = { version = "0.44", default-features = false, features = ["reqwest
toml = "1.1"

[dev-dependencies]
# `test-util` (not part of tokio's `full`) enables the paused clock used to
# exercise the SSE keepalive timing deterministically.
tokio = { version = "1", features = ["full", "test-util"] }
49 changes: 40 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ file.
upstream model supports it, otherwise translated through chat completions).
- **Gemini-compatible** `/v1beta/models/{model}:generateContent`,
`:streamGenerateContent`, and `:countTokens` endpoints (translated through chat
completions).
completions). `generationConfig` parameters with an OpenAI counterpart are
carried across — `temperature`, `topP`, `maxOutputTokens`, `stopSequences`,
`candidateCount`, `seed`, the penalties, and `responseMimeType`/`responseSchema`
as `response_format`. `topK` and `safetySettings` have no counterpart and are
dropped, which the dashboard states rather than leaving you to assume they
took effect.
- **GitHub Models inference** — requests whose model id uses the
`publisher/model` form (e.g. `openai/gpt-4o`) are transparently routed to the
[GitHub Models](https://models.github.ai) API instead of Copilot, authenticated
Expand All @@ -59,7 +64,23 @@ file.
- **Copilot token management** with automatic refresh.
- **Orphaned `tool_use_id` recovery** — retries with offending tool results
stripped when the upstream returns the corresponding 400 error.
- **Request analytics dashboard** at `/` and a request browser at `/requests`.
- **The Responses API over WebSocket.** Ten models advertise a `ws:/responses`
transport in the Copilot catalog. `GET /v1/responses` with an `Upgrade:
websocket` header relays to it, recorded like any other request. A model that
does not advertise the transport is refused with an error frame naming the
endpoint that does work.
- **Request analytics dashboard** across three pages: an overview at `/` led by
what Copilot actually billed (in AI units, read from `copilot_usage`, not
estimated from a price list), a request browser at `/requests` where each
exchange renders as a conversation with the raw frames a tab away, and a
metrics UI at `/metrics/dashboard`. Prompt-cache statistics per model at
`GET /api/cache` show where input tokens came from and what the cache was
worth. Failed attempts are counted separately rather than diluting the
statistics.
- **Body capture toggled at runtime** from the dashboard or
`POST /api/config/debug`, so you can start recording request and response
bodies without a restart. Not persisted to `config.yaml`, since capture puts
prompts into memory and the log and should lapse on restart.
- **Interactive setup wizard** (`--setup`, or first launch in a terminal):
GitHub sign-in, live model catalog, and model-mapping configuration.
- **1M-context support** — forwards the `anthropic-beta: context-1m-2025-08-07`
Expand Down Expand Up @@ -91,6 +112,7 @@ ghc-proxy [options]
--fetch-version Fetch the latest VS Code version at startup
--no-fetch-version Disable dynamic VS Code version fetching
--auto-upgrade Auto-upgrade app when a newer release is available
(default: on)
--no-auto-upgrade Disable app auto-upgrade
--update-config Persist non-schema config write-backs (schema upgrades apply automatically)
-v, --version Show version
Expand Down Expand Up @@ -157,22 +179,22 @@ Config file: `~/.ghc-tunnel/config.yaml` (`%APPDATA%/ghc-tunnel/config.yaml`
on Windows). It is generated on first run or with `--config`.

```yaml
config_version: 2
config_version: 4
address: 127.0.0.1
port: 8314
debug: false
account_type: individual # individual | business | enterprise
vscode_version: "1.123.0"
vscode_version: "1.130.0"
api_version: "2025-05-01"
copilot_version: "0.48.1"
auto_upgrade: false
auto_upgrade: true # self-update on startup; false to disable
model_mappings:
exact:
opus: claude-opus-4.8
sonnet: claude-opus-4.8
opus: claude-opus-5
sonnet: claude-opus-5
haiku: claude-haiku-4.5
prefix:
claude-sonnet-4-: claude-opus-4.8
claude-sonnet-4-: claude-opus-5
github_models:
enabled: true # route publisher/model ids to GitHub Models
# org: my-org # attribute inference to an organization
Expand All @@ -181,6 +203,7 @@ system_prompt_remove: []
system_prompt_add: []
tool_result_suffix_remove: []
max_connection_retries: 3
upstream_read_timeout_seconds: 900 # max silence from upstream; 0 disables

# Optional: require this key on all LLM endpoints (Bearer / x-api-key /
# x-goog-api-key). Omit or leave empty to disable authentication.
Expand Down Expand Up @@ -239,6 +262,7 @@ the dashboard and model listings.
|----------|-------------|
| `POST /v1/chat/completions` | OpenAI chat completions |
| `POST /v1/responses` | OpenAI responses API (Codex) |
| `GET /v1/responses` *(with `Upgrade: websocket`)* | Responses API over WebSocket, for models advertising `ws:/responses` |
| `GET /v1/models` | List available models |
| `GET /v1/models/{model}` | Retrieve a single model (aliases resolved) |
| `POST /v1/messages` | Anthropic messages API |
Expand All @@ -248,13 +272,20 @@ the dashboard and model listings.
| `POST /v1beta/models/{model}:countTokens` | Gemini token counting |
| `GET /health` | Liveness/readiness probe (`?strict=true` for 503 when not ready) |
| `GET /openapi.json` | OpenAPI v3 specification |
| `GET /` | Web dashboard |
| `GET /` | Web dashboard — overview |
| `GET /metrics/dashboard` | Metrics dashboard UI |
| `GET /metrics` | OpenMetrics endpoint |
| `GET /requests` | Request browser |
| `GET /app.css` | Stylesheet shared by the three dashboard pages |
| `POST /api/config/reload` | Reload config.yaml without restart |
| `POST /api/config/debug` | Turn body capture on or off (`{"debug": true}`) |
| `GET /api/stats` | Running totals, including what Copilot billed |
| `GET /api/cache` | Prompt-cache statistics, overall and per model |
| `GET /api/models` | All supported models (used by the dashboard) |

The `/api/config/` routes are guarded by the API key when one is configured;
the read-only dashboard endpoints stay open.

## Example Usage

### OpenAI SDK
Expand Down
Loading