Claude Code
·
GitHub Copilot
·
A small Python reverse proxy that lets Claude Code and Codex CLI talk to GitHub Copilot - swap the auth header, keep the native protocols. No separate Anthropic or OpenAI API key needed.
Unofficial, unsupported, proof-of-concept. This project is an independent experiment and is not affiliated with, endorsed by, or supported by Microsoft, GitHub, or Anthropic. None of those organizations have reviewed, blessed, or sanctioned it.
It is not a product. There is no warranty, no SLA, no support, and no guarantee of fitness for any purpose. APIs, headers, model availability, and tenant behavior on the upstream services can change at any time and break this proxy without notice.
Use at your own risk. You are solely responsible for ensuring that your use of GitHub Copilot through this proxy complies with your Copilot subscription terms, your employer's acceptable-use policies, and any applicable laws. The authors and contributors accept no liability for account suspension, data loss, billing surprises, security incidents, or any other damages arising from use of this software. See
LICENSEfor the full no-warranty / no-liability terms.
A small Python reverse proxy that runs in Docker. It accepts Anthropic Messages API requests from Claude Code under /claude/v1 and forwards them to Copilot's native /v1/messages endpoint. It accepts OpenAI Responses API requests from Codex CLI under /codex/v1 and forwards them to Copilot's native /responses endpoint. No protocol translation - Copilot speaks both protocols natively for the model families this proxy exposes.
+==============================+ +========================================+ +================================+
| Claude Code | | copilot-cli-relay | | GitHub Copilot |
| Anthropic Messages JSON/SSE | | 127.0.0.1:4141 | | Anthropic native endpoints |
| POST /claude/v1/messages | ====> | Claude route handlers | ====> | POST /v1/messages |
| GET /claude/v1/models | <==== | build_claude_outbound_headers | <==== | GET /models |
+==============================+ | | +================================+
| uvicorn + Starlette |
| shared httpx AsyncClient, HTTP/2 |
| proxy_shared streaming + passthrough |
+==============================+ | | +================================+
| Codex CLI | | Codex route handlers | | GitHub Copilot |
| OpenAI Responses JSON/SSE | | build_codex_outbound_headers | | Responses native endpoints |
| POST /codex/v1/responses | ====> | compact fallback, body cleanup | ====> | POST /responses |
| POST /codex/v1/responses/ | ====> | | ====> | POST /responses/compact |
| compact | | | | or fallback to /responses |
| GET /codex/v1/models | <==== | | <==== | GET /models |
+==============================+ +====================^===================+ +================================+
|
|
.env: COPILOT_GITHUB_TOKEN
extracted from Windows Credential Manager
| Client | Local relay routes | Upstream Copilot routes | Protocol |
|---|---|---|---|
| Claude Code | POST /claude/v1/messagesGET /claude/v1/models |
POST /v1/messagesGET /models |
Anthropic Messages JSON/SSE |
| Codex CLI | POST /codex/v1/responsesPOST /codex/v1/responses/compactGET /codex/v1/models |
POST /responsestries POST /responses/compact, then falls back to POST /responsesGET /models |
OpenAI Responses JSON/SSE |
| Relay step | What happens |
|---|---|
| Auth | Reads COPILOT_GITHUB_TOKEN from .env, originally extracted from Windows Credential Manager. |
| Header safety | Strips local client auth, cookies, host, content length, and hop-by-hop headers. |
| Protocol headers | Adds the Copilot bearer plus Claude-specific or Codex-specific headers. |
| Compatibility | Applies small route-specific body rewrites, then streams the native upstream response format back unchanged. |
The proxy keeps each client on its native protocol. Claude Code sends Anthropic Messages traffic to /claude/v1/messages; Codex CLI sends OpenAI Responses traffic to /codex/v1/responses. The proxy swaps local dummy auth for the GitHub OAuth bearer Copilot expects, adds the Copilot headers each route needs, and leaves response streaming in the original SSE format.
Four things make this work cleanly:
- The
copilotCLI's OAuth token (in Windows Credential Manager) authenticates directly againstapi.githubcopilot.com— no session-token exchange needed. - Copilot exposes a native Anthropic Messages endpoint at
/v1/messagesthat returns proper Anthropic SSE — no API translation needed. - Copilot exposes a native OpenAI Responses endpoint at
/responsesfor GPT/Codex models — no Chat Completions translation needed. - Setting
Copilot-Integration-Id: copilot-developer-cliis what unlocks Claude and GPT-5 Responses models on this Copilot tenant (vscode-chatrejectedgpt-5.5during live testing).
Requires: Docker Desktop, PowerShell 7+ (pwsh), and a working copilot CLI login (copilot then sign in).
# 1. Extract your Copilot OAuth token from Credential Manager to .env
pwsh scripts\extract-token.ps1
# 2. Start the proxy
docker compose up --build
# 3. In another terminal: verify Claude routes
curl http://127.0.0.1:4141/claude/healthz
curl http://127.0.0.1:4141/claude/v1/modelsThen point Claude Code at the proxy:
# Print the Claude Code settings snippet
pwsh scripts\claude-config.ps1
# Optional: update ~/.claude/settings.json and Windows user env after typing YES
pwsh scripts\claude-config.ps1 -WriteThe helper writes the same settings as this manual ~/.claude/settings.json example:
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:4141/claude",
"ANTHROPIC_AUTH_TOKEN": "sk-dummy",
"ANTHROPIC_MODEL": "claude-sonnet-5",
"ANTHROPIC_SMALL_FAST_MODEL": "claude-sonnet-4-6"
},
"model": "claude-sonnet-5",
"effortLevel": "medium"
}Then restart Claude Code and run claude as usual. If Claude Code logs show POST /v1/messages instead of POST /claude/v1/messages, the base URL is missing the /claude suffix; rerun pwsh scripts\claude-config.ps1 -Write.
Requires: Docker Desktop, PowerShell 7+ (pwsh), a working copilot CLI login, and Codex CLI.
# 1. Extract your Copilot OAuth token from Credential Manager to .env
pwsh scripts\extract-token.ps1
# 2. Start the proxy if it is not already running
docker compose up --build
# 3. In another terminal: verify Codex routes
curl http://127.0.0.1:4141/codex/healthz
curl http://127.0.0.1:4141/codex/v1/modelsCodex uses its own path prefix so Claude Code's /claude/v1/models response stays Anthropic-shaped while Codex gets OpenAI Responses-compatible routes:
# Print the Codex config snippet
pwsh scripts\codex-config.ps1
# Optional: write ~/.codex/config.toml + ~/.codex/copilot.config.toml after typing YES
pwsh scripts\codex-config.ps1 -WriteThe helper configures two files. The provider lives in ~/.codex/config.toml:
[model_providers.copilot]
name = "GitHub Copilot via local relay"
base_url = "http://127.0.0.1:4141/codex/v1"
wire_api = "responses"
requires_openai_auth = false
request_max_retries = 4
stream_max_retries = 10
stream_idle_timeout_ms = 300000
[model_providers.copilot.auth]
command = "pwsh"
args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "[Console]::Out.Write('dummy')"]
timeout_ms = 5000
refresh_interval_ms = 0The profile lives in its own ~/.codex/copilot.config.toml (top-level keys, not a [profiles.copilot] table). Codex 0.134+ overlays this file when you pass --profile copilot and no longer reads a legacy [profiles.copilot] table or a profile = "copilot" selector from config.toml; -Write removes any such legacy tables for you:
# ~/.codex/copilot.config.toml
model_provider = "copilot"
model = "gpt-5.6-sol"
model_reasoning_effort = "medium"Then start Codex with the relay profile. The proxy strips the dummy client auth and injects your Copilot OAuth bearer upstream:
codex -p copilotThe command-auth block emits a non-secret local dummy token. Codex 0.143+ only refreshes /models for custom providers with command auth; an env_key provider falls back to Codex's bundled picker and hides Copilot models such as GPT-5.6. The relay discards the dummy token and injects your Copilot OAuth bearer upstream. The proxy also strips Codex's unsupported image_generation tool and previous_response_id before forwarding. web_search is preserved because this tenant's Copilot /responses endpoint accepts it.
Useful Codex checks:
# Confirm Codex can read the proxy model catalog
codex debug models -c model_provider='"copilot"' -c model='"gpt-5.6-sol"'
# Minimal end-to-end request through the proxy
codex exec --skip-git-repo-check --ephemeral --sandbox read-only -p copilot "Reply with exactly ok."/codex/v1/models returns both OpenAI's model-list shape (object, data) and Codex's model catalog shape (models) because current Codex CLI tooling uses the richer catalog for discovery.
Interactive menu wrapping the common docker compose flows. Run it with no
arguments and pick a numbered action:
pwsh scripts\proxy.ps1copilot-cli-relay - pick an action:
1) start Start container (build only if image missing)
2) stop Stop and remove container + network
3) restart Restart to pick up src/ edits (starts if down)
4) status Show container status + port bind
5) rebuild Rebuild image and recreate container
6) claude Check Claude routes: /claude/healthz + /claude/v1/models
7) codex Check Codex routes: /codex/healthz + /codex/v1/models
8) quit Exit without doing anything
For scripted use, prefer the underlying docker compose commands directly.
Claude Code resolves the active model from three places, in this order of precedence:
- Top-level
"model"insettings.json— what the UI shows as your default and what/modelswitches between in-session. ANTHROPIC_MODELenv var — the fallback used when no top-level"model"is set.ANTHROPIC_SMALL_FAST_MODELenv var — used for cheap/quick background calls (title generation, tool-name guesses, etc.). Point this at a Haiku- or Sonnet-tier model so background traffic doesn't burn Opus quota.
For consistency, set both the top-level "model" and ANTHROPIC_MODEL to the same id so a stale env var can't silently override your settings.
A few things to know about the IDs:
- Use the dash form (
claude-opus-4-7, notclaude-opus-4.7). The proxy canonicalizes/claude/v1/modelsoutput to dash form because that's the shape Claude Code recognizes as a known Anthropic model — using it unlocks the right request shape (adaptive thinking, etc.). Both forms work upstream, but dash is what you want here. effortLevelis Claude Code's reasoning-effort knob. Valid values arelow,medium,high. The proxy clamps each request to the values the target model actually accepts, read live from Copilot's/modelscapabilities (cached, ~5 min) and falling back to a small built-in table only if/modelsis unreachable. Currently Opus 4.8 and Opus 4.7 accept onlymedium, and Haiku 4.5 / Opus 4.5 / Sonnet 4.5 don't support reasoning effort at all (the field is stripped). If you sethighfor an Opus 4.8 / 4.7 default, the proxy quietly downgrades it tomediumrather than letting the request 400 — and because the allowed set is read from upstream, new models are handled automatically without a proxy update. Copilot's endpoint also moved this knob: it now wantsoutput_config.effort(not a top-levelreasoning_effort) andthinking.type: "adaptive"(not"enabled"with abudget_tokens). The proxy rewrites both legacy shapes for you, so Claude Code's older request bodies keep working.- 1M context: Copilot exposes 1M-context variants for Opus 4.6 and 4.7 only —
/claude/v1/modelsadvertises them asclaude-opus-4-6-1mandclaude-opus-4-7-1m-internal(dash form, what Claude Code's/modelvalidation accepts). The proxy converts these ids to the dot form (claude-opus-4.6-1m/claude-opus-4.7-1m-internal) only at the last hop before forwarding upstream — Copilot returnsmodel_not_supportedfor the dash form on these specific ids, while accepting both forms for every other model. When Claude Code's hardcoded "Opus 4.7 (1M context)" picker tier sends the standardclaude-opus-4-7id plus thecontext-1m-2025-08-07beta header, the proxy auto-rewrites the model id to the 1M variant so you actually get 1M context (Copilot rejects the beta header itself; the-1mmodel id is the real switch). To pin a 1M variant as your session default, set"model": "claude-opus-4-7-1m-internal"insettings.json. Sonnet has no 1M variant on this tenant — picker tier "Sonnet (1M context)" silently downgrades to 200K because there's nothing to remap to. - Switching mid-session:
/model claude-sonnet-4-6inside Claude Code changes the model for the current conversation without editingsettings.json.
Optional env vars to consider adding to the "env" block:
"DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1"— skip the small/fast background calls entirely if you don't want any haiku traffic."CLAUDE_CODE_ATTRIBUTION_HEADER": "0"— drop theX-Claude-Code-…attribution header from outbound requests.
| Environment variable | Default | Purpose |
|---|---|---|
COPILOT_GITHUB_TOKEN |
- | Required. Set by extract-token.ps1. |
COPILOT_PROXY_PORT |
4141 |
Read only by the python -m copilot_cli_relay entrypoint. The Docker CMD hardcodes --port 4141, so changing this in .env has no effect when running via docker compose. Change the published port in docker-compose.yml instead. |
COPILOT_PROXY_HOST |
127.0.0.1 |
Read only by the python -m copilot_cli_relay entrypoint, which the Docker CMD does not use. Only relevant if you bypass the default CMD inside the container. |
COPILOT_API_BASE |
https://api.githubcopilot.com |
Override upstream base. Must be https:// unless COPILOT_ALLOW_INSECURE_API_BASE=1 is also set for mocks/tests. |
COPILOT_ALLOW_INSECURE_API_BASE |
0 |
Set to 1 to permit non-https COPILOT_API_BASE for local mocks. Default refuses. |
COPILOT_INTEGRATION_ID |
copilot-developer-cli |
Do not change unless you know your org needs a different value. |
COPILOT_EDITOR_VERSION |
copilot-cli-relay/<package version> |
Sent as Editor-Version and User-Agent. Defaults to __version__ in src/copilot_cli_relay/__init__.py. |
COPILOT_CODEX_INTEGRATION_ID |
value of COPILOT_INTEGRATION_ID, normally copilot-developer-cli |
Sent only on Codex /codex/v1/responses and /codex/v1/models. Live testing on this tenant showed copilot-developer-cli exposes GPT-5 Responses models. |
COPILOT_CODEX_EDITOR_VERSION |
vscode/1.99.0 |
VS Code-style Editor-Version for Copilot Responses requests. |
COPILOT_CODEX_PLUGIN_VERSION |
copilot-chat/0.43.2026033101 |
VS Code Copilot Chat plugin version for Responses requests. |
COPILOT_CODEX_USER_AGENT |
GitHubCopilotChat/0.43.2026033101 |
User agent for Responses requests. |
COPILOT_CODEX_GITHUB_API_VERSION |
2026-01-09 |
x-github-api-version for Responses requests. |
COPILOT_CODEX_SESSION_ID |
generated per container start | Optional stable VS Code session id for the process lifetime. |
COPILOT_CODEX_MACHINE_ID |
generated per container start | Optional stable VS Code machine id for the process lifetime. |
LOG_LEVEL |
info |
Set to debug for header-level debug. |
LOG_BODIES |
0 |
Set to 1 to log redacted request/response bodies. Bodies contain your source code; off by default. |
/claude/v1/modelsreturns no Claude models: yourCOPILOT_INTEGRATION_IDis wrong, or your org policy doesn't include Claude models for your account.- 401 from upstream: your token rotated. Re-run
pwsh scripts\extract-token.ps1anddocker compose restart proxy. COPILOT_GITHUB_TOKEN is required: runextract-token.ps1first; it writes.env.- Source edits not picked up: the container runs uvicorn without
--reload. Rundocker compose restart proxyafter editingsrc/. (Bind-mount file events on Windows are also flaky, so the restart is the reliable path.) Unable to connect to API (ConnectionRefused)in Claude Code: the container isn't running.docker compose up -d proxybrings it back. The compose file usesrestart: unless-stopped, so this should only happen after a cleandocker compose down.- Claude Code posts to
/v1/messagesand gets 404: your Claude base URL is missing the namespace. Runpwsh scripts\claude-config.ps1 -Write, restart Claude Code, and confirmANTHROPIC_BASE_URLishttp://127.0.0.1:4141/claude. - Codex says stream disconnected before completion: confirm the proxy is running and Codex is pointed at
http://127.0.0.1:4141/codex/v1, not the Claude base URLhttp://127.0.0.1:4141/claude. - Codex
/modelonly shows bundled models such as GPT-5.5: runpwsh scripts\codex-config.ps1 -Writeto replace the oldenv_keyprovider with command auth, then restart Codex. - Codex 401 or missing auth: re-run
pwsh scripts\codex-config.ps1 -Writeand confirm[model_providers.copilot.auth]is present in~/.codex/config.toml. - Codex errors with
--profile copilot cannot be used while ... contains legacy profile = "copilot" or [profiles.copilot] config: Codex 0.134+ moved profiles into their own file. Re-runpwsh scripts\codex-config.ps1 -Write(it removes the legacy[profiles.copilot*]tables fromconfig.tomland writes the profile to~/.codex/copilot.config.toml). To migrate by hand, move the keys under[profiles.copilot]into~/.codex/copilot.config.tomlas top-level keys (drop theprofiles.copilotprefix) and delete the legacy tables. - Codex model not supported: run
curl http://127.0.0.1:4141/codex/v1/modelsand pick a model listed there. The current default isgpt-5.6-solwhen your Copilot tenant exposes it. - Codex model discovery:
/codex/v1/modelsincludes both OpenAI-compatibledataand Codex-compatiblemodelsfields.
Upstream model availability and request schemas change without notice. Reasoning-effort values are handled automatically (read live from /models), but a schema change — a renamed field, a new required shape — needs a quick diagnosis. The drill:
- Confirm it's upstream, not you. Hit the model directly through the proxy and read the error. A 400 like
reasoning_effort: Extra inputs are not permittedorthinking.type.enabled is not supportedis an upstream schema change, not a proxy bug:Add the field you suspect ($b = @{model="claude-opus-4-8"; max_tokens=16; messages=@(@{role="user"; content="hi"})} | ConvertTo-Json -Depth 6 curl.exe -s -H "content-type: application/json" -d $b http://127.0.0.1:4141/claude/v1/messages
reasoning_effort,output_config,thinking) and compare. The upstream message usually tells you the new shape ("Use thinking.type.adaptive and output_config.effort"). - Check advertised capabilities for the model (effort values, context, endpoints):
Note that
$tok = (Select-String -Path .env -Pattern "COPILOT_GITHUB_TOKEN=(.+)").Matches[0].Groups[1].Value.Trim() curl.exe -s -H "Authorization: Bearer $tok" -H "Copilot-Integration-Id: copilot-developer-cli" ` https://api.githubcopilot.com/models | ConvertFrom-Json | % data | ? vendor -eq Anthropic | % { "{0,-32} effort=[{1}]" -f $_.id, ($_.capabilities.supports.reasoning_effort -join ",") }
/modelscan advertise a capability the/v1/messagesendpoint no longer accepts in the old place — trust the live/v1/messageserror over the catalog. - Fix in the right spot (almost always one of these, not the handler):
- Reasoning-effort values per model → nothing to do; the live cache adapts.
- A new model id that needs a
-1mremap →_MODEL_1M_REWRITES/_DASH_1M_VERSION_REinclaude_proxy.py. - A renamed/moved body field (like
reasoning_effort→output_config.effort, orthinking.typeenabled → adaptive) → the body-massaging helpers_apply_effort_rewrite/_apply_thinking_rewriteinclaude_proxy.py. - A beta token Copilot rejects →
UNSUPPORTED_BETA_TOKENSinheaders.py.
- Add a unit test asserting the new rewrite (see
tests/unit/test_claude_body_rewrite.py), then re-run the suite and live-verify the model returns 200. Codex is fully dynamic (it filters/modelson/responsessupport) and rarely needs model-specific changes.
Everything happens in Docker. Python is not required on the host — there's no host-side venv, no host-side uv, no host-side pip install. The Docker image is the only environment the project runs in, including for tests and for managing dependencies. This keeps dev, test, and prod identical and means git clone + Docker Desktop is the entire prerequisite list.
# Run the proxy (Docker)
docker compose up -d --build proxy
# Tail logs
docker compose logs -f proxy
# Restart to pick up source edits (src/ and tests/ are bind-mounted read-only)
docker compose restart proxy
# Stop
docker compose down
# Run unit tests
docker compose run --rm proxy uv run pytest tests/unit -q
# Lint
docker compose run --rm proxy uv run ruff check src tests
# Run a single test file or test
docker compose run --rm proxy uv run pytest tests/unit/test_claude_proxy.py -q
docker compose run --rm proxy uv run pytest tests/unit/test_claude_proxy.py::test_models_filters_and_canonicalizes -q
# Coverage report
docker compose run --rm proxy sh -lc "uv run coverage run --source=copilot_cli_relay -m pytest tests/unit -q && uv run coverage report --show-missing"
# Open a shell inside the running container
docker compose exec proxy bash
# Codex route smoke checks
curl http://127.0.0.1:4141/codex/healthz
curl http://127.0.0.1:4141/codex/v1/modelsThe lockfile (uv.lock) is committed and the Dockerfile uses uv sync --frozen, so any change to pyproject.toml requires a matching uv lock regeneration before the next build will succeed. Both steps run in throwaway containers — nothing leaks onto the host.
# 1. Edit pyproject.toml (add/remove/bump a package).
# 2. Regenerate the lockfile in a throwaway container that mounts the repo.
docker run --rm -v "${PWD}:/work" -w /work --user root copilot-cli-relay:dev uv lock
# 3. Rebuild the proxy image with the new lockfile.
docker compose build proxy
# 4. Run the tests to confirm nothing broke.
docker compose run --rm proxy uv run pytest tests/unit -qIf you don't yet have the copilot-cli-relay:dev image locally, replace step 2 with the standalone uv image: docker run --rm -v "${PWD}:/work" -w /work ghcr.io/astral-sh/uv:python3.14-bookworm-slim uv lock.
- The OAuth token is read fresh from
.envat container start; the proxy never persists it. extract-token.ps1writes.envwith a hardened ACL: inheritance is disabled and dropped, then explicit ACEs are added for the current user andNT AUTHORITY\SYSTEM. In practice the resulting on-disk DACL also includesBUILTIN\Administratorswith FullControl when the script is run by an account in that group; the script does not strip this. It refuses to write the token if the ACL hardening fails. On a single-user dev box this is "owner-only" in practice; on a host with multiple local Administrators, anyone in that group can read the file.- The proxy binds only to
127.0.0.1:4141on the host. Not reachable from the network. - The app also rejects non-loopback
Hostheaders, browser cross-siteOrigin/Sec-Fetch-Siterequests, and non-JSON POSTs to proxy endpoints. This blocks DNS-rebinding and simple-form browser requests before the relay injects the Copilot token. - The container runs as a non-root user (UID 1000).
- Inbound
Authorization,x-api-key,proxy-authorization,Cookie, andUser-Agentheaders from clients are stripped before any upstream forward (regression-tested). UpstreamSet-Cookieis stripped from responses returned to the client. - Codex-specific local headers such as
x-codex-turn-metadata,session_id, andthread_idare not forwarded to Copilot; the proxy rebuilds the Responses headers from its own settings. - Body logging is off by default. When enabled, secrets are redacted: GitHub tokens (classic
gh[ousrp]_and fine-grainedgithub_pat_), JWTs,sk-/sk-ant-/sk-proj-keys, AWS access key ids, AWS secret access keys when prefixed by their conventional key name, Slack tokens (xox[baprs]-), Stripe live/restricted keys (sk_live_/rk_live_), Google API keys (AIza…), genericBearer …tokens, the values ofAuthorization/x-api-key/api-key/proxy-authorizationheader lines (both rawHeader: valueform and quoted dict-/JSON-repr form), and JSON keys namedpassword/api_key/access_token/auth_token/secret/client_secret/refresh_token/private_key. Exception messages on every error path also go through the same redaction before being logged or returned to the client. Bodies still contain the user's source code — redaction is best-effort, not a substitute for treating logs as sensitive. .envand.env.*are gitignored and dockerignored (with!.env.exampleexception). Verify withgit check-ignore -v .envbefore your first commit.
MIT.