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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Fixed
- **`sonnet` now maps to Sonnet, not Opus.** The default model mappings folded
every Claude spelling — Sonnet included — into the newest Opus. That is
right for aliases actually named after a Claude version Copilot dropped, but
`sonnet` and every `claude-sonnet-*` spelling now resolve to `claude-sonnet-5`
instead, so a caller that asked for the mid tier gets its cost and rate
limits rather than being silently upgraded to Opus.

### Removed
- Removed GitHub Models routing, catalog merging, token configuration, and setup
now that the GitHub Models inference service has been retired. All model
requests now use GitHub Copilot.

## [1.4.3] - 2026-08-09

### Added
Expand Down
74 changes: 11 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ file.
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
with a token that has the `models: read` permission. Enabled by
default; the catalog is merged into `/v1/models`.
- **Optional API-key authentication** on the LLM endpoints
(`Authorization: Bearer`, `x-api-key`, or `x-goog-api-key`), disabled by
default and compared in constant time.
Expand Down Expand Up @@ -134,11 +129,7 @@ The GitHub token is exchanged for a short-lived **Copilot token** via
`https://api.github.com/copilot_internal/v2/token`, which is refreshed
automatically before it expires.

The interactive Device Flow requests the `read:user copilot` scopes. GitHub
Models is **not** covered by the Device Flow token (`models` is not a valid
classic OAuth scope). To use the [GitHub Models](#github-models) inference API,
supply a dedicated token with the `models: read` permission (a fine-grained PAT)
via `github_models.token`.
The interactive Device Flow requests the `read:user copilot` scopes.

## Endpoint Authentication

Expand Down Expand Up @@ -180,7 +171,7 @@ 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: 4
config_version: 6
address: 127.0.0.1
port: 8314
debug: false
Expand All @@ -192,14 +183,10 @@ auto_upgrade: true # self-update on startup; false to disable
model_mappings:
exact:
opus: claude-opus-5
sonnet: claude-opus-5
sonnet: claude-sonnet-5
haiku: claude-haiku-4.5
prefix:
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
# token: ghp_xxx # dedicated token (models: read permission)
claude-sonnet-4-: claude-sonnet-5
system_prompt_remove: []
system_prompt_add: []
tool_result_suffix_remove: []
Expand All @@ -211,52 +198,6 @@ upstream_read_timeout_seconds: 900 # max silence from upstream; 0 disables
# api_key: my-secret-key
```

## GitHub Models

Besides Copilot, GitHub offers a separate model **inference** service —
[GitHub Models](https://models.github.ai) — exposing OpenAI-compatible endpoints
for models from OpenAI, Meta, Mistral, xAI, DeepSeek, and others. This proxy
routes to it transparently.

**Routing.** GitHub Models identifies models by a `publisher/model` id (e.g.
`openai/gpt-4o`, `meta/llama-4-maverick`). When `github_models.enabled` is true
(the default), any request whose *translated* model id contains a `/` is sent to
GitHub Models instead of Copilot. Because Copilot model ids never contain a `/`,
the two never collide, and existing model mappings are unaffected. This works on
`/v1/chat/completions`, `/v1/messages` (translated), and the Gemini endpoints.

```bash
curl http://127.0.0.1:8314/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hi!"}]}'
```

**Authentication.** GitHub Models uses the raw GitHub token (not the Copilot
token) via `Authorization: Bearer`. The token must carry the **`models: read`**
permission (a fine-grained PAT). The token minted by the Device Flow does **not**
have this permission (`models` is not a valid classic OAuth scope), so set a
dedicated token via `github_models.token` or the
`GHC_PROXY_GITHUB_MODELS_TOKEN` environment variable. Tokens without the
permission get an `Unauthorized` response from GitHub.

**Configuration.**

```yaml
github_models:
enabled: true # set false to always use Copilot
org: my-org # optional: attribute inference to an organization
token: ghp_xxx # optional: dedicated token (models: read permission)
```

| Environment variable | Effect |
|----------------------|--------|
| `GHC_PROXY_GITHUB_MODELS_ENABLED` | Enable/disable routing (`true`/`1`) |
| `GHC_PROXY_GITHUB_MODELS_ORG` | Attribute inference to an organization |
| `GHC_PROXY_GITHUB_MODELS_TOKEN` | Dedicated token for GitHub Models |

The GitHub Models catalog is merged into `GET /v1/models` so those ids show up in
the dashboard and model listings.

## API Endpoints

| Endpoint | Description |
Expand All @@ -271,6 +212,9 @@ the dashboard and model listings.
| `POST /v1beta/models/{model}:generateContent` | Gemini generate content |
| `POST /v1beta/models/{model}:streamGenerateContent` | Gemini streaming (SSE) |
| `POST /v1beta/models/{model}:countTokens` | Gemini token counting |
| `POST /v1/embeddings` | Embeddings (also `/embeddings`) |
| `GET /v1/models/full/` | Raw upstream model catalog with capabilities |
| `GET /usage` | Copilot plan and quota usage (also `check-usage`) |
| `GET /health` | Liveness/readiness probe (`?strict=true` for 503 when not ready) |
| `GET /openapi.json` | OpenAPI v3 specification |
| `GET /` | Web dashboard — overview |
Expand All @@ -282,6 +226,9 @@ the dashboard and model listings.
| `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/requests` | Recent requests (JSON) |
| `GET /api/audit` | Filtered audit records |
| `GET /api/audit/summary` | Aggregated audit summary |
| `GET /api/models` | All supported models (used by the dashboard) |

The `/api/config/` routes are guarded by the API key when one is configured;
Expand Down Expand Up @@ -335,6 +282,7 @@ cargo clippy # lint
| File | Responsibility |
|------|----------------|
| `src/main.rs` | CLI parsing and server startup |
| `src/lib.rs` | Library crate root re-exporting the modules below |
| `src/setup.rs` | Interactive first-run setup wizard |
| `src/config.rs` | Config dir, YAML config, defaults, model-mapping defaults |
| `src/auth.rs` | GitHub token resolution (env/file/Device Flow), Copilot token exchange |
Expand Down
5 changes: 0 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,6 @@ counts, estimated cost, and prompt-cache hit rate.

## Notable behaviors

- **GitHub Models routing** — when enabled (default), requests whose translated
model id uses the `publisher/model` form (e.g. `openai/gpt-4o`) are routed to
the [GitHub Models](https://models.github.ai) inference API instead of Copilot,
authenticated with a token that has the `models: read` permission.
See [Configuration](configuration.md#github-models).
- **Model translation** — model names are rewritten per your
[mappings](configuration.md#model-mappings) before being forwarded.
- **1M context** — for Anthropic-native requests, the proxy forwards the
Expand Down
7 changes: 4 additions & 3 deletions docs/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ model_mappings:
claude-opus-4-8: claude-opus-5
```

The built-in defaults already do this for every Claude spelling. They apply to
a *new* config file only — an existing one keeps the targets it has, so if you
wrote yours before Opus 5 shipped, either edit it or re-run `--setup`.
The built-in defaults already do this for every Opus and Sonnet spelling. They
apply to a *new* config file only — an existing one keeps the targets it has,
so if you wrote yours before Opus 5 shipped, either edit it or re-run
`--setup`.

Restart the proxy after editing `config.yaml` — mappings are read at startup.

Expand Down
42 changes: 7 additions & 35 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Windows). Generated on first run, with `--config`, or through the setup wizard.

```yaml
# Schema version for migration/write-back behavior
config_version: 4
config_version: 6

# Server settings
address: 127.0.0.1
Expand All @@ -46,15 +46,7 @@ model_mappings:
sonnet: claude-sonnet-5
haiku: claude-haiku-4.5
prefix:
claude-sonnet-4-: claude-opus-5

# GitHub Models (https://models.github.ai) inference
# Route publisher/model ids (e.g. openai/gpt-4o) to GitHub Models instead of
# Copilot. Needs a token with the `models: read` permission (fine-grained PAT).
github_models:
enabled: true
# org: my-org
# token: ghp_xxx
claude-sonnet-4-: claude-sonnet-5

# Content filtering
system_prompt_remove: []
Expand Down Expand Up @@ -85,8 +77,9 @@ Incoming model names are rewritten before the request is forwarded upstream:
Exact matches take priority over prefix matches. Unmapped names pass through
unchanged. Use the live catalog at `GET /v1/models` to discover valid targets.

The built-in mappings point every Claude spelling at the newest generally
available Opus — currently `claude-opus-5` — and every Haiku spelling at
The built-in mappings point every Opus spelling at the newest generally
available Opus — currently `claude-opus-5` — every Sonnet spelling at the
newest Sonnet (`claude-sonnet-5`), and every Haiku spelling at
`claude-haiku-4.5`. Anthropic writes the same version two ways (`4.8` and
`4-8`), so both forms are listed.

Expand All @@ -105,26 +98,6 @@ Controls the upstream base URL only:

Set this to match the Copilot seat your token actually has.

### GitHub Models

[GitHub Models](https://models.github.ai) is GitHub's OpenAI-compatible model
**inference** service, separate from Copilot. When `github_models.enabled` is
true (the default), any request whose *translated* model id uses the
`publisher/model` form (contains a `/`, e.g. `openai/gpt-4o`) is routed there
instead of Copilot. These ids never collide with Copilot ids, so mappings and
existing behavior are unaffected. Routing applies to `/v1/chat/completions`,
`/v1/messages` (translated), and the Gemini endpoints.

GitHub Models authenticates with the **raw GitHub token** (not the Copilot
token) via `Authorization: Bearer`. That token must carry the **`models: read`**
permission (a fine-grained PAT). This is **not** covered by the Device Flow login
(`models` is not a valid classic OAuth scope), so supply a dedicated token via
`github_models.token` (or `GHC_PROXY_GITHUB_MODELS_TOKEN`); otherwise GitHub
Models requests fall back to the Device Flow token, which lacks this permission.
The [setup wizard](getting-started.md#the-setup-wizard) can capture and validate
this token for you. Set `github_models.org` to attribute inference to an
organization. The catalog is merged into `GET /v1/models`.

### Schema upgrades

`config_version` records the schema an existing `config.yaml` was written
Expand All @@ -147,6 +120,8 @@ Schema versions so far:
| 2 | Opus 4.8 aliases |
| 3 | `upstream_read_timeout_seconds`; `auto_upgrade` defaulting to true |
| 4 | Opus 5 and Sonnet 5 aliases |
| 5 | Gemini CLI model mappings (`gemini-*` prefixes) |
| 6 | Removed the retired GitHub Models settings from persisted configuration |

`--update-config` remains for the other, non-schema write-backs (for example
restoring the built-in `model_mappings` when the file has none).
Expand Down Expand Up @@ -208,9 +183,6 @@ Every config field has a `GHC_PROXY_*` override:
| `GHC_PROXY_RATE_LIMIT_WAIT` | Wait instead of rejecting when limited (`true`/`1`) |
| `GHC_PROXY_MANUAL_APPROVE` | Require manual approval per request (`true`/`1`) |
| `GHC_PROXY_API_KEY` | Require this key on LLM endpoints (empty = disabled) |
| `GHC_PROXY_GITHUB_MODELS_ENABLED` | Route `publisher/model` ids to GitHub Models (`true`/`1`) |
| `GHC_PROXY_GITHUB_MODELS_ORG` | Attribute GitHub Models inference to an organization |
| `GHC_PROXY_GITHUB_MODELS_TOKEN` | Dedicated token for GitHub Models (`models: read` permission) |

Token-related variables (`COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`) are
covered in [Getting Started](getting-started.md#authentication).
Expand Down
13 changes: 2 additions & 11 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,7 @@ The GitHub token is exchanged for a short-lived **Copilot token** via
`https://api.github.com/copilot_internal/v2/token`, which the proxy refreshes
automatically before it expires.

The Device Flow requests the `read:user copilot` scopes. The
[GitHub Models](configuration.md#github-models) inference API is not covered by
the Device Flow token (`models` is not a valid classic OAuth scope); to use it,
supply a dedicated token with the `models: read` permission (a fine-grained PAT)
via `github_models.token`.
The Device Flow requests the `read:user copilot` scopes.

To authenticate without starting the server (useful for CI/headless setups):

Expand All @@ -98,12 +94,7 @@ It walks through:
3. **Model mappings** — fetches the live model catalog and lets you map the
`opus` / `sonnet` / `haiku` aliases to specific models, or keep the
recommended defaults.
4. **GitHub Models** — optionally enable routing of `publisher/model` ids to the
[GitHub Models](configuration.md#github-models) inference API. The wizard
checks whether your GitHub token already grants access and, if not, guides you
to create a fine-grained PAT with the `models: read` permission, validates the
token against the catalog, and saves it to `github_models.token`.
5. **Client setup** — optionally configure Claude Code
4. **Client setup** — optionally configure Claude Code
(`~/.claude/settings.json`), Codex (`~/.codex/config.toml`), and the Gemini
CLI (`~/.gemini/.env`) to route through the proxy. Existing settings are
preserved and any user-set API key is left untouched.
Expand Down
31 changes: 26 additions & 5 deletions public/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,16 @@ <h2>Prompt cache</h2>
it cannot dominate the page you open to check spend. -->
<details class="panel fold">
<summary id="modelsSummary">Supported models</summary>
<div class="panel-body flush scroll-y">
<div class="panel-body flush scroll-y scroll-x">
<table>
<thead><tr><th>ID</th><th>Display name</th><th>Owned by</th></tr></thead>
<thead>
<tr>
<th>ID</th><th>Display name</th><th>Owned by</th>
<th class="num">Context window</th>
<th class="num">Max output</th>
<th class="num">1M context</th>
</tr>
</thead>
<tbody id="modelsBody"></tbody>
</table>
</div>
Expand Down Expand Up @@ -493,9 +500,23 @@ <h2>Prompt cache</h2>
const data = await (await fetch('/api/models')).json();
const models = (data && data.data) || [];
document.getElementById('modelsSummary').textContent = `Supported models (${models.length})`;
document.getElementById('modelsBody').innerHTML = models.map(m =>
`<tr><td class="mono">${esc(m.id)}</td><td>${esc(m.display_name)}</td><td class="muted">${esc(m.owned_by)}</td></tr>`
).join('');
document.getElementById('modelsBody').innerHTML = models.map(m => {
const context = m.context_window == null ? '—' : compact(m.context_window);
const output = m.max_output_tokens == null ? '—' : compact(m.max_output_tokens);
const extended = m.supports_1m_context == null
? '<span class="muted">—</span>'
: m.supports_1m_context
? '<span class="status-ok">yes</span>'
: '<span class="muted">no</span>';
return `<tr>
<td class="mono">${esc(m.id)}</td>
<td>${esc(m.display_name)}</td>
<td class="muted">${esc(m.owned_by)}</td>
<td class="num">${context}</td>
<td class="num">${output}</td>
<td class="num">${extended}</td>
</tr>`;
}).join('');
}

async function refresh() {
Expand Down
Loading