betahi-copilot-bridge 0.1.1 → 0.1.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 +32 -8
- package/dist/main.js +221 -127
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ endpoint, so [Codex CLI](https://github.com/openai/codex) and
|
|
|
5
5
|
[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) can
|
|
6
6
|
talk to Copilot transparently.
|
|
7
7
|
|
|
8
|
-
**Supports:** Codex CLI and Claude Code out of the box.
|
|
8
|
+
**Supports:** **Codex CLI** and **Claude Code** out of the box.
|
|
9
9
|
|
|
10
10
|
> [!WARNING]
|
|
11
11
|
> This is a reverse-engineered bridge for the GitHub Copilot API. It is not
|
|
@@ -95,6 +95,11 @@ model = "gpt-5.3-codex"
|
|
|
95
95
|
model_reasoning_effort = "high"
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
If `model_reasoning_effort` is omitted, the bridge leaves it unset and Codex
|
|
99
|
+
uses that model's default reasoning effort (a startup info log also reminds you).
|
|
100
|
+
|
|
101
|
+
`model_reasoning_effort` only affects Codex requests; it does not affect Claude.
|
|
102
|
+
|
|
98
103
|
That's it — `codex exec '...'` will now route through the bridge to Copilot.
|
|
99
104
|
|
|
100
105
|
Use `--no-codex-setup` to skip the managed-block writer entirely (e.g. if you
|
|
@@ -112,7 +117,13 @@ limit (about 900k succeeds; around 1,000,046 is rejected as too long).
|
|
|
112
117
|
|
|
113
118
|
Bridge endpoints: `POST /v1/messages`, `POST /v1/messages/count_tokens`.
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
`copilot-bridge start` automatically writes `ANTHROPIC_BASE_URL` (and a dummy
|
|
121
|
+
`ANTHROPIC_AUTH_TOKEN` if absent) into **`~/.claude/settings.json`**, so
|
|
122
|
+
`claude` works the moment the bridge is listening — even on a different host
|
|
123
|
+
or port. Pass `--no-claude-setup` to skip this. All other keys in your
|
|
124
|
+
settings file (model overrides, plugins, marketplaces) are preserved.
|
|
125
|
+
|
|
126
|
+
If you prefer to wire it yourself:
|
|
116
127
|
|
|
117
128
|
```json
|
|
118
129
|
{
|
|
@@ -122,15 +133,20 @@ Point Claude Code at the bridge through **`~/.claude/settings.json`**:
|
|
|
122
133
|
"ANTHROPIC_MODEL": "claude-opus-4.7",
|
|
123
134
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
|
124
135
|
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4.5",
|
|
125
|
-
"
|
|
136
|
+
"MODEL_REASONING_EFFORT": "medium"
|
|
126
137
|
},
|
|
127
138
|
"model": "claude-opus-4.7"
|
|
128
139
|
}
|
|
129
140
|
```
|
|
130
141
|
|
|
131
|
-
`
|
|
132
|
-
`.claude/settings.json` and `.claude/settings.local.json` and
|
|
133
|
-
|
|
142
|
+
`MODEL_REASONING_EFFORT` (case-insensitive key lookup) is also read from the
|
|
143
|
+
project-local `.claude/settings.json` and `.claude/settings.local.json` and
|
|
144
|
+
applied to Claude requests only when the model supports reasoning.
|
|
145
|
+
|
|
146
|
+
For backward compatibility, `COPILOT_REASONING_EFFORT` is accepted as an alias.
|
|
147
|
+
|
|
148
|
+
If Claude-side reasoning is not configured, Claude requests fall back to
|
|
149
|
+
`medium` (when the target model supports reasoning).
|
|
134
150
|
|
|
135
151
|
## Environment overrides
|
|
136
152
|
|
|
@@ -140,7 +156,10 @@ upstream call when the model supports reasoning.
|
|
|
140
156
|
| `COPILOT_ACCOUNT_TYPE` | `individual` \| `business` \| `enterprise`. |
|
|
141
157
|
| `COPILOT_BASE_URL` | Override the upstream Copilot base URL. |
|
|
142
158
|
| `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
|
|
143
|
-
| `
|
|
159
|
+
| `MODEL_REASONING_EFFORT` | Claude-side default reasoning effort (preferred key). |
|
|
160
|
+
| `COPILOT_REASONING_EFFORT` | Alias of `MODEL_REASONING_EFFORT` (backward compatible). |
|
|
161
|
+
|
|
162
|
+
|
|
144
163
|
|
|
145
164
|
## Supported models
|
|
146
165
|
|
|
@@ -189,7 +208,8 @@ accepts upstream.
|
|
|
189
208
|
If an unsupported value is sent for a reasoning-capable model, the bridge
|
|
190
209
|
falls back to the model's default (`medium` for the GPT-5 / Claude Opus 4.7
|
|
191
210
|
families) instead of forwarding an invalid request upstream. Set the default
|
|
192
|
-
globally via `
|
|
211
|
+
globally via `MODEL_REASONING_EFFORT` (or `COPILOT_REASONING_EFFORT` alias)
|
|
212
|
+
(env, or `env` in
|
|
193
213
|
`~/.claude/settings.json`); per-request `reasoning_effort` (or Anthropic
|
|
194
214
|
`thinking.budget_tokens`) takes precedence.
|
|
195
215
|
|
|
@@ -211,3 +231,7 @@ bun run ./src/main.ts start --host 127.0.0.1 --port 4141 --no-prompt
|
|
|
211
231
|
Adding another CLI: drop a new translator under `src/bridges/<client>/`,
|
|
212
232
|
reuse `src/services/copilot/` for upstream calls, register routes in
|
|
213
233
|
`src/server.ts`, and add tests under `tests/`.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/main.js
CHANGED
|
@@ -281,6 +281,111 @@ const auth = defineCommand({
|
|
|
281
281
|
}
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/lib/claude-settings.ts
|
|
286
|
+
const getClaudeSettingsPaths = () => {
|
|
287
|
+
const cwd = process.cwd();
|
|
288
|
+
const home = process.env.HOME ?? os.homedir();
|
|
289
|
+
return [
|
|
290
|
+
path.join(home, ".claude", "settings.json"),
|
|
291
|
+
path.join(cwd, ".claude", "settings.json"),
|
|
292
|
+
path.join(cwd, ".claude", "settings.local.json")
|
|
293
|
+
];
|
|
294
|
+
};
|
|
295
|
+
const readClaudeSettingsFile = async (filePath) => {
|
|
296
|
+
try {
|
|
297
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
298
|
+
return JSON.parse(content);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error.code === "ENOENT") return;
|
|
301
|
+
consola.warn(`Failed to read Claude settings from ${filePath}:`, error);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Read just the `ANTHROPIC_BASE_URL` from a single Claude settings file.
|
|
307
|
+
* Used at startup so the user can pin the bridge port by editing one place
|
|
308
|
+
* instead of passing `--port` every time.
|
|
309
|
+
*/
|
|
310
|
+
const readClaudeBaseUrl = async (configPath) => {
|
|
311
|
+
const value = (await readClaudeSettingsFile(configPath))?.env?.ANTHROPIC_BASE_URL;
|
|
312
|
+
return typeof value === "string" ? value : void 0;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Parse the port from an `ANTHROPIC_BASE_URL`-style string. Returns
|
|
316
|
+
* `undefined` if the URL is malformed or has no explicit port.
|
|
317
|
+
*/
|
|
318
|
+
const parsePortFromBaseUrl = (baseUrl) => {
|
|
319
|
+
if (!baseUrl) return void 0;
|
|
320
|
+
try {
|
|
321
|
+
const url = new URL(baseUrl);
|
|
322
|
+
if (!url.port) return void 0;
|
|
323
|
+
const port = Number.parseInt(url.port, 10);
|
|
324
|
+
return Number.isFinite(port) && port > 0 ? port : void 0;
|
|
325
|
+
} catch {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const getClaudeSettingsEnv = async () => {
|
|
330
|
+
const merged = {};
|
|
331
|
+
for (const filePath of getClaudeSettingsPaths()) {
|
|
332
|
+
const settings = await readClaudeSettingsFile(filePath);
|
|
333
|
+
if (!settings?.env) continue;
|
|
334
|
+
for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
|
|
335
|
+
}
|
|
336
|
+
return merged;
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Update `~/.claude/settings.json` so its env block points Claude Code at the
|
|
340
|
+
* running bridge. Preserves all unrelated keys (model overrides, plugins,
|
|
341
|
+
* marketplaces, etc.). Sets a dummy auth token only if none is present.
|
|
342
|
+
*/
|
|
343
|
+
async function applyClaudeConfig(input) {
|
|
344
|
+
const { configPath, baseUrl } = input;
|
|
345
|
+
let raw = "";
|
|
346
|
+
let created = false;
|
|
347
|
+
try {
|
|
348
|
+
raw = await fs.readFile(configPath, "utf8");
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error.code === "ENOENT") created = true;
|
|
351
|
+
else throw error;
|
|
352
|
+
}
|
|
353
|
+
let parsed = {};
|
|
354
|
+
if (raw.trim().length > 0) try {
|
|
355
|
+
parsed = JSON.parse(raw);
|
|
356
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) parsed = {};
|
|
357
|
+
} catch {
|
|
358
|
+
return {
|
|
359
|
+
configPath,
|
|
360
|
+
changed: false,
|
|
361
|
+
created: false
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const env = typeof parsed.env === "object" && parsed.env !== null && !Array.isArray(parsed.env) ? { ...parsed.env } : {};
|
|
365
|
+
const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
366
|
+
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
367
|
+
if (typeof env.ANTHROPIC_AUTH_TOKEN !== "string" || !env.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = "dummy";
|
|
368
|
+
const next = {
|
|
369
|
+
...parsed,
|
|
370
|
+
env
|
|
371
|
+
};
|
|
372
|
+
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
373
|
+
if (serialized === raw) return {
|
|
374
|
+
configPath,
|
|
375
|
+
changed: false,
|
|
376
|
+
created: false,
|
|
377
|
+
previousBaseUrl
|
|
378
|
+
};
|
|
379
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
380
|
+
await fs.writeFile(configPath, serialized);
|
|
381
|
+
return {
|
|
382
|
+
configPath,
|
|
383
|
+
changed: true,
|
|
384
|
+
created,
|
|
385
|
+
previousBaseUrl
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
284
389
|
//#endregion
|
|
285
390
|
//#region src/lib/codex-config.ts
|
|
286
391
|
const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>";
|
|
@@ -395,6 +500,18 @@ async function readCodexUserConfigFromDisk(configPath) {
|
|
|
395
500
|
}
|
|
396
501
|
}
|
|
397
502
|
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/lib/defaults.ts
|
|
505
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
506
|
+
const DEFAULT_PORT = 4142;
|
|
507
|
+
const CODEX_DEFAULTS = {
|
|
508
|
+
providerId: "bridge",
|
|
509
|
+
providerName: "Copilot Bridge",
|
|
510
|
+
setAsDefault: true,
|
|
511
|
+
configPath: path.join(os.homedir(), ".codex", "config.toml")
|
|
512
|
+
};
|
|
513
|
+
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
514
|
+
|
|
398
515
|
//#endregion
|
|
399
516
|
//#region src/lib/model-capabilities.ts
|
|
400
517
|
const MODEL_CAPABILITIES = [
|
|
@@ -615,49 +732,6 @@ const clampReasoningEffort = (modelId, requested) => {
|
|
|
615
732
|
};
|
|
616
733
|
};
|
|
617
734
|
|
|
618
|
-
//#endregion
|
|
619
|
-
//#region src/lib/settings.ts
|
|
620
|
-
const SETTINGS_DIR = path.join(os.homedir(), ".config", "copilot-bridge");
|
|
621
|
-
const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
|
|
622
|
-
const DEFAULT_SETTINGS = {
|
|
623
|
-
host: "127.0.0.1",
|
|
624
|
-
port: 4142,
|
|
625
|
-
codex: {
|
|
626
|
-
enabled: true,
|
|
627
|
-
providerId: "bridge",
|
|
628
|
-
providerName: "Copilot Bridge",
|
|
629
|
-
setAsDefault: true,
|
|
630
|
-
configPath: path.join(os.homedir(), ".codex", "config.toml")
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
function deepMerge(base, override) {
|
|
634
|
-
if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) return override ?? base;
|
|
635
|
-
const out = { ...base };
|
|
636
|
-
for (const [key, value] of Object.entries(override)) out[key] = deepMerge(base[key], value);
|
|
637
|
-
return out;
|
|
638
|
-
}
|
|
639
|
-
async function loadSettings() {
|
|
640
|
-
try {
|
|
641
|
-
const raw = await fs.readFile(SETTINGS_FILE, "utf8");
|
|
642
|
-
return deepMerge(DEFAULT_SETTINGS, JSON.parse(raw));
|
|
643
|
-
} catch {
|
|
644
|
-
return {
|
|
645
|
-
...DEFAULT_SETTINGS,
|
|
646
|
-
codex: { ...DEFAULT_SETTINGS.codex }
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
async function ensureSettingsFile() {
|
|
651
|
-
const settings = await loadSettings();
|
|
652
|
-
try {
|
|
653
|
-
await fs.access(SETTINGS_FILE);
|
|
654
|
-
} catch {
|
|
655
|
-
await fs.mkdir(SETTINGS_DIR, { recursive: true });
|
|
656
|
-
await fs.writeFile(SETTINGS_FILE, `${JSON.stringify(settings, null, 2)}\n`);
|
|
657
|
-
}
|
|
658
|
-
return settings;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
735
|
//#endregion
|
|
662
736
|
//#region src/lib/rate-limit.ts
|
|
663
737
|
const sleep = (ms) => new Promise((resolve) => {
|
|
@@ -702,37 +776,6 @@ var RateLimitError = class extends Error {
|
|
|
702
776
|
}
|
|
703
777
|
};
|
|
704
778
|
|
|
705
|
-
//#endregion
|
|
706
|
-
//#region src/lib/claude-settings.ts
|
|
707
|
-
const getClaudeSettingsPaths = () => {
|
|
708
|
-
const cwd = process.cwd();
|
|
709
|
-
const home = process.env.HOME ?? os.homedir();
|
|
710
|
-
return [
|
|
711
|
-
path.join(home, ".claude", "settings.json"),
|
|
712
|
-
path.join(cwd, ".claude", "settings.json"),
|
|
713
|
-
path.join(cwd, ".claude", "settings.local.json")
|
|
714
|
-
];
|
|
715
|
-
};
|
|
716
|
-
const readClaudeSettingsFile = async (filePath) => {
|
|
717
|
-
try {
|
|
718
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
719
|
-
return JSON.parse(content);
|
|
720
|
-
} catch (error) {
|
|
721
|
-
if (error.code === "ENOENT") return;
|
|
722
|
-
consola.warn(`Failed to read Claude settings from ${filePath}:`, error);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
|
-
const getClaudeSettingsEnv = async () => {
|
|
727
|
-
const merged = {};
|
|
728
|
-
for (const filePath of getClaudeSettingsPaths()) {
|
|
729
|
-
const settings = await readClaudeSettingsFile(filePath);
|
|
730
|
-
if (!settings?.env) continue;
|
|
731
|
-
for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
|
|
732
|
-
}
|
|
733
|
-
return merged;
|
|
734
|
-
};
|
|
735
|
-
|
|
736
779
|
//#endregion
|
|
737
780
|
//#region src/services/copilot/responses.ts
|
|
738
781
|
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.4-mini)(?:-|$)/i;
|
|
@@ -961,7 +1004,7 @@ function getFinishReason(response, hasFunctionCalls) {
|
|
|
961
1004
|
//#endregion
|
|
962
1005
|
//#region src/services/copilot/create-chat-completions.ts
|
|
963
1006
|
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
964
|
-
const isClaudeOpus47Model$1 = (modelId) => modelId
|
|
1007
|
+
const isClaudeOpus47Model$1 = (modelId) => modelId.startsWith("claude-opus-4.7");
|
|
965
1008
|
const MAX_USER_LENGTH = 64;
|
|
966
1009
|
const defaultReasoningEffort = (modelId) => usesMaxCompletionTokens(modelId) ? "medium" : void 0;
|
|
967
1010
|
const getAllowedReasoningEfforts = (modelId) => {
|
|
@@ -1016,33 +1059,47 @@ const normalizeClaudeOpus47Effort = (value) => {
|
|
|
1016
1059
|
default: return;
|
|
1017
1060
|
}
|
|
1018
1061
|
};
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1062
|
+
const getEnvValueCaseInsensitive = (env, key) => {
|
|
1063
|
+
const direct = env[key];
|
|
1064
|
+
if (typeof direct === "string") return direct;
|
|
1065
|
+
const lower = key.toLowerCase();
|
|
1066
|
+
return Object.entries(env).find(([k]) => k.toLowerCase() === lower)?.[1];
|
|
1067
|
+
};
|
|
1068
|
+
const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
|
|
1069
|
+
if (client !== "claude") return;
|
|
1070
|
+
return process.env.MODEL_REASONING_EFFORT ?? process.env.COPILOT_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT") ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "COPILOT_REASONING_EFFORT");
|
|
1071
|
+
};
|
|
1072
|
+
const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
|
|
1073
|
+
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1074
|
+
const defaultEffort = client === "claude" ? "medium" : defaultReasoningEffort(payload.model);
|
|
1075
|
+
const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
|
|
1076
|
+
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultEffort;
|
|
1022
1077
|
};
|
|
1023
|
-
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv) => {
|
|
1078
|
+
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
|
|
1024
1079
|
if (!isClaudeOpus47Model$1(payload.model)) return;
|
|
1025
|
-
|
|
1080
|
+
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1081
|
+
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort) ?? (client === "claude" ? "medium" : void 0);
|
|
1026
1082
|
};
|
|
1027
1083
|
const sanitizeUserIdentifier = (user) => {
|
|
1028
1084
|
if (!user) return;
|
|
1029
1085
|
return user.slice(0, MAX_USER_LENGTH);
|
|
1030
1086
|
};
|
|
1031
|
-
const buildRequestPayload = (payload, claudeSettingsEnv) => {
|
|
1032
|
-
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1033
|
-
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv);
|
|
1087
|
+
const buildRequestPayload = (payload, claudeSettingsEnv, client) => {
|
|
1088
|
+
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
|
|
1089
|
+
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv, client);
|
|
1034
1090
|
const reasoningEffort = usesMaxCompletionTokens(payload.model) && payload.tools !== null && payload.tools !== void 0 && payload.tools.length > 0 ? void 0 : requestedReasoningEffort;
|
|
1035
1091
|
if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) {
|
|
1092
|
+
const shouldAttachReasoningEffort = !isClaudeOpus47Model$1(payload.model);
|
|
1036
1093
|
const sanitizedPayload = {
|
|
1037
1094
|
...payload,
|
|
1038
1095
|
output_config: requestedClaudeOpus47Effort ? {
|
|
1039
1096
|
...payload.output_config,
|
|
1040
1097
|
effort: requestedClaudeOpus47Effort
|
|
1041
1098
|
} : payload.output_config,
|
|
1042
|
-
reasoning_effort:
|
|
1099
|
+
reasoning_effort: shouldAttachReasoningEffort ? payload.reasoning_effort : void 0,
|
|
1043
1100
|
user: sanitizeUserIdentifier(payload.user)
|
|
1044
1101
|
};
|
|
1045
|
-
return reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
|
|
1102
|
+
return !shouldAttachReasoningEffort || reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
|
|
1046
1103
|
...sanitizedPayload,
|
|
1047
1104
|
reasoning_effort: reasoningEffort
|
|
1048
1105
|
};
|
|
@@ -1057,13 +1114,14 @@ const buildRequestPayload = (payload, claudeSettingsEnv) => {
|
|
|
1057
1114
|
};
|
|
1058
1115
|
const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
|
|
1059
1116
|
const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
1060
|
-
const createChatCompletions = async (config, payload) => {
|
|
1117
|
+
const createChatCompletions = async (config, payload, options) => {
|
|
1061
1118
|
const provider = getCopilotProviderContext(config);
|
|
1062
1119
|
const enableVision = messagesIncludeImage(payload.messages);
|
|
1063
1120
|
const initiator = isAgentInitiator(payload.messages);
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1121
|
+
const client = options?.client ?? "generic";
|
|
1122
|
+
const claudeSettingsEnv = client === "claude" ? await getClaudeSettingsEnv() : {};
|
|
1123
|
+
const requestPayload = buildRequestPayload(payload, claudeSettingsEnv, client);
|
|
1124
|
+
if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, client, {
|
|
1067
1125
|
vision: enableVision,
|
|
1068
1126
|
initiator
|
|
1069
1127
|
});
|
|
@@ -1079,7 +1137,7 @@ const createChatCompletions = async (config, payload) => {
|
|
|
1079
1137
|
initiator
|
|
1080
1138
|
});
|
|
1081
1139
|
if (!response.ok) {
|
|
1082
|
-
if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, {
|
|
1140
|
+
if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, client, {
|
|
1083
1141
|
vision: enableVision,
|
|
1084
1142
|
initiator
|
|
1085
1143
|
});
|
|
@@ -1089,8 +1147,8 @@ const createChatCompletions = async (config, payload) => {
|
|
|
1089
1147
|
if (payload.stream) return events(response);
|
|
1090
1148
|
return await response.json();
|
|
1091
1149
|
};
|
|
1092
|
-
async function createResponses(provider, payload, claudeSettingsEnv, options) {
|
|
1093
|
-
const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1150
|
+
async function createResponses(provider, payload, claudeSettingsEnv, client, options) {
|
|
1151
|
+
const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
|
|
1094
1152
|
const response = await fetchCopilot(provider, "/responses", {
|
|
1095
1153
|
method: "POST",
|
|
1096
1154
|
headers: {
|
|
@@ -1125,7 +1183,7 @@ chatCompletionRoutes.post("/", async (c) => {
|
|
|
1125
1183
|
try {
|
|
1126
1184
|
await checkRateLimit();
|
|
1127
1185
|
const payload = await c.req.json();
|
|
1128
|
-
const response = await createChatCompletions(c.var.config, payload);
|
|
1186
|
+
const response = await createChatCompletions(c.var.config, payload, { client: "generic" });
|
|
1129
1187
|
if (isNonStreaming(response)) return c.json(response);
|
|
1130
1188
|
return streamSSE(c, async (stream) => {
|
|
1131
1189
|
for await (const chunk of response) await stream.writeSSE(chunk);
|
|
@@ -1272,7 +1330,7 @@ function isClaudeModel(modelId) {
|
|
|
1272
1330
|
return modelId.startsWith("claude-");
|
|
1273
1331
|
}
|
|
1274
1332
|
function isClaudeOpus47Model(modelId) {
|
|
1275
|
-
return modelId
|
|
1333
|
+
return modelId.startsWith("claude-opus-4.7");
|
|
1276
1334
|
}
|
|
1277
1335
|
function normalizeClaudeEffort(value) {
|
|
1278
1336
|
switch (value?.toLowerCase()) {
|
|
@@ -1814,7 +1872,7 @@ messageRoutes.post("/", async (c) => {
|
|
|
1814
1872
|
}
|
|
1815
1873
|
const openAIPayload = translateToOpenAI(await c.req.json());
|
|
1816
1874
|
try {
|
|
1817
|
-
const response = await createChatCompletions(config, openAIPayload);
|
|
1875
|
+
const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
|
|
1818
1876
|
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
|
|
1819
1877
|
return streamSSE(c, async (stream) => {
|
|
1820
1878
|
const streamState = {
|
|
@@ -2428,11 +2486,11 @@ const start = defineCommand({
|
|
|
2428
2486
|
args: {
|
|
2429
2487
|
host: {
|
|
2430
2488
|
type: "string",
|
|
2431
|
-
description:
|
|
2489
|
+
description: `Host to bind (default: ${DEFAULT_HOST}).`
|
|
2432
2490
|
},
|
|
2433
2491
|
port: {
|
|
2434
2492
|
type: "string",
|
|
2435
|
-
description:
|
|
2493
|
+
description: `Port to listen on (default: ${DEFAULT_PORT}).`
|
|
2436
2494
|
},
|
|
2437
2495
|
"show-token": {
|
|
2438
2496
|
type: "boolean",
|
|
@@ -2444,15 +2502,20 @@ const start = defineCommand({
|
|
|
2444
2502
|
default: false,
|
|
2445
2503
|
description: "Skip writing the bridge provider into ~/.codex/config.toml."
|
|
2446
2504
|
},
|
|
2505
|
+
"no-claude-setup": {
|
|
2506
|
+
type: "boolean",
|
|
2507
|
+
default: false,
|
|
2508
|
+
description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
|
|
2509
|
+
},
|
|
2447
2510
|
"select-model": {
|
|
2448
2511
|
type: "boolean",
|
|
2449
2512
|
default: false,
|
|
2450
|
-
description: "Force the model picker even when codex.
|
|
2513
|
+
description: "Force the model picker even when ~/.codex/config.toml already has a model."
|
|
2451
2514
|
},
|
|
2452
2515
|
"no-prompt": {
|
|
2453
2516
|
type: "boolean",
|
|
2454
2517
|
default: false,
|
|
2455
|
-
description: "Never prompt; use
|
|
2518
|
+
description: "Never prompt; use the existing model from ~/.codex/config.toml as-is."
|
|
2456
2519
|
},
|
|
2457
2520
|
"rate-limit": {
|
|
2458
2521
|
type: "string",
|
|
@@ -2465,10 +2528,13 @@ const start = defineCommand({
|
|
|
2465
2528
|
}
|
|
2466
2529
|
},
|
|
2467
2530
|
async run({ args }) {
|
|
2468
|
-
const
|
|
2531
|
+
const claudeConfigPath = CLAUDE_CONFIG_PATH;
|
|
2532
|
+
const claudePort = parsePortFromBaseUrl(await readClaudeBaseUrl(claudeConfigPath));
|
|
2533
|
+
const envPortRaw = process.env.PORT;
|
|
2534
|
+
const envPort = envPortRaw && Number.isFinite(Number(envPortRaw)) ? Number(envPortRaw) : void 0;
|
|
2469
2535
|
const config = readBridgeConfig({
|
|
2470
|
-
host: args.host ? String(args.host) :
|
|
2471
|
-
port: args.port ? Number(args.port) :
|
|
2536
|
+
host: args.host ? String(args.host) : DEFAULT_HOST,
|
|
2537
|
+
port: args.port ? Number(args.port) : envPort !== void 0 ? envPort : claudePort !== void 0 ? claudePort : DEFAULT_PORT
|
|
2472
2538
|
});
|
|
2473
2539
|
const rateLimitRaw = args["rate-limit"];
|
|
2474
2540
|
if (rateLimitRaw !== void 0) {
|
|
@@ -2485,48 +2551,76 @@ const start = defineCommand({
|
|
|
2485
2551
|
const server = startServer(config);
|
|
2486
2552
|
consola.success(`copilot-bridge listening on http://${config.host}:${config.port}`);
|
|
2487
2553
|
consola.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2554
|
+
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2555
|
+
const codexConfigPath = CODEX_DEFAULTS.configPath;
|
|
2556
|
+
if (!args["no-claude-setup"]) try {
|
|
2557
|
+
const claudeResult = await applyClaudeConfig({
|
|
2558
|
+
baseUrl,
|
|
2559
|
+
configPath: claudeConfigPath
|
|
2560
|
+
});
|
|
2561
|
+
if (claudeResult.changed) {
|
|
2562
|
+
consola.success(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
|
|
2563
|
+
if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) consola.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
|
|
2564
|
+
} else consola.info(`claude settings already up to date: ${claudeResult.configPath}`);
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
consola.warn(`Could not update claude settings (${claudeConfigPath}):`, error);
|
|
2567
|
+
}
|
|
2568
|
+
const codexUserConfig = await readCodexUserConfigFromDisk(codexConfigPath);
|
|
2569
|
+
let chosenModel = codexUserConfig.model;
|
|
2570
|
+
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2571
|
+
if (chosenModel && !chosenEffort) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
|
|
2572
|
+
if (!args["no-codex-setup"]) try {
|
|
2573
|
+
const result = await applyCodexConfig({
|
|
2574
|
+
baseUrl: `${baseUrl}/v1`,
|
|
2575
|
+
configPath: codexConfigPath,
|
|
2576
|
+
settings: CODEX_DEFAULTS,
|
|
2577
|
+
model: chosenModel,
|
|
2578
|
+
modelReasoningEffort: chosenEffort
|
|
2579
|
+
});
|
|
2580
|
+
if (result.changed) {
|
|
2581
|
+
consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
|
|
2582
|
+
if (CODEX_DEFAULTS.setAsDefault) consola.info(`codex now defaults to provider "${CODEX_DEFAULTS.providerId}". Run \`codex exec "..."\` directly.`);
|
|
2583
|
+
else consola.info(`provider "${CODEX_DEFAULTS.providerId}" registered. Use \`codex -c model_provider="${CODEX_DEFAULTS.providerId}" ...\``);
|
|
2584
|
+
} else consola.info(`codex config already up to date: ${result.configPath}`);
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
consola.warn(`Could not update codex config (${codexConfigPath}):`, error);
|
|
2587
|
+
}
|
|
2488
2588
|
const models = await fetchAvailableModels(config);
|
|
2489
2589
|
const supportedIds = new Set(MODEL_CAPABILITIES.map((m) => m.id));
|
|
2490
2590
|
const pickable = models.length > 0 ? models.filter((id) => supportedIds.has(id)) : MODEL_CAPABILITIES.map((m) => m.id);
|
|
2491
2591
|
const finalPickable = pickable.length > 0 ? pickable : MODEL_CAPABILITIES.map((m) => m.id);
|
|
2492
2592
|
if (models.length > 0) consola.info(`Available models:\n${models.map((id) => `- ${id}${supportedIds.has(id) ? " (bridge-supported)" : ""}`).join("\n")}`);
|
|
2493
2593
|
else consola.warn("Could not fetch model list from upstream Copilot API");
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2594
|
+
consola.box([
|
|
2595
|
+
`🌐 Usage viewer`,
|
|
2596
|
+
``,
|
|
2597
|
+
` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
|
|
2598
|
+
].join("\n"));
|
|
2497
2599
|
if (!args["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
|
|
2498
2600
|
const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
|
|
2499
|
-
const selected = await consola.prompt(`Select a model for codex (writes to ${
|
|
2601
|
+
const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
|
|
2500
2602
|
type: "select",
|
|
2501
2603
|
options: finalPickable,
|
|
2502
2604
|
initial: defaultId
|
|
2503
2605
|
});
|
|
2504
|
-
if (selected) {
|
|
2606
|
+
if (selected && selected !== chosenModel) {
|
|
2505
2607
|
chosenModel = selected;
|
|
2506
2608
|
const supported = getModelCapability(selected)?.reasoning?.supported;
|
|
2507
2609
|
if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
|
|
2610
|
+
if (!args["no-codex-setup"]) try {
|
|
2611
|
+
const result = await applyCodexConfig({
|
|
2612
|
+
baseUrl: `${baseUrl}/v1`,
|
|
2613
|
+
configPath: codexConfigPath,
|
|
2614
|
+
settings: CODEX_DEFAULTS,
|
|
2615
|
+
model: chosenModel,
|
|
2616
|
+
modelReasoningEffort: chosenEffort
|
|
2617
|
+
});
|
|
2618
|
+
if (result.changed) consola.success(`codex config updated: ${result.configPath}`);
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
consola.warn(`Could not update codex config (${codexConfigPath}):`, error);
|
|
2621
|
+
}
|
|
2508
2622
|
}
|
|
2509
2623
|
}
|
|
2510
|
-
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2511
|
-
consola.box([
|
|
2512
|
-
`🌐 Usage viewer`,
|
|
2513
|
-
``,
|
|
2514
|
-
` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
|
|
2515
|
-
].join("\n"));
|
|
2516
|
-
if (settings.codex.enabled && !args["no-codex-setup"]) {
|
|
2517
|
-
const result = await applyCodexConfig({
|
|
2518
|
-
baseUrl: `${baseUrl}/v1`,
|
|
2519
|
-
configPath: settings.codex.configPath,
|
|
2520
|
-
settings: settings.codex,
|
|
2521
|
-
model: chosenModel,
|
|
2522
|
-
modelReasoningEffort: chosenEffort
|
|
2523
|
-
});
|
|
2524
|
-
if (result.changed) {
|
|
2525
|
-
consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
|
|
2526
|
-
if (settings.codex.setAsDefault) consola.info(`codex now defaults to provider "${settings.codex.providerId}". Run \`codex exec "..."\` directly.`);
|
|
2527
|
-
else consola.info(`provider "${settings.codex.providerId}" registered. Use \`codex -c model_provider="${settings.codex.providerId}" ...\``);
|
|
2528
|
-
} else consola.info(`codex config already up to date: ${result.configPath}`);
|
|
2529
|
-
}
|
|
2530
2624
|
await new Promise((resolve, reject) => {
|
|
2531
2625
|
server.on("close", resolve);
|
|
2532
2626
|
server.on("error", reject);
|