@dianshuv/copilot-api
v0.20.1
Published
Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!
Readme
Copilot API Proxy (Fork)
[!NOTE] This is a fork of @hsupu/copilot-api, which itself is a fork of
ericc-ch/copilot-api, with additional improvements and bug fixes.
[!WARNING] This is a reverse-engineered proxy of GitHub Copilot API. It is not supported by GitHub, and may break unexpectedly. Use at your own risk.
New Features (over @hsupu/copilot-api)
- Responses API endpoint:
/v1/responsespassthrough for codex models (e.g.,gpt-5.2-codex,gpt-5.3-codex) used by tools like OpenCode. Includes stream ID synchronization for@ai-sdk/openaicompatibility. - SubagentStart marker support: Detects
__SUBAGENT_MARKER__injected by Claude Code hooks to overrideX-Initiatorheader to"agent"for subagent requests, ensuring correct credit tier usage. Includes a ready-to-use Claude plugin (claude-plugin/). - Token analytics tab: The
/historypage includes a Tokens tab with per-model token usage summary table and cumulative ECharts line chart for visualizing API consumption over time. - Real-time history updates: The
/historyUI uses WebSocket for live updates instead of polling, with automatic fallback to polling and exponential backoff reconnection. - Graceful shutdown: 4-phase shutdown sequence — stops accepting requests, waits for in-flight requests to complete, sends abort signal, then force-closes. Configurable via
--shutdown-graceful-waitand--shutdown-abort-wait. - Stream repetition detection: Detects when models get stuck in repetitive output loops using KMP-based pattern matching and logs a warning.
- Stale request reaping: Automatically force-fails requests that exceed a configurable maximum age (default 600s) to prevent resource leaks.
- PostHog analytics: Optional PostHog Cloud integration (
--posthog-key) sends per-request token usage events for long-term trend analysis. Free tier (1M events/month) is more than sufficient for individual use. - GitHub Copilot CLI emulation: All upstream requests to GitHub — device-flow
login,/copilot_internal/userbootstrap, and CAPI model/chat calls — carry the official GitHub Copilot CLI's (@github/copilot) identity: itscopilot-integration-id(copilot-developer-cli),editor-version/user-agent(copilot/<version>),x-github-api-version, and a persistentx-client-machine-id(stored at~/.local/share/copilot-api/machine_id). The CAPI Bearer is the GitHub OAuth token itself — the CLI does not perform a separate token exchange. Theloginflow uses the CLI's own OAuth app, so new logins request theread:user,read:org,repo, andgistscopes; existing tokens keep working without re-authentication.
Quick Start
Install from npm (Recommended)
# Run directly with npx
npx @dianshuv/copilot-api start
# Or install globally
npm install -g @dianshuv/copilot-api
copilot-api startDevelopment
# Start the server (foreground, production mode)
make up
# Stop the server (graceful shutdown)
make downCommand Reference
| Command | Description |
|---------|-------------|
| start | Start the API server (handles auth if needed) |
| login | Run GitHub authentication flow only |
| logout | Remove the local GitHub token file (does not revoke authorization on GitHub) |
| debug | Display diagnostic information |
Start Command Options
| Option | Description | Default |
|--------|-------------|---------|
| --port, -p | Port to listen on | 4141 |
| --host, -H | Host/interface to bind to (0.0.0.0 = all interfaces) | 127.0.0.1 |
| --verbose, -v | Enable verbose logging | false |
| --account-type, -a | Account type (individual, business, enterprise) | individual |
| --no-rate-limit | Disable adaptive rate limiting | false |
| --github-token, -g | Provide GitHub token directly (or GH_TOKEN env) | none |
| --posthog-key | PostHog API key for token usage analytics (opt-in) | none |
| --api-key | Proxy API key for inbound authentication (see Authentication). Empty = disabled | none |
| --allow-token-endpoint | Opt-in to expose GET /token (echoes the raw GitHub OAuth token; requires --api-key too). COPILOT_ALLOW_TOKEN_ENDPOINT=1 env twin. | false |
Hidden Models
By default the proxy hides 30 stale / duplicate / unused upstream model ids from
its listing surfaces (/v1/models and the startup banner).
Important: this is a display filter only. The remaining POST endpoints
(/v1/chat/completions, /v1/messages, /v1/responses) do not reject
hidden ids — an explicit POST with a hidden id is still forwarded to
upstream verbatim. (The embedding and Gemini ids in the list have no callable
endpoint in this proxy at all; they're hidden purely to denoise the
/v1/models listing — the upstream /models catalogue still returns them.)
To see the full catalogue or change the blacklist, edit
src/lib/hidden-models.ts and re-publish (this fork uses /publish).
Currently hidden (grouped):
- Legacy GPT —
gpt-3.5-turbo,gpt-3.5-turbo-0613,gpt-4,gpt-4-0613,gpt-4-0125-preview - GPT-4o family —
gpt-4o,gpt-4o-mini,gpt-4-o-preview,gpt-4o-2024-05-13,gpt-4o-2024-08-06,gpt-4o-2024-11-20,gpt-4o-mini-2024-07-18 - GPT-4.1 family —
gpt-4.1,gpt-4.1-2025-04-14,gpt-41-copilot - Older / smaller GPT-5 —
gpt-5-mini,gpt-5.3-codex - All embeddings —
text-embedding-ada-002,text-embedding-3-small,text-embedding-3-small-inference - Older Gemini —
gemini-2.5-pro,gemini-3-flash-preview - Older / variant Claude —
claude-opus-4.5,claude-opus-4.6,claude-opus-4.7-high,claude-opus-4.7-xhigh,claude-sonnet-4.5 - Special-purpose —
mai-code-1-flash-internal,trajectory-compaction
API Endpoints
OpenAI Compatible
| Endpoint | Method | Description |
|----------|--------|-------------|
| /v1/chat/completions | POST | Chat completions |
| /v1/models | GET | List available models |
| /v1/responses | POST | Responses API (for codex models) |
Anthropic Compatible
| Endpoint | Method | Description |
|----------|--------|-------------|
| /v1/messages | POST | Messages API |
| /v1/messages/count_tokens | POST | Token counting |
| /v1/event_logging/batch | POST | Event logging (no-op) |
Utility
| Endpoint | Method | Description |
|----------|--------|-------------|
| / | GET | Server status |
| /usage | GET | Copilot usage stats |
| /token | GET | Current CAPI Bearer (= raw GitHub OAuth token). Disabled by default; requires --allow-token-endpoint and --api-key, otherwise 403. |
| /health | GET | Health check |
| /history | GET | Request history Web UI with token analytics (enabled by default) |
| /history/api/* | GET/DELETE | History API endpoints |
Auto-Truncate
When enabled (default), auto-truncate automatically compacts conversation history when it exceeds the model's token limit. This prevents request failures due to context overflow.
- Token-based truncation: Uses the model's
max_context_window_tokensfrom the Copilot API to determine when truncation is needed. A 2% safety margin is applied. - No preset byte limit: There is no hardcoded request body size limit. If the Copilot API returns a 413 (Request Entity Too Large), the proxy dynamically learns the byte limit and applies it to subsequent requests.
- Orphan filtering: After truncation, orphaned tool results (without matching tool calls) are automatically removed.
Authentication
By default the proxy is unauthenticated: every endpoint is open and any
request passes through unchanged. You can optionally protect the proxy's
inbound surface with a Proxy API key (this is your own secret for this
proxy — it is unrelated to the GitHub OAuth / Copilot token the proxy uses
upstream). When a key is configured, all endpoints except / and /health
require it (fail-closed: any route not on that exemption list — including future
ones — is protected). Genuine CORS preflight OPTIONS requests are also let
through (they carry no payload), so browser clients aren't tripped by an opaque
preflight failure.
Enabling it
Provide the key via either source:
--api-key <key>flagCOPILOT_API_KEYenvironment variable
Rules:
- Precedence: when both are set to a non-empty value, the flag wins and the env value is ignored.
- Trim + empty disables: each source is trimmed first; a value that is empty
or whitespace-only (
--api-key "",COPILOT_API_KEY=) counts as not provided. If neither source yields a non-empty value, auth stays disabled (the default).
The startup banner prints whether auth is on/off and, when on, the source
(flag / env) — but never the key value itself.
[!CAUTION] Ambient
COPILOT_API_KEYfootgun. Because the env var is read on every start, aCOPILOT_API_KEYexported anywhere the process can see it (your shell profile, a.envsourced into the environment, a systemd unit, a container env) silently enables authentication for that instance — even when you did not pass--api-key. If a previously-open instance suddenly returns401, check for an inheritedCOPILOT_API_KEY. The startup banner'ssource: envline makes this visible.
Network binding
The proxy binds loopback (127.0.0.1) by default, so an unconfigured
instance is reachable only from the local machine. To expose it on your network
— behind auth, a tunnel, or inside a container — opt into an all-interfaces
bind explicitly:
copilot-api start --host 0.0.0.0The bind host resolves with --host flag > HOST env > 127.0.0.1. Note
that a HOST env var — inherited from your shell or a container — also widens
the bind: HOST=0.0.0.0 opens all interfaces just like the flag. A blank
HOST (HOST=, or HOST=$UNSET) is ignored and falls back to the loopback
default, so an accidentally-empty env var can't silently expose the proxy. The
startup banner always reports the real bind address (127.0.0.1 for the
loopback default, 0.0.0.0 for an all-interfaces bind, or the narrowed host you
passed), so a wide-open bind is never hidden.
Choosing a key
Use a high-entropy secret (e.g. openssl rand -hex 32), not a guessable
word. The comparison is constant-time, but a weak key can still be guessed
offline or brute-forced over the network.
Migrating clients
Once a key is configured, clients that previously sent a placeholder must send the real key. Two header shapes are accepted and either is sufficient:
- OpenAI-compatible clients →
Authorization: Bearer <key>(the scheme is matched case-insensitively; a bare value with noBearerprefix is also tolerated). - Anthropic-native clients →
x-api-key: <key>.
If both headers are present, a match on either one is accepted.
Per client:
- opencode (
opencode.json): setprovider.<name>.options.apiKeyfrom"dummy"to your real key (the@ai-sdk/openai-compatibleprovider sends it asAuthorization: Bearer). - Claude Code (
.claude/settings.json, see below): changeANTHROPIC_AUTH_TOKENfrom"dummy"to your real key. Claude Code talks to the proxy's/v1/messages(Anthropic) surface; the auth gate accepts the token from either header. - Raw OpenAI SDK →
Authorization: Bearer <key>; raw Anthropic SDK →x-api-key: <key>.
[!TIP] Keep the key out of shell history. Don't paste the literal key onto a command line (e.g.
curl -H "Authorization: Bearer sk-real-key" …), or it lands in~/.zsh_history. Read it from an env var or a file instead:# Put the key in an env var read from a non-echoed prompt or a 0600 file, # then reference the var — the literal secret never appears in argv/history. read -rs COPILOT_API_KEY # typed key is not echoed or saved export COPILOT_API_KEY curl -H "Authorization: Bearer $COPILOT_API_KEY" http://127.0.0.1:4141/v1/models
A failed auth returns 401 with a client-appropriate body: requests to the
Anthropic surface (/v1/messages, /v1/messages/count_tokens) get
{"type":"error","error":{"type":"authentication_error", …}}; every other
endpoint gets the OpenAI-style {"error":{"code":"invalid_api_key", …}}.
Missing and wrong keys return the identical body (no oracle), always with a
WWW-Authenticate: Bearer header.
Browser tools when auth is on
The request logger does not record the Authorization / x-api-key
headers, so your key never lands in logs. But the browser-based tools can't send
a custom header, so when auth is on they stop working:
- History UI (
/history) and its live-update WebSocket (/history/ws) — the page load and socket upgrade are gated like any other protected route. - External Usage Viewer (the
ericc-ch.github.iolink in the banner) — it fetches/usage, which is now protected, so it can't load your data.
Workarounds:
- Run a separate local, unauthenticated instance (no
--api-key; the default127.0.0.1bind keeps it local) for the browser UI, or - Hit the history JSON API directly with a key, e.g.
curl -H "Authorization: Bearer $COPILOT_API_KEY" http://127.0.0.1:4141/history/api/entries.
Using with Claude Code
Create .claude/settings.json in your project:
{
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:4141",
"ANTHROPIC_AUTH_TOKEN": "dummy",
"ANTHROPIC_MODEL": "claude-opus-4.8",
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4.5",
"DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"permissions": {
"deny": ["WebSearch"]
}
}