betahi-copilot-bridge 0.20.8 → 0.20.9
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 +19 -5
- package/dist/main.js +95 -23
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## Contents
|
|
15
15
|
- [Demo](#demo)
|
|
16
|
+
- [Why this bridge?](#why-this-bridge)
|
|
16
17
|
- [Install & run](#install--run)
|
|
17
18
|
- [Configure Codex CLI](#configure-codex-cli)
|
|
18
19
|
- [Configure Claude Code](#configure-claude-code)
|
|
@@ -34,6 +35,19 @@
|
|
|
34
35
|
|
|
35
36
|

|
|
36
37
|
|
|
38
|
+
## Why this bridge?
|
|
39
|
+
|
|
40
|
+
copilot-bridge is more than a raw HTTP proxy:
|
|
41
|
+
|
|
42
|
+
1. **Codex and Claude support.** Exposes Codex `/v1/responses`, Claude Code
|
|
43
|
+
`/v1/messages`, and OpenAI-compatible chat, embeddings, and models routes.
|
|
44
|
+
2. **Web search support.** Model-selected web search can be executed by the
|
|
45
|
+
bridge and fed back into a final model pass.
|
|
46
|
+
3. **End-to-end reasoning support.** Normalizes model aliases and
|
|
47
|
+
`reasoning_effort`, routes through the right upstream API, and preserves
|
|
48
|
+
returned `reasoning_text` / `reasoning_content` in streaming and
|
|
49
|
+
non-streaming responses.
|
|
50
|
+
|
|
37
51
|
## Install & run
|
|
38
52
|
|
|
39
53
|
```sh
|
|
@@ -169,7 +183,7 @@ COPILOT_WEB_SEARCH_BACKEND = "gpt-5.5"
|
|
|
169
183
|
|
|
170
184
|
| Value | Search path | Requirement |
|
|
171
185
|
| ----- | ----------- | ----------- |
|
|
172
|
-
| Copilot model id, for example `gpt-5.5` | Copilot HTTP `/responses` + `web_search_preview` |
|
|
186
|
+
| Copilot model id, for example `gpt-5.5` | Copilot HTTP `/responses` + `web_search_preview` | Recommended: `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `gpt-5.2-codex`. |
|
|
173
187
|
| `searxng`, use `"COPILOT_WEB_SEARCH_BACKEND": "searxng"` | Local SearXNG at `http://localhost:8080` | Start SearXNG yourself. Setup guide: https://github.com/betaHi/openclaw-searxng-search. |
|
|
174
188
|
| `copilot-cli` or `copilot`, use `"COPILOT_WEB_SEARCH_BACKEND": "copilot-cli"` | GitHub Copilot CLI `web_search` tool, using the current request model | Install and sign in to GitHub Copilot CLI yourself. |
|
|
175
189
|
|
|
@@ -254,11 +268,11 @@ accepts upstream.
|
|
|
254
268
|
| Model | Reasoning efforts | Notes |
|
|
255
269
|
| -------------------------------- | --------------------------------------- | -------------------------------------- |
|
|
256
270
|
| `claude-opus-4.7` | `medium` | Effort sent as `output_config.effort`. |
|
|
257
|
-
| `claude-opus-4.7-1m` | `low`, `medium`, `high`, `xhigh` | 1M-token context window
|
|
258
|
-
| `claude-opus-4.7-high` | `high` | Fixed high reasoning
|
|
259
|
-
| `claude-opus-4.7-xhigh` | `xhigh` | Fixed extra-high reasoning
|
|
271
|
+
| `claude-opus-4.7-1m` | `low`, `medium`, `high`, `xhigh` | 1M-token context window, prefer use `claude-opus-4.7-[1m]` in config|
|
|
272
|
+
| `claude-opus-4.7-high` | `high` | Fixed high reasoning|
|
|
273
|
+
| `claude-opus-4.7-xhigh` | `xhigh` | Fixed extra-high reasoning|
|
|
260
274
|
| `claude-opus-4.6` | `low`, `medium`, `high` | |
|
|
261
|
-
| `claude-opus-4.6-1m` | `low`, `medium`, `high` | 1M-token context window.
|
|
275
|
+
| `claude-opus-4.6-1m` | `low`, `medium`, `high` | 1M-token context window, prefer use `claude-opus-4.6-[1m]` in config |
|
|
262
276
|
| `claude-sonnet-4.6` | `low`, `medium`, `high` | |
|
|
263
277
|
| `claude-opus-4.5` | — | Reasoning not accepted upstream. |
|
|
264
278
|
| `claude-sonnet-4.5` | — | Reasoning not accepted upstream. |
|
package/dist/main.js
CHANGED
|
@@ -3,9 +3,11 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import consola from "consola";
|
|
4
4
|
import fs, { appendFile } from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
6
|
+
import path, { dirname, resolve } from "node:path";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
9
11
|
import { serve } from "@hono/node-server";
|
|
10
12
|
import { Hono } from "hono";
|
|
11
13
|
import { cors } from "hono/cors";
|
|
@@ -13,7 +15,6 @@ import { logger } from "hono/logger";
|
|
|
13
15
|
import { streamSSE } from "hono/streaming";
|
|
14
16
|
import { events } from "fetch-event-stream";
|
|
15
17
|
import { execFile } from "node:child_process";
|
|
16
|
-
import { accessSync, constants } from "node:fs";
|
|
17
18
|
|
|
18
19
|
//#region src/lib/error.ts
|
|
19
20
|
var BridgeNotImplementedError = class extends Error {
|
|
@@ -148,8 +149,8 @@ const standardHeaders = () => ({
|
|
|
148
149
|
accept: "application/json",
|
|
149
150
|
"content-type": "application/json"
|
|
150
151
|
});
|
|
151
|
-
const sleep$1 = (ms) => new Promise((resolve) => {
|
|
152
|
-
setTimeout(resolve, ms);
|
|
152
|
+
const sleep$1 = (ms) => new Promise((resolve$1) => {
|
|
153
|
+
setTimeout(resolve$1, ms);
|
|
153
154
|
});
|
|
154
155
|
const readGitHubToken$1 = async () => {
|
|
155
156
|
return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim();
|
|
@@ -208,7 +209,7 @@ const getCopilotToken = async (githubToken, vsCodeVersion) => {
|
|
|
208
209
|
return await response.json();
|
|
209
210
|
};
|
|
210
211
|
const isCopilotTokenError = (error) => error instanceof HTTPError && error.message === "Failed to get Copilot token";
|
|
211
|
-
const ensureGitHubToken = async (
|
|
212
|
+
const ensureGitHubToken = async (options = {}) => {
|
|
212
213
|
await ensurePaths();
|
|
213
214
|
const existingToken = options.force ? "" : await readGitHubToken$1();
|
|
214
215
|
if (existingToken) return existingToken;
|
|
@@ -217,17 +218,23 @@ const ensureGitHubToken = async (config, options = {}) => {
|
|
|
217
218
|
const githubToken = await pollAccessToken(deviceCode);
|
|
218
219
|
await writeGitHubToken(githubToken);
|
|
219
220
|
if (options.showToken) consola.info("GitHub token:", githubToken);
|
|
220
|
-
const user = await getGitHubUser(githubToken, config.vsCodeVersion);
|
|
221
|
-
consola.success(`Logged in as ${user.login}`);
|
|
222
221
|
return githubToken;
|
|
223
222
|
};
|
|
223
|
+
const loadGitHubLogin = async (githubToken, vsCodeVersion) => {
|
|
224
|
+
try {
|
|
225
|
+
return (await getGitHubUser(githubToken, vsCodeVersion)).login;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
consola.warn("Could not load GitHub user:", error);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
224
231
|
const setupBridgeAuth = async (config, options = {}) => {
|
|
225
232
|
if (config.copilotToken) {
|
|
226
233
|
if (options.showToken) consola.info("Using COPILOT_TOKEN from environment");
|
|
227
234
|
await loadModels(config);
|
|
228
|
-
return;
|
|
235
|
+
return { source: "copilot-token" };
|
|
229
236
|
}
|
|
230
|
-
let githubToken = await ensureGitHubToken(
|
|
237
|
+
let githubToken = await ensureGitHubToken(options);
|
|
231
238
|
const applyCopilotToken = async () => {
|
|
232
239
|
const { refresh_in, token } = await getCopilotToken(githubToken, config.vsCodeVersion);
|
|
233
240
|
config.copilotToken = token;
|
|
@@ -240,7 +247,7 @@ const setupBridgeAuth = async (config, options = {}) => {
|
|
|
240
247
|
} catch (error) {
|
|
241
248
|
if (options.force || !isCopilotTokenError(error)) throw error;
|
|
242
249
|
consola.warn("Cached GitHub auth could not get a Copilot token; running device auth again.");
|
|
243
|
-
githubToken = await ensureGitHubToken(
|
|
250
|
+
githubToken = await ensureGitHubToken({
|
|
244
251
|
...options,
|
|
245
252
|
force: true
|
|
246
253
|
});
|
|
@@ -256,6 +263,10 @@ const setupBridgeAuth = async (config, options = {}) => {
|
|
|
256
263
|
}
|
|
257
264
|
}, refreshInterval);
|
|
258
265
|
await loadModels(config);
|
|
266
|
+
return {
|
|
267
|
+
githubLogin: await loadGitHubLogin(githubToken, config.vsCodeVersion),
|
|
268
|
+
source: "github-token"
|
|
269
|
+
};
|
|
259
270
|
};
|
|
260
271
|
const loadModels = async (config) => {
|
|
261
272
|
try {
|
|
@@ -321,10 +332,11 @@ const auth = defineCommand({
|
|
|
321
332
|
port: Number(args.port)
|
|
322
333
|
});
|
|
323
334
|
config.copilotToken = void 0;
|
|
324
|
-
await setupBridgeAuth(config, {
|
|
335
|
+
const authSession = await setupBridgeAuth(config, {
|
|
325
336
|
force: true,
|
|
326
337
|
showToken: args["show-token"]
|
|
327
338
|
});
|
|
339
|
+
if (authSession.githubLogin) consola.info(`Logged in as ${authSession.githubLogin}`);
|
|
328
340
|
consola.success("copilot-bridge auth completed");
|
|
329
341
|
}
|
|
330
342
|
});
|
|
@@ -475,9 +487,11 @@ function tomlEscape(value) {
|
|
|
475
487
|
}
|
|
476
488
|
function buildManagedBlock(input) {
|
|
477
489
|
const { baseUrl, settings } = input;
|
|
490
|
+
const modelContextWindow = normalizeModelContextWindow(input.modelContextWindow);
|
|
478
491
|
const lines = [];
|
|
479
492
|
lines.push(BEGIN_MARK);
|
|
480
493
|
if (settings.setAsDefault) lines.push(`model_provider = "${tomlEscape(settings.providerId)}"`);
|
|
494
|
+
if (modelContextWindow !== void 0) lines.push(`model_context_window = ${modelContextWindow}`);
|
|
481
495
|
lines.push("model_supports_reasoning_summaries = true");
|
|
482
496
|
lines.push("");
|
|
483
497
|
lines.push(`[model_providers.${settings.providerId}]`);
|
|
@@ -489,6 +503,15 @@ function buildManagedBlock(input) {
|
|
|
489
503
|
lines.push(END_MARK);
|
|
490
504
|
return lines.join("\n");
|
|
491
505
|
}
|
|
506
|
+
function extractPreservedContentFromManagedBlock(content) {
|
|
507
|
+
const lines = content.split("\n");
|
|
508
|
+
const preserveStart = lines.findIndex((line) => {
|
|
509
|
+
const trimmed = line.trim();
|
|
510
|
+
return trimmed.startsWith("[") && !trimmed.startsWith("[model_providers.");
|
|
511
|
+
});
|
|
512
|
+
if (preserveStart === -1) return "";
|
|
513
|
+
return lines.slice(preserveStart).join("\n").replace(/^\n+|\n+$/g, "");
|
|
514
|
+
}
|
|
492
515
|
function stripManagedBlock(content) {
|
|
493
516
|
let next = content;
|
|
494
517
|
for (const [begin, end] of [[BEGIN_MARK, END_MARK], [LEGACY_BEGIN_MARK, LEGACY_END_MARK]]) while (true) {
|
|
@@ -496,8 +519,9 @@ function stripManagedBlock(content) {
|
|
|
496
519
|
if (beginIdx === -1) break;
|
|
497
520
|
const endIdx = next.indexOf(end, beginIdx);
|
|
498
521
|
if (endIdx === -1) break;
|
|
522
|
+
const preserved = extractPreservedContentFromManagedBlock(next.slice(beginIdx + begin.length, endIdx));
|
|
499
523
|
const before = next.slice(0, beginIdx).replace(/\n*$/, "");
|
|
500
|
-
const after = next.slice(endIdx + end.length).replace(/^\n+/, "");
|
|
524
|
+
const after = [preserved, next.slice(endIdx + end.length)].filter(Boolean).join("\n").replace(/^\n+/, "");
|
|
501
525
|
if (before.length === 0) next = after;
|
|
502
526
|
else if (after.length === 0) next = `${before}\n`;
|
|
503
527
|
else next = `${before}\n\n${after}`;
|
|
@@ -541,6 +565,19 @@ function removeTopKey(topSection, key) {
|
|
|
541
565
|
const re = /* @__PURE__ */ new RegExp(`^\\s*${key}\\s*=`);
|
|
542
566
|
return lines.filter((line) => !re.test(line)).join("\n");
|
|
543
567
|
}
|
|
568
|
+
function normalizeModelContextWindow(value) {
|
|
569
|
+
if (value === void 0 || !Number.isFinite(value)) return;
|
|
570
|
+
const normalized = Math.trunc(value);
|
|
571
|
+
return normalized > 0 ? normalized : void 0;
|
|
572
|
+
}
|
|
573
|
+
function removeTopLevelContextWindowWhenManaged(content, input) {
|
|
574
|
+
if (normalizeModelContextWindow(input.modelContextWindow) === void 0) return content;
|
|
575
|
+
const { top, rest } = splitTopSection(content);
|
|
576
|
+
const nextTop = removeTopKey(top, "model_context_window");
|
|
577
|
+
if (nextTop === top) return content;
|
|
578
|
+
if (rest.length === 0) return nextTop.endsWith("\n") ? nextTop : `${nextTop}\n`;
|
|
579
|
+
return `${nextTop}${nextTop.endsWith("\n") ? "" : "\n"}${rest}`;
|
|
580
|
+
}
|
|
544
581
|
function sanitizeTopReasoningEffort(topSection) {
|
|
545
582
|
const match = topSection.match(scalarRegex("model_reasoning_effort"));
|
|
546
583
|
if (!match) return topSection;
|
|
@@ -571,9 +608,14 @@ async function applyCodexConfig(input) {
|
|
|
571
608
|
}
|
|
572
609
|
let stripped = stripManagedBlock(existing);
|
|
573
610
|
stripped = applyUserScalars(stripped, input);
|
|
611
|
+
stripped = removeTopLevelContextWindowWhenManaged(stripped, input);
|
|
574
612
|
const block = buildManagedBlock(input);
|
|
575
|
-
const
|
|
576
|
-
const next =
|
|
613
|
+
const { top, rest } = splitTopSection(stripped);
|
|
614
|
+
const next = `${[
|
|
615
|
+
top,
|
|
616
|
+
block,
|
|
617
|
+
rest
|
|
618
|
+
].map((part) => part.replace(/^\n+|\n+$/g, "")).filter(Boolean).join("\n\n")}\n`;
|
|
577
619
|
if (next === existing) return {
|
|
578
620
|
configPath,
|
|
579
621
|
changed: false,
|
|
@@ -858,10 +900,28 @@ const clampReasoningEffort = (modelId, requested) => {
|
|
|
858
900
|
};
|
|
859
901
|
};
|
|
860
902
|
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/lib/version.ts
|
|
905
|
+
const PACKAGE_NAME = "betahi-copilot-bridge";
|
|
906
|
+
const readPackageVersion = () => {
|
|
907
|
+
let currentDir = dirname(fileURLToPath(import.meta.url));
|
|
908
|
+
while (true) {
|
|
909
|
+
try {
|
|
910
|
+
const packageJsonPath = resolve(currentDir, "package.json");
|
|
911
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
912
|
+
if (packageJson.name === PACKAGE_NAME && typeof packageJson.version === "string") return packageJson.version;
|
|
913
|
+
} catch {}
|
|
914
|
+
const parentDir = dirname(currentDir);
|
|
915
|
+
if (parentDir === currentDir) return "unknown";
|
|
916
|
+
currentDir = parentDir;
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
const BRIDGE_VERSION = readPackageVersion();
|
|
920
|
+
|
|
861
921
|
//#endregion
|
|
862
922
|
//#region src/lib/rate-limit.ts
|
|
863
|
-
const sleep = (ms) => new Promise((resolve) => {
|
|
864
|
-
setTimeout(resolve, ms);
|
|
923
|
+
const sleep = (ms) => new Promise((resolve$1) => {
|
|
924
|
+
setTimeout(resolve$1, ms);
|
|
865
925
|
});
|
|
866
926
|
/**
|
|
867
927
|
* Enforces the configured rate limit window between mutating upstream calls.
|
|
@@ -2492,7 +2552,7 @@ const findCopilotCliCommand = () => {
|
|
|
2492
2552
|
const candidates = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean).flatMap((directory) => copilotExecutableNames().map((name) => path.join(directory, name))).filter(canExecute);
|
|
2493
2553
|
return candidates.find((candidate) => !COPILOT_CHAT_WRAPPER_PATH_PATTERN.test(candidate)) ?? candidates[0] ?? "copilot";
|
|
2494
2554
|
};
|
|
2495
|
-
const runCopilotCliCommand = (args, timeout) => new Promise((resolve, reject) => {
|
|
2555
|
+
const runCopilotCliCommand = (args, timeout) => new Promise((resolve$1, reject) => {
|
|
2496
2556
|
execFile(findCopilotCliCommand(), args, {
|
|
2497
2557
|
encoding: "utf8",
|
|
2498
2558
|
timeout
|
|
@@ -2505,7 +2565,7 @@ const runCopilotCliCommand = (args, timeout) => new Promise((resolve, reject) =>
|
|
|
2505
2565
|
});
|
|
2506
2566
|
return;
|
|
2507
2567
|
}
|
|
2508
|
-
resolve({
|
|
2568
|
+
resolve$1({
|
|
2509
2569
|
stdout,
|
|
2510
2570
|
stderr
|
|
2511
2571
|
});
|
|
@@ -4067,6 +4127,12 @@ const fetchAvailableModels = async (config) => {
|
|
|
4067
4127
|
return [];
|
|
4068
4128
|
}
|
|
4069
4129
|
};
|
|
4130
|
+
const getCodexModelContextWindow = (model) => {
|
|
4131
|
+
if (!model) return void 0;
|
|
4132
|
+
const resolvedModel = resolveModelId(model);
|
|
4133
|
+
const contextWindow = (runtimeState.models?.data.find((candidate) => candidate.id === resolvedModel || candidate.id === model || getPublicModelId(candidate.id) === model || getPublicModelId(candidate.id) === resolvedModel))?.capabilities?.limits?.max_context_window_tokens;
|
|
4134
|
+
return typeof contextWindow === "number" && contextWindow > 0 ? Math.trunc(contextWindow) : void 0;
|
|
4135
|
+
};
|
|
4070
4136
|
const start = defineCommand({
|
|
4071
4137
|
meta: {
|
|
4072
4138
|
name: "start",
|
|
@@ -4142,8 +4208,12 @@ const start = defineCommand({
|
|
|
4142
4208
|
runtimeState.rateLimitWait = Boolean(args.wait);
|
|
4143
4209
|
consola.info(`Rate limit: ${parsed}s between requests (${runtimeState.rateLimitWait ? "wait" : "reject"} on overflow)`);
|
|
4144
4210
|
}
|
|
4145
|
-
await setupBridgeAuth(config, { showToken: args["show-token"] });
|
|
4211
|
+
const authSession = await setupBridgeAuth(config, { showToken: args["show-token"] });
|
|
4146
4212
|
const server = startServer(config);
|
|
4213
|
+
consola.info(`copilot-bridge version: ${BRIDGE_VERSION}`);
|
|
4214
|
+
if (authSession.githubLogin) consola.info(`GitHub user: ${authSession.githubLogin}`);
|
|
4215
|
+
else if (authSession.source === "copilot-token") consola.info("GitHub user: unavailable (using COPILOT_TOKEN)");
|
|
4216
|
+
else consola.warn("GitHub user: unavailable");
|
|
4147
4217
|
consola.success(`copilot-bridge listening on http://${config.host}:${config.port}`);
|
|
4148
4218
|
consola.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
4149
4219
|
const baseUrl = `http://${config.host}:${config.port}`;
|
|
@@ -4180,7 +4250,8 @@ const start = defineCommand({
|
|
|
4180
4250
|
configPath: codexConfigPath,
|
|
4181
4251
|
settings: CODEX_DEFAULTS,
|
|
4182
4252
|
model: chosenModel,
|
|
4183
|
-
modelReasoningEffort: chosenEffort
|
|
4253
|
+
modelReasoningEffort: chosenEffort,
|
|
4254
|
+
modelContextWindow: getCodexModelContextWindow(chosenModel)
|
|
4184
4255
|
});
|
|
4185
4256
|
if (result.changed) {
|
|
4186
4257
|
consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
|
|
@@ -4221,7 +4292,8 @@ const start = defineCommand({
|
|
|
4221
4292
|
configPath: codexConfigPath,
|
|
4222
4293
|
settings: CODEX_DEFAULTS,
|
|
4223
4294
|
model: chosenModel,
|
|
4224
|
-
modelReasoningEffort: chosenEffort
|
|
4295
|
+
modelReasoningEffort: chosenEffort,
|
|
4296
|
+
modelContextWindow: getCodexModelContextWindow(chosenModel)
|
|
4225
4297
|
});
|
|
4226
4298
|
if (result.changed) consola.success(`codex config updated: ${result.configPath}`);
|
|
4227
4299
|
} catch (error) {
|
|
@@ -4229,8 +4301,8 @@ const start = defineCommand({
|
|
|
4229
4301
|
}
|
|
4230
4302
|
}
|
|
4231
4303
|
}
|
|
4232
|
-
await new Promise((resolve, reject) => {
|
|
4233
|
-
server.on("close", resolve);
|
|
4304
|
+
await new Promise((resolve$1, reject) => {
|
|
4305
|
+
server.on("close", resolve$1);
|
|
4234
4306
|
server.on("error", reject);
|
|
4235
4307
|
});
|
|
4236
4308
|
}
|