betahi-copilot-bridge 0.20.1 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,34 +1,15 @@
1
1
  <h1 align="center">copilot-bridge</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.20.0" alt="npm version"></a>
4
+ <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.20.3" alt="npm version"></a>
5
5
  <a href="https://github.com/betahi/copilot-bridge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/betahi-copilot-bridge.svg" alt="license"></a>
6
6
  </p>
7
7
 
8
8
  > Use GitHub Copilot as a local OpenAI/Anthropic-compatible API, so [Codex CLI](https://developers.openai.com/codex/cli), [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) and Continue can talk to Copilot with minimal configuration.
9
9
 
10
- > [!WARNING]
11
- > This is a reverse-engineered bridge for the GitHub Copilot API. It is not
12
- > supported by GitHub, and may break unexpectedly. Use at your own risk.
13
-
14
- > [!WARNING]
15
- > **GitHub Security Notice:**
16
- > Excessive automated or scripted use of Copilot (including rapid or bulk
17
- > requests, such as via automated tools) may trigger GitHub's abuse-detection
18
- > systems. You may receive a warning from GitHub Security, and further
19
- > anomalous activity could result in temporary suspension of your Copilot
20
- > access.
21
- >
22
- > GitHub prohibits use of their servers for excessive automated bulk activity
23
- > or any activity that places undue burden on their infrastructure.
24
- >
25
- > Please review:
26
- >
27
- > - [GitHub Acceptable Use Policies](https://docs.github.com/site-policy/acceptable-use-policies/github-acceptable-use-policies#4-spam-and-inauthentic-activity-on-github)
28
- > - [GitHub Copilot Terms](https://docs.github.com/site-policy/github-terms/github-terms-for-additional-products-and-features#github-copilot)
29
- >
30
- > Use this bridge responsibly to avoid account restrictions. The
31
- > `--rate-limit <seconds>` flag is provided to help throttle upstream traffic.
10
+ > [!CAUTION]
11
+ > This is an unofficial bridge for the GitHub Copilot API and may break if the
12
+ > upstream API changes.
32
13
 
33
14
  ## Demo
34
15
 
@@ -50,10 +31,6 @@ npx betahi-copilot-bridge@latest auth
50
31
  npx betahi-copilot-bridge@latest start
51
32
  ```
52
33
 
53
- `start` flags: `--host`, `--port`, `--show-token`, `--debug`,
54
- `--no-codex-setup`, `--select-model`, `--no-prompt`,
55
- `--rate-limit <seconds>`, `--wait`.
56
-
57
34
  After startup the banner prints a **Usage Viewer** link of the form
58
35
  `https://betahi.github.io/copilot-bridge?endpoint=http://127.0.0.1:4142/usage`,
59
36
  which renders the Copilot quota snapshot (chat / completions / premium
@@ -65,22 +42,18 @@ The bridge exposes both adapter-style endpoints (`/v1/responses`,
65
42
  LiteLLM, Continue, Cline and Aider work out of the box. CORS is enabled
66
43
  globally for browser-based clients.
67
44
 
68
- `--rate-limit N` enforces a minimum of N seconds between upstream
69
- requests (anti–abuse-detection throttle). Add `--wait` to block instead
70
- of returning HTTP 429 when the window has not elapsed.
71
-
72
- `--debug` enables extra upstream error diagnostics in console logs.
73
- - Includes: token limits, stream mode, tool count, invalid tool names, and suspicious tool-schema paths.
74
- - Does not include: request messages, prompt text, bearer tokens, tool descriptions, or the full request body.
75
-
76
- **Review or redact debug logs before sharing them publicly.**
77
-
78
45
  ## Configure Codex CLI
79
46
 
80
47
  `start` writes a managed block into **`~/.codex/config.toml`**. You don't edit
81
- this block; the bridge regenerates it on every start.
48
+ between the markers; the bridge regenerates that block on every start. To pin
49
+ the **default model** for `codex` (without passing `-m` every time), add your
50
+ top-level keys above the managed block:
82
51
 
83
52
  ```toml
53
+ # User defaults; edit these freely.
54
+ model = "gpt-5.3-codex"
55
+ model_reasoning_effort = "high"
56
+
84
57
  # >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>
85
58
  model_provider = "bridge"
86
59
  model_supports_reasoning_summaries = true
@@ -94,37 +67,21 @@ requires_openai_auth = false
94
67
  # <<< copilot-bridge managed block — edits outside this block are preserved <<<
95
68
  ```
96
69
 
97
- To pin the **default model** for `codex` (without passing `-m` every time),
98
- edit the **top of the same file** — outside the markers. The bridge preserves
99
- these keys across rewrites:
100
-
101
- ```toml
102
- model = "gpt-5.3-codex"
103
- model_reasoning_effort = "high"
104
- ```
105
-
106
- That's it — `codex exec '...'` will now route through the bridge to Copilot.
107
-
108
- Use `--no-codex-setup` to skip the managed-block writer entirely (e.g. if you
109
- manage `~/.codex/config.toml` yourself).
70
+ Use `--no-codex-setup` to skip this writer if you manage
71
+ `~/.codex/config.toml` yourself.
110
72
 
111
73
  ### Codex warning: "Model metadata ... not found"
112
74
 
113
- This is a Codex client-side metadata warning, not a bridge routing failure.
114
- Requests can still complete through the bridge.
75
+ This is a Codex client-side metadata warning, not a bridge routing failure, requests can still complete through the bridge.
115
76
 
116
- For `claude-opus-4.6-1m`, upstream still enforces a 1,000,000-token prompt
77
+ For 1M models, upstream still enforces a 1,000,000-token prompt
117
78
  limit (about 900k succeeds; around 1,000,046 is rejected as too long).
118
79
 
119
80
  ## Configure Claude Code
120
81
 
121
- Bridge endpoints: `POST /v1/messages`, `POST /v1/messages/count_tokens`.
122
-
123
- `copilot-bridge start` automatically writes `ANTHROPIC_BASE_URL` (and a dummy
124
- `ANTHROPIC_AUTH_TOKEN` if absent) into **`~/.claude/settings.json`**, so
125
- `claude` works the moment the bridge is listening — even on a different host
126
- or port. Pass `--no-claude-setup` to skip this. All other keys in your
127
- settings file (model overrides, plugins, marketplaces) are preserved.
82
+ `copilot-bridge start` writes `ANTHROPIC_BASE_URL` and a dummy
83
+ `ANTHROPIC_AUTH_TOKEN` into **`~/.claude/settings.json`**. Other settings are
84
+ preserved; use `--no-claude-setup` to skip this writer.
128
85
 
129
86
  Minimal recommended config is:
130
87
 
@@ -146,12 +103,12 @@ the bridge maps it to the real Copilot `-1m` model upstream:
146
103
  ```json
147
104
  {
148
105
  "env": {
149
- "ANTHROPIC_MODEL": "claude-opus-4.7-[1m]",
106
+ "ANTHROPIC_MODEL": "claude-opus-4.7-[1m]"
150
107
  }
151
108
  }
152
109
  ```
153
110
 
154
- Optional slot-specific:
111
+ Slot-specific overrides:
155
112
 
156
113
  ```json
157
114
  {
@@ -171,6 +128,44 @@ project-local `.claude/settings.json` and `.claude/settings.local.json` and
171
128
  applied to Claude requests only when the model supports reasoning. If it is not
172
129
  configured, Claude requests do not infer or attach a reasoning effort.
173
130
 
131
+ ## Start flags
132
+
133
+ Common:
134
+
135
+ | Flag | Purpose |
136
+ | ---- | ------- |
137
+ | `--host <host>` | Bind address. Defaults to `127.0.0.1`. |
138
+ | `--port <port>` | Listen port. Overrides `$PORT` and the port inferred from Claude settings. |
139
+ | `--model <model>` | Override the request model for this bridge process only; does not edit config files. |
140
+ | `--rate-limit <seconds>` | Enforce a minimum delay between upstream requests. |
141
+ | `--wait` | With `--rate-limit`, wait instead of returning HTTP 429. |
142
+
143
+ Codex:
144
+
145
+ | Flag | Purpose |
146
+ | ---- | ------- |
147
+ | `--no-codex-setup` | Skip writing the managed block into `~/.codex/config.toml`. |
148
+ | `--no-prompt` | Never prompt for a Codex default model. |
149
+
150
+ Claude:
151
+
152
+ | Flag | Purpose |
153
+ | ---- | ------- |
154
+ | `--no-claude-setup` | Skip writing `ANTHROPIC_BASE_URL` into `~/.claude/settings.json`. |
155
+
156
+ Diagnostics:
157
+
158
+ | Flag | Purpose |
159
+ | ---- | ------- |
160
+ | `--debug` | Print extra upstream error diagnostics. |
161
+ | `--show-token` | Print GitHub and Copilot tokens during startup. Sensitive; use only for local debugging. |
162
+
163
+ `--debug` enables extra upstream error diagnostics in console logs.
164
+ - Includes: token limits, stream mode, tool count, invalid tool names, and suspicious tool-schema paths.
165
+ - Does not include: request messages, prompt text, bearer tokens, tool descriptions, or the full request body.
166
+
167
+ **Review or redact debug logs before sharing them publicly.**
168
+
174
169
  ## Environment overrides
175
170
 
176
171
  | Variable | Purpose |
@@ -181,8 +176,6 @@ configured, Claude requests do not infer or attach a reasoning effort.
181
176
  | `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
182
177
  | `MODEL_REASONING_EFFORT` | Claude-side reasoning effort override. |
183
178
 
184
-
185
-
186
179
  ## Supported models
187
180
 
188
181
  The bridge resolves aliases and clamps reasoning effort to what each model
@@ -265,6 +258,11 @@ Adding another CLI: drop a new translator under `src/bridges/<client>/`,
265
258
  reuse `src/services/copilot/` for upstream calls, register routes in
266
259
  `src/server.ts`, and add tests under `tests/`.
267
260
 
261
+ ## Acknowledgements
262
+
263
+ Claude Code bridge notes inspired by
264
+ [ericc-ch/copilot-api](https://github.com/ericc-ch/copilot-api). Respect.
265
+
268
266
  ## License
269
267
 
270
268
  MIT — see [LICENSE](./LICENSE).
package/dist/main.js CHANGED
@@ -417,22 +417,6 @@ async function applyClaudeConfig(input) {
417
417
  };
418
418
  }
419
419
 
420
- //#endregion
421
- //#region src/lib/claude-launch.ts
422
- const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
423
- const pickClaudeLaunchDefaults = (availableModels, preferredModel) => {
424
- return { model: preferredModel && availableModels.includes(preferredModel) ? preferredModel : availableModels.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : availableModels[0] ?? "gpt-5.3-codex" };
425
- };
426
- const buildClaudeLaunchCommand = (input) => {
427
- const env = {
428
- ANTHROPIC_BASE_URL: input.baseUrl,
429
- ANTHROPIC_AUTH_TOKEN: "dummy",
430
- DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
431
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
432
- };
433
- return `${Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")} && claude --model ${shellQuote(input.model)}`;
434
- };
435
-
436
420
  //#endregion
437
421
  //#region src/lib/codex-config.ts
438
422
  const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>";
@@ -1353,7 +1337,11 @@ chatCompletionRoutes.post("/", async (c) => {
1353
1337
  try {
1354
1338
  await checkRateLimit();
1355
1339
  const payload = await c.req.json();
1356
- const response = await createChatCompletions(c.var.config, payload, { client: "generic" });
1340
+ const effectivePayload = runtimeState.modelOverride ? {
1341
+ ...payload,
1342
+ model: runtimeState.modelOverride
1343
+ } : payload;
1344
+ const response = await createChatCompletions(c.var.config, effectivePayload, { client: "generic" });
1357
1345
  if (isNonStreaming(response)) return c.json(response);
1358
1346
  return streamSSE(c, async (stream) => {
1359
1347
  for await (const chunk of response) await stream.writeSSE(chunk);
@@ -2197,9 +2185,13 @@ messageRoutes.post("/", async (c) => {
2197
2185
  }
2198
2186
  const anthropicPayload = await c.req.json();
2199
2187
  const claudeSettings = await getClaudeSettings();
2200
- const upstreamModel = translateModelName(anthropicPayload.model, claudeSettings);
2188
+ const effectivePayload = runtimeState.modelOverride ? {
2189
+ ...anthropicPayload,
2190
+ model: runtimeState.modelOverride
2191
+ } : anthropicPayload;
2192
+ const upstreamModel = translateModelName(effectivePayload.model, claudeSettings);
2201
2193
  const toolNameMapper = createAnthropicToolNameMapper(anthropicPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
2202
- const openAIPayload = translateToOpenAI(anthropicPayload, claudeSettings, toolNameMapper);
2194
+ const openAIPayload = translateToOpenAI(effectivePayload, claudeSettings, toolNameMapper);
2203
2195
  try {
2204
2196
  const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
2205
2197
  if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response, toolNameMapper));
@@ -2254,7 +2246,11 @@ messageRoutes.post("/count_tokens", async (c) => {
2254
2246
  try {
2255
2247
  const anthropicBeta = c.req.header("anthropic-beta");
2256
2248
  const anthropicPayload = await c.req.json();
2257
- const openAIPayload = translateToOpenAI(anthropicPayload, await getClaudeSettings());
2249
+ const claudeSettings = await getClaudeSettings();
2250
+ const openAIPayload = translateToOpenAI(runtimeState.modelOverride ? {
2251
+ ...anthropicPayload,
2252
+ model: runtimeState.modelOverride
2253
+ } : anthropicPayload, claudeSettings);
2258
2254
  const selectedModel = resolveModel(openAIPayload.model);
2259
2255
  if (!selectedModel) {
2260
2256
  consola.warn(`Model ${openAIPayload.model} not found in registry, returning fallback token count`);
@@ -2718,7 +2714,11 @@ responsesRoutes.post("/", async (c) => {
2718
2714
  if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
2719
2715
  throw error;
2720
2716
  }
2721
- const payload = normalizeCodexResponsesRequest(await c.req.json(), await readConfiguredCodexReasoningEffort());
2717
+ const rawPayload = await c.req.json();
2718
+ const payload = normalizeCodexResponsesRequest(runtimeState.modelOverride ? {
2719
+ ...rawPayload,
2720
+ model: runtimeState.modelOverride
2721
+ } : rawPayload, await readConfiguredCodexReasoningEffort());
2722
2722
  const provider = getCopilotProviderContext(c.get("config"));
2723
2723
  const search = new URL(c.req.url).search;
2724
2724
  const capability = getModelCapability(payload.model);
@@ -2920,15 +2920,9 @@ const start = defineCommand({
2920
2920
  default: true,
2921
2921
  description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
2922
2922
  },
2923
- "claude-code": {
2924
- type: "boolean",
2925
- default: false,
2926
- description: "Print a one-shot `export ... && claude` command and do not modify user config files."
2927
- },
2928
- "select-model": {
2929
- type: "boolean",
2930
- default: false,
2931
- description: "Force the model picker even when ~/.codex/config.toml already has a model."
2923
+ model: {
2924
+ type: "string",
2925
+ description: "Override the request model for this bridge process only."
2932
2926
  },
2933
2927
  prompt: {
2934
2928
  type: "boolean",
@@ -2973,9 +2967,12 @@ const start = defineCommand({
2973
2967
  consola.info(`copilot base url: ${config.copilotBaseUrl}`);
2974
2968
  const baseUrl = `http://${config.host}:${config.port}`;
2975
2969
  const codexConfigPath = CODEX_DEFAULTS.configPath;
2976
- const isClaudeCodeMode = Boolean(args["claude-code"]);
2977
- if (isClaudeCodeMode) consola.info("--claude-code mode: not modifying ~/.claude/settings.json or ~/.codex/config.toml");
2978
- if (!isClaudeCodeMode && args["claude-setup"]) try {
2970
+ const requestedModel = args.model !== void 0 ? String(args.model) : void 0;
2971
+ if (requestedModel) {
2972
+ runtimeState.modelOverride = requestedModel;
2973
+ consola.info(`Runtime model override: ${requestedModel}`);
2974
+ } else delete runtimeState.modelOverride;
2975
+ if (args["claude-setup"]) try {
2979
2976
  const claudeResult = await applyClaudeConfig({
2980
2977
  baseUrl,
2981
2978
  configPath: claudeConfigPath
@@ -2996,7 +2993,7 @@ const start = defineCommand({
2996
2993
  else if (capability?.reasoning) consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
2997
2994
  else consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
2998
2995
  }
2999
- if (!isClaudeCodeMode && args["codex-setup"]) try {
2996
+ if (args["codex-setup"]) try {
3000
2997
  const result = await applyCodexConfig({
3001
2998
  baseUrl: `${baseUrl}/v1`,
3002
2999
  configPath: codexConfigPath,
@@ -3026,7 +3023,7 @@ const start = defineCommand({
3026
3023
  ``,
3027
3024
  ` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
3028
3025
  ].join("\n"));
3029
- if (!isClaudeCodeMode && args.prompt && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
3026
+ if (args.prompt && finalPickable.length > 0 && !chosenModel && !requestedModel) {
3030
3027
  const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
3031
3028
  const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
3032
3029
  type: "select",
@@ -3051,26 +3048,6 @@ const start = defineCommand({
3051
3048
  }
3052
3049
  }
3053
3050
  }
3054
- if (isClaudeCodeMode) {
3055
- const defaults = pickClaudeLaunchDefaults(finalPickable, chosenModel);
3056
- let selectedModel = defaults.model;
3057
- if (args.prompt && finalPickable.length > 0) selectedModel = await consola.prompt("Select a model to use with Claude Code", {
3058
- type: "select",
3059
- options: finalPickable,
3060
- initial: defaults.model
3061
- });
3062
- const command = buildClaudeLaunchCommand({
3063
- baseUrl,
3064
- model: selectedModel
3065
- });
3066
- consola.box([
3067
- "Claude one-shot command",
3068
- "",
3069
- ` ${command}`,
3070
- "",
3071
- " The bridge verified this path with explicit --model; current Claude CLI did not reliably honor ANTHROPIC_MODEL env defaults."
3072
- ].join("\n"));
3073
- }
3074
3051
  await new Promise((resolve, reject) => {
3075
3052
  server.on("close", resolve);
3076
3053
  server.on("error", reject);