betahi-copilot-bridge 0.1.2 → 0.1.4
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 +45 -15
- package/dist/main.js +187 -89
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">copilot-bridge</h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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" alt="npm version"></a>
|
|
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
|
+
</p>
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
> Use GitHub Copilot as a local OpenAI/Anthropic-compatible API, so [Codex CLI](https://github.com/openai/codex), [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) and Continue can talk to Copilot with minimal configuration.
|
|
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
|
|
@@ -118,7 +123,20 @@ Bridge endpoints: `POST /v1/messages`, `POST /v1/messages/count_tokens`.
|
|
|
118
123
|
or port. Pass `--no-claude-setup` to skip this. All other keys in your
|
|
119
124
|
settings file (model overrides, plugins, marketplaces) are preserved.
|
|
120
125
|
|
|
121
|
-
|
|
126
|
+
Minimal recommended config is:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"env": {
|
|
131
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:4142",
|
|
132
|
+
"ANTHROPIC_AUTH_TOKEN": "dummy",
|
|
133
|
+
"ANTHROPIC_MODEL": "claude-opus-4.7",
|
|
134
|
+
"MODEL_REASONING_EFFORT": "medium"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Optional slot-specific:
|
|
122
140
|
|
|
123
141
|
```json
|
|
124
142
|
{
|
|
@@ -128,15 +146,19 @@ If you prefer to wire it yourself:
|
|
|
128
146
|
"ANTHROPIC_MODEL": "claude-opus-4.7",
|
|
129
147
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
|
130
148
|
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4.5",
|
|
131
|
-
"
|
|
132
|
-
}
|
|
133
|
-
"model": "claude-opus-4.7"
|
|
149
|
+
"MODEL_REASONING_EFFORT": "medium"
|
|
150
|
+
}
|
|
134
151
|
}
|
|
135
152
|
```
|
|
136
153
|
|
|
137
|
-
`
|
|
138
|
-
`.claude/settings.json` and `.claude/settings.local.json` and
|
|
139
|
-
|
|
154
|
+
`MODEL_REASONING_EFFORT` (case-insensitive key lookup) is also read from the
|
|
155
|
+
project-local `.claude/settings.json` and `.claude/settings.local.json` and
|
|
156
|
+
applied to Claude requests only when the model supports reasoning.
|
|
157
|
+
|
|
158
|
+
For backward compatibility, `COPILOT_REASONING_EFFORT` is accepted as an alias.
|
|
159
|
+
|
|
160
|
+
If Claude-side reasoning is not configured, Claude requests fall back to
|
|
161
|
+
`medium` (when the target model supports reasoning).
|
|
140
162
|
|
|
141
163
|
## Environment overrides
|
|
142
164
|
|
|
@@ -146,7 +168,10 @@ upstream call when the model supports reasoning.
|
|
|
146
168
|
| `COPILOT_ACCOUNT_TYPE` | `individual` \| `business` \| `enterprise`. |
|
|
147
169
|
| `COPILOT_BASE_URL` | Override the upstream Copilot base URL. |
|
|
148
170
|
| `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
|
|
149
|
-
| `
|
|
171
|
+
| `MODEL_REASONING_EFFORT` | Claude-side default reasoning effort (preferred key). |
|
|
172
|
+
| `COPILOT_REASONING_EFFORT` | Alias of `MODEL_REASONING_EFFORT` (backward compatible). |
|
|
173
|
+
|
|
174
|
+
|
|
150
175
|
|
|
151
176
|
## Supported models
|
|
152
177
|
|
|
@@ -195,7 +220,8 @@ accepts upstream.
|
|
|
195
220
|
If an unsupported value is sent for a reasoning-capable model, the bridge
|
|
196
221
|
falls back to the model's default (`medium` for the GPT-5 / Claude Opus 4.7
|
|
197
222
|
families) instead of forwarding an invalid request upstream. Set the default
|
|
198
|
-
globally via `
|
|
223
|
+
globally via `MODEL_REASONING_EFFORT` (or `COPILOT_REASONING_EFFORT` alias)
|
|
224
|
+
(env, or `env` in
|
|
199
225
|
`~/.claude/settings.json`); per-request `reasoning_effort` (or Anthropic
|
|
200
226
|
`thinking.budget_tokens`) takes precedence.
|
|
201
227
|
|
|
@@ -217,3 +243,7 @@ bun run ./src/main.ts start --host 127.0.0.1 --port 4141 --no-prompt
|
|
|
217
243
|
Adding another CLI: drop a new translator under `src/bridges/<client>/`,
|
|
218
244
|
reuse `src/services/copilot/` for upstream calls, register routes in
|
|
219
245
|
`src/server.ts`, and add tests under `tests/`.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/main.js
CHANGED
|
@@ -326,15 +326,21 @@ const parsePortFromBaseUrl = (baseUrl) => {
|
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
328
|
};
|
|
329
|
-
const
|
|
329
|
+
const getClaudeSettings = async () => {
|
|
330
330
|
const merged = {};
|
|
331
|
+
let model;
|
|
331
332
|
for (const filePath of getClaudeSettingsPaths()) {
|
|
332
333
|
const settings = await readClaudeSettingsFile(filePath);
|
|
334
|
+
if (typeof settings?.model === "string") model = settings.model;
|
|
333
335
|
if (!settings?.env) continue;
|
|
334
336
|
for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
|
|
335
337
|
}
|
|
336
|
-
return
|
|
338
|
+
return {
|
|
339
|
+
env: merged,
|
|
340
|
+
model
|
|
341
|
+
};
|
|
337
342
|
};
|
|
343
|
+
const getClaudeSettingsEnv = async () => (await getClaudeSettings()).env;
|
|
338
344
|
/**
|
|
339
345
|
* Update `~/.claude/settings.json` so its env block points Claude Code at the
|
|
340
346
|
* running bridge. Preserves all unrelated keys (model overrides, plugins,
|
|
@@ -386,6 +392,22 @@ async function applyClaudeConfig(input) {
|
|
|
386
392
|
};
|
|
387
393
|
}
|
|
388
394
|
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/lib/claude-launch.ts
|
|
397
|
+
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
398
|
+
const pickClaudeLaunchDefaults = (availableModels, preferredModel) => {
|
|
399
|
+
return { model: preferredModel && availableModels.includes(preferredModel) ? preferredModel : availableModels.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : availableModels[0] ?? "gpt-5.3-codex" };
|
|
400
|
+
};
|
|
401
|
+
const buildClaudeLaunchCommand = (input) => {
|
|
402
|
+
const env = {
|
|
403
|
+
ANTHROPIC_BASE_URL: input.baseUrl,
|
|
404
|
+
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
405
|
+
DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
|
|
406
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
407
|
+
};
|
|
408
|
+
return `${Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")} && claude --model ${shellQuote(input.model)}`;
|
|
409
|
+
};
|
|
410
|
+
|
|
389
411
|
//#endregion
|
|
390
412
|
//#region src/lib/codex-config.ts
|
|
391
413
|
const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>";
|
|
@@ -585,6 +607,10 @@ const MODEL_CAPABILITIES = [
|
|
|
585
607
|
"xhigh"
|
|
586
608
|
],
|
|
587
609
|
default: "medium"
|
|
610
|
+
},
|
|
611
|
+
textVerbosity: {
|
|
612
|
+
supported: ["medium"],
|
|
613
|
+
default: "medium"
|
|
588
614
|
}
|
|
589
615
|
},
|
|
590
616
|
{
|
|
@@ -696,6 +722,8 @@ const CAPABILITY_BY_ID = new Map(MODEL_CAPABILITIES.flatMap((m) => {
|
|
|
696
722
|
const resolveModelId = (modelId) => CAPABILITY_BY_ID.get(modelId)?.id ?? modelId;
|
|
697
723
|
const getModelCapability = (modelId) => CAPABILITY_BY_ID.get(modelId);
|
|
698
724
|
const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
725
|
+
const isTextVerbosity = (value) => value === "low" || value === "medium" || value === "high";
|
|
726
|
+
const normalizeRequestedReasoningEffort = (requested) => requested === "minimal" ? "low" : requested;
|
|
699
727
|
const clampReasoningEffort = (modelId, requested) => {
|
|
700
728
|
const capability = getModelCapability(modelId);
|
|
701
729
|
if (!capability?.reasoning) return void 0;
|
|
@@ -704,14 +732,16 @@ const clampReasoningEffort = (modelId, requested) => {
|
|
|
704
732
|
effort: defaultEffort,
|
|
705
733
|
changed: false
|
|
706
734
|
};
|
|
707
|
-
|
|
735
|
+
const normalizedRequested = normalizeRequestedReasoningEffort(requested);
|
|
736
|
+
if (!isReasoningEffort(normalizedRequested)) return {
|
|
708
737
|
effort: defaultEffort,
|
|
709
738
|
changed: true,
|
|
710
739
|
reason: "unsupported-effort"
|
|
711
740
|
};
|
|
712
|
-
if (supported.includes(
|
|
713
|
-
effort:
|
|
714
|
-
changed:
|
|
741
|
+
if (supported.includes(normalizedRequested)) return {
|
|
742
|
+
effort: normalizedRequested,
|
|
743
|
+
changed: normalizedRequested !== requested,
|
|
744
|
+
reason: normalizedRequested !== requested ? "unsupported-effort" : void 0
|
|
715
745
|
};
|
|
716
746
|
const order = [
|
|
717
747
|
"none",
|
|
@@ -721,7 +751,7 @@ const clampReasoningEffort = (modelId, requested) => {
|
|
|
721
751
|
"xhigh",
|
|
722
752
|
"max"
|
|
723
753
|
];
|
|
724
|
-
const reqIdx = order.indexOf(
|
|
754
|
+
const reqIdx = order.indexOf(normalizedRequested);
|
|
725
755
|
const supportedSorted = [...supported].sort((a, b) => order.indexOf(a) - order.indexOf(b));
|
|
726
756
|
const lowest = supportedSorted[0];
|
|
727
757
|
const highest = supportedSorted[supportedSorted.length - 1];
|
|
@@ -1007,36 +1037,9 @@ const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
|
1007
1037
|
const isClaudeOpus47Model$1 = (modelId) => modelId.startsWith("claude-opus-4.7");
|
|
1008
1038
|
const MAX_USER_LENGTH = 64;
|
|
1009
1039
|
const defaultReasoningEffort = (modelId) => usesMaxCompletionTokens(modelId) ? "medium" : void 0;
|
|
1010
|
-
const getAllowedReasoningEfforts = (modelId) => {
|
|
1011
|
-
if (modelId.startsWith("gpt-5.5")) return [
|
|
1012
|
-
"none",
|
|
1013
|
-
"low",
|
|
1014
|
-
"medium",
|
|
1015
|
-
"high",
|
|
1016
|
-
"xhigh"
|
|
1017
|
-
];
|
|
1018
|
-
if (modelId.startsWith("gpt-5.4-mini")) return [
|
|
1019
|
-
"none",
|
|
1020
|
-
"low",
|
|
1021
|
-
"medium"
|
|
1022
|
-
];
|
|
1023
|
-
if (modelId.startsWith("gpt-5.4") || modelId.startsWith("gpt-5.3-codex")) return [
|
|
1024
|
-
"low",
|
|
1025
|
-
"medium",
|
|
1026
|
-
"high",
|
|
1027
|
-
"xhigh"
|
|
1028
|
-
];
|
|
1029
|
-
if (usesMaxCompletionTokens(modelId)) return [
|
|
1030
|
-
"low",
|
|
1031
|
-
"medium",
|
|
1032
|
-
"high",
|
|
1033
|
-
"xhigh"
|
|
1034
|
-
];
|
|
1035
|
-
return [];
|
|
1036
|
-
};
|
|
1037
1040
|
const sanitizeReasoningEffortForModel = (modelId, reasoningEffort) => {
|
|
1038
1041
|
if (!reasoningEffort) return;
|
|
1039
|
-
return
|
|
1042
|
+
return clampReasoningEffort(modelId, reasoningEffort)?.effort;
|
|
1040
1043
|
};
|
|
1041
1044
|
const normalizeReasoningEffort$1 = (value) => {
|
|
1042
1045
|
switch (value?.toLowerCase()) {
|
|
@@ -1059,36 +1062,46 @@ const normalizeClaudeOpus47Effort = (value) => {
|
|
|
1059
1062
|
default: return;
|
|
1060
1063
|
}
|
|
1061
1064
|
};
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
+
const getEnvValueCaseInsensitive = (env, key) => {
|
|
1066
|
+
const direct = env[key];
|
|
1067
|
+
if (typeof direct === "string") return direct;
|
|
1068
|
+
const lower = key.toLowerCase();
|
|
1069
|
+
return Object.entries(env).find(([k]) => k.toLowerCase() === lower)?.[1];
|
|
1070
|
+
};
|
|
1071
|
+
const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
|
|
1072
|
+
if (client !== "claude") return;
|
|
1073
|
+
return process.env.MODEL_REASONING_EFFORT ?? process.env.COPILOT_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT") ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "COPILOT_REASONING_EFFORT");
|
|
1065
1074
|
};
|
|
1066
|
-
const
|
|
1075
|
+
const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
|
|
1076
|
+
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1077
|
+
const defaultEffort = client === "claude" ? clampReasoningEffort(payload.model, void 0)?.effort : defaultReasoningEffort(payload.model);
|
|
1078
|
+
const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
|
|
1079
|
+
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultEffort;
|
|
1080
|
+
};
|
|
1081
|
+
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
|
|
1067
1082
|
if (!isClaudeOpus47Model$1(payload.model)) return;
|
|
1068
|
-
|
|
1083
|
+
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1084
|
+
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort) ?? (client === "claude" ? "medium" : void 0);
|
|
1069
1085
|
};
|
|
1070
1086
|
const sanitizeUserIdentifier = (user) => {
|
|
1071
1087
|
if (!user) return;
|
|
1072
1088
|
return user.slice(0, MAX_USER_LENGTH);
|
|
1073
1089
|
};
|
|
1074
|
-
const buildRequestPayload = (payload, claudeSettingsEnv) => {
|
|
1075
|
-
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1076
|
-
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv);
|
|
1090
|
+
const buildRequestPayload = (payload, claudeSettingsEnv, client) => {
|
|
1091
|
+
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
|
|
1092
|
+
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv, client);
|
|
1077
1093
|
const reasoningEffort = usesMaxCompletionTokens(payload.model) && payload.tools !== null && payload.tools !== void 0 && payload.tools.length > 0 ? void 0 : requestedReasoningEffort;
|
|
1078
1094
|
if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) {
|
|
1079
|
-
const
|
|
1095
|
+
const shouldAttachReasoningEffort = !isClaudeOpus47Model$1(payload.model);
|
|
1096
|
+
return {
|
|
1080
1097
|
...payload,
|
|
1081
1098
|
output_config: requestedClaudeOpus47Effort ? {
|
|
1082
1099
|
...payload.output_config,
|
|
1083
1100
|
effort: requestedClaudeOpus47Effort
|
|
1084
1101
|
} : payload.output_config,
|
|
1085
|
-
reasoning_effort:
|
|
1102
|
+
reasoning_effort: shouldAttachReasoningEffort ? reasoningEffort : void 0,
|
|
1086
1103
|
user: sanitizeUserIdentifier(payload.user)
|
|
1087
1104
|
};
|
|
1088
|
-
return reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
|
|
1089
|
-
...sanitizedPayload,
|
|
1090
|
-
reasoning_effort: reasoningEffort
|
|
1091
|
-
};
|
|
1092
1105
|
}
|
|
1093
1106
|
return {
|
|
1094
1107
|
...payload,
|
|
@@ -1100,13 +1113,14 @@ const buildRequestPayload = (payload, claudeSettingsEnv) => {
|
|
|
1100
1113
|
};
|
|
1101
1114
|
const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
|
|
1102
1115
|
const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
1103
|
-
const createChatCompletions = async (config, payload) => {
|
|
1116
|
+
const createChatCompletions = async (config, payload, options) => {
|
|
1104
1117
|
const provider = getCopilotProviderContext(config);
|
|
1105
1118
|
const enableVision = messagesIncludeImage(payload.messages);
|
|
1106
1119
|
const initiator = isAgentInitiator(payload.messages);
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1120
|
+
const client = options?.client ?? "generic";
|
|
1121
|
+
const claudeSettingsEnv = client === "claude" ? await getClaudeSettingsEnv() : {};
|
|
1122
|
+
const requestPayload = buildRequestPayload(payload, claudeSettingsEnv, client);
|
|
1123
|
+
if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, client, {
|
|
1110
1124
|
vision: enableVision,
|
|
1111
1125
|
initiator
|
|
1112
1126
|
});
|
|
@@ -1122,18 +1136,21 @@ const createChatCompletions = async (config, payload) => {
|
|
|
1122
1136
|
initiator
|
|
1123
1137
|
});
|
|
1124
1138
|
if (!response.ok) {
|
|
1125
|
-
if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, {
|
|
1139
|
+
if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, client, {
|
|
1126
1140
|
vision: enableVision,
|
|
1127
1141
|
initiator
|
|
1128
1142
|
});
|
|
1129
|
-
|
|
1143
|
+
await logUpstreamError("Failed to create chat completions", response, {
|
|
1144
|
+
model: payload.model,
|
|
1145
|
+
route: "/chat/completions"
|
|
1146
|
+
});
|
|
1130
1147
|
throw new HTTPError("Failed to create chat completions", response);
|
|
1131
1148
|
}
|
|
1132
1149
|
if (payload.stream) return events(response);
|
|
1133
1150
|
return await response.json();
|
|
1134
1151
|
};
|
|
1135
|
-
async function createResponses(provider, payload, claudeSettingsEnv, options) {
|
|
1136
|
-
const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
|
|
1152
|
+
async function createResponses(provider, payload, claudeSettingsEnv, client, options) {
|
|
1153
|
+
const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
|
|
1137
1154
|
const response = await fetchCopilot(provider, "/responses", {
|
|
1138
1155
|
method: "POST",
|
|
1139
1156
|
headers: {
|
|
@@ -1146,12 +1163,25 @@ async function createResponses(provider, payload, claudeSettingsEnv, options) {
|
|
|
1146
1163
|
initiator: options.initiator
|
|
1147
1164
|
});
|
|
1148
1165
|
if (!response.ok) {
|
|
1149
|
-
|
|
1166
|
+
await logUpstreamError("Failed to create responses", response, {
|
|
1167
|
+
model: payload.model,
|
|
1168
|
+
route: "/responses"
|
|
1169
|
+
});
|
|
1150
1170
|
throw new HTTPError("Failed to create responses", response);
|
|
1151
1171
|
}
|
|
1152
1172
|
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1153
1173
|
return translateResponsesToChatCompletion(await response.json());
|
|
1154
1174
|
}
|
|
1175
|
+
async function logUpstreamError(message, response, context) {
|
|
1176
|
+
const errorBody = await response.clone().text().catch(() => "");
|
|
1177
|
+
consola.error(message, {
|
|
1178
|
+
route: context.route,
|
|
1179
|
+
model: context.model,
|
|
1180
|
+
status: response.status,
|
|
1181
|
+
statusText: response.statusText,
|
|
1182
|
+
body: errorBody || void 0
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1155
1185
|
async function shouldRetryWithResponses(response) {
|
|
1156
1186
|
try {
|
|
1157
1187
|
return (await response.clone().json()).error?.code === "unsupported_api_for_model";
|
|
@@ -1168,7 +1198,7 @@ chatCompletionRoutes.post("/", async (c) => {
|
|
|
1168
1198
|
try {
|
|
1169
1199
|
await checkRateLimit();
|
|
1170
1200
|
const payload = await c.req.json();
|
|
1171
|
-
const response = await createChatCompletions(c.var.config, payload);
|
|
1201
|
+
const response = await createChatCompletions(c.var.config, payload, { client: "generic" });
|
|
1172
1202
|
if (isNonStreaming(response)) return c.json(response);
|
|
1173
1203
|
return streamSSE(c, async (stream) => {
|
|
1174
1204
|
for await (const chunk of response) await stream.writeSSE(chunk);
|
|
@@ -1232,14 +1262,22 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
1232
1262
|
const normalizeModelId = (modelId) => modelId.trim().toLowerCase().replaceAll(/[\s._-]+/g, "");
|
|
1233
1263
|
const stripSnapshotSuffix = (modelId) => {
|
|
1234
1264
|
const id = modelId.trim().toLowerCase().replace(/\[1m\]$/, "");
|
|
1235
|
-
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1265
|
+
const claudeSnapshotMatch = id.match(/^claude-(opus|sonnet|haiku)-(\d)-(\d)(-1m)?(?:-\d{8})?$/);
|
|
1266
|
+
if (claudeSnapshotMatch) {
|
|
1267
|
+
const [, family, major, minor, contextWindow = ""] = claudeSnapshotMatch;
|
|
1268
|
+
return `claude-${family}-${major}.${minor}${contextWindow}`;
|
|
1269
|
+
}
|
|
1238
1270
|
return id;
|
|
1239
1271
|
};
|
|
1240
1272
|
const getAliasCandidates = (modelId) => {
|
|
1241
1273
|
const canonicalModelId = stripSnapshotSuffix(modelId);
|
|
1242
1274
|
const aliases = new Set([canonicalModelId]);
|
|
1275
|
+
const canonicalClaudeMatch = canonicalModelId.match(/^claude-(opus|sonnet|haiku)-(\d+)\.(\d+)(-1m)?$/);
|
|
1276
|
+
if (canonicalClaudeMatch) {
|
|
1277
|
+
const [, family, major, minor, contextWindow = ""] = canonicalClaudeMatch;
|
|
1278
|
+
aliases.add(`claude-${family}-${major}-${minor}${contextWindow}`);
|
|
1279
|
+
return [...aliases];
|
|
1280
|
+
}
|
|
1243
1281
|
const familyMatch = canonicalModelId.match(/^[a-z]+(?:-[a-z]+)*-\d+(?:\.\d+)?/);
|
|
1244
1282
|
if (familyMatch) {
|
|
1245
1283
|
aliases.add(familyMatch[0]);
|
|
@@ -1303,13 +1341,36 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
1303
1341
|
//#endregion
|
|
1304
1342
|
//#region src/bridges/claude/non-stream-translation.ts
|
|
1305
1343
|
const normalizeClaudeModelAlias = (model) => {
|
|
1306
|
-
const
|
|
1344
|
+
const trimmed = model.trim().toLowerCase();
|
|
1345
|
+
if (trimmed === "opus[1m]") return "claude-opus-4.6-1m";
|
|
1346
|
+
const normalized = trimmed.replace(/\[1m\]$/, "");
|
|
1347
|
+
if (normalized === "opus") return "claude-opus";
|
|
1348
|
+
if (normalized === "sonnet") return "claude-sonnet";
|
|
1349
|
+
if (normalized === "haiku") return "claude-haiku";
|
|
1307
1350
|
if (/^claude-(opus|sonnet|haiku)-\d-\d(?:-1m)?$/.test(normalized)) return normalized.replace(/^claude-(opus|sonnet|haiku)-(\d)-(\d)(-1m)?$/, "claude-$1-$2.$3$4");
|
|
1308
1351
|
if (/^claude-opus-4-7-\d{8}$/.test(normalized)) return "claude-opus-4.7";
|
|
1309
1352
|
return normalized;
|
|
1310
1353
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1354
|
+
const getConfiguredClaudeDefaultModel = (settings) => {
|
|
1355
|
+
if (!settings) return;
|
|
1356
|
+
const env = settings.env ?? {};
|
|
1357
|
+
const topLevelModel = settings.model?.trim();
|
|
1358
|
+
const anthropicModel = env.ANTHROPIC_MODEL;
|
|
1359
|
+
if (!topLevelModel) return anthropicModel;
|
|
1360
|
+
switch (topLevelModel.toLowerCase()) {
|
|
1361
|
+
case "opus[1m]": return env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? topLevelModel;
|
|
1362
|
+
case "opus": return env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? topLevelModel;
|
|
1363
|
+
case "sonnet": return env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? topLevelModel;
|
|
1364
|
+
case "haiku": return env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? env.ANTHROPIC_SMALL_FAST_MODEL ?? topLevelModel;
|
|
1365
|
+
default: return topLevelModel;
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const resolveClaudeRequestedModel = (model, settings) => {
|
|
1369
|
+
if (model.trim().toLowerCase() !== "definitely-not-a-real-model") return model;
|
|
1370
|
+
return getConfiguredClaudeDefaultModel(settings) ?? model;
|
|
1371
|
+
};
|
|
1372
|
+
function translateModelName(model, settings) {
|
|
1373
|
+
return resolveUpstreamModelId(normalizeClaudeModelAlias(resolveClaudeRequestedModel(model, settings)));
|
|
1313
1374
|
}
|
|
1314
1375
|
function isClaudeModel(modelId) {
|
|
1315
1376
|
return modelId.startsWith("claude-");
|
|
@@ -1338,13 +1399,13 @@ function getClaudeOpus47Effort(payload) {
|
|
|
1338
1399
|
if (budgetTokens <= 24576) return "high";
|
|
1339
1400
|
return "xhigh";
|
|
1340
1401
|
}
|
|
1341
|
-
function translateThinking(payload) {
|
|
1342
|
-
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1402
|
+
function translateThinking(payload, settings) {
|
|
1403
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
|
|
1343
1404
|
if (payload.thinking?.type === "adaptive") return { type: "adaptive" };
|
|
1344
1405
|
return payload.thinking?.type === "enabled" ? { type: "adaptive" } : void 0;
|
|
1345
1406
|
}
|
|
1346
|
-
function translateOutputConfig(payload) {
|
|
1347
|
-
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1407
|
+
function translateOutputConfig(payload, settings) {
|
|
1408
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
|
|
1348
1409
|
const effort = getClaudeOpus47Effort(payload);
|
|
1349
1410
|
return effort ? { effort } : void 0;
|
|
1350
1411
|
}
|
|
@@ -1359,8 +1420,8 @@ function normalizeReasoningEffort(value) {
|
|
|
1359
1420
|
default: return;
|
|
1360
1421
|
}
|
|
1361
1422
|
}
|
|
1362
|
-
function translateReasoningEffort(payload) {
|
|
1363
|
-
const modelId = translateModelName(payload.model);
|
|
1423
|
+
function translateReasoningEffort(payload, settings) {
|
|
1424
|
+
const modelId = translateModelName(payload.model, settings);
|
|
1364
1425
|
if (isClaudeModel(modelId)) return;
|
|
1365
1426
|
if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
|
|
1366
1427
|
if (payload.thinking?.type !== "enabled") return;
|
|
@@ -1371,18 +1432,18 @@ function translateReasoningEffort(payload) {
|
|
|
1371
1432
|
if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
|
|
1372
1433
|
return sanitizeReasoningEffortForModel(modelId, "xhigh");
|
|
1373
1434
|
}
|
|
1374
|
-
function translateToOpenAI(payload) {
|
|
1435
|
+
function translateToOpenAI(payload, settings) {
|
|
1375
1436
|
return {
|
|
1376
|
-
model: translateModelName(payload.model),
|
|
1437
|
+
model: translateModelName(payload.model, settings),
|
|
1377
1438
|
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1378
1439
|
max_tokens: payload.max_tokens,
|
|
1379
1440
|
stop: payload.stop_sequences,
|
|
1380
1441
|
stream: payload.stream,
|
|
1381
1442
|
temperature: payload.temperature,
|
|
1382
1443
|
top_p: payload.top_p,
|
|
1383
|
-
thinking: translateThinking(payload),
|
|
1384
|
-
output_config: translateOutputConfig(payload),
|
|
1385
|
-
reasoning_effort: translateReasoningEffort(payload),
|
|
1444
|
+
thinking: translateThinking(payload, settings),
|
|
1445
|
+
output_config: translateOutputConfig(payload, settings),
|
|
1446
|
+
reasoning_effort: translateReasoningEffort(payload, settings),
|
|
1386
1447
|
user: payload.metadata?.user_id,
|
|
1387
1448
|
tools: translateAnthropicToolsToOpenAI(payload.tools),
|
|
1388
1449
|
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
@@ -1855,9 +1916,9 @@ messageRoutes.post("/", async (c) => {
|
|
|
1855
1916
|
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1856
1917
|
throw error;
|
|
1857
1918
|
}
|
|
1858
|
-
const openAIPayload = translateToOpenAI(await c.req.json());
|
|
1919
|
+
const openAIPayload = translateToOpenAI(await c.req.json(), await getClaudeSettings());
|
|
1859
1920
|
try {
|
|
1860
|
-
const response = await createChatCompletions(config, openAIPayload);
|
|
1921
|
+
const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
|
|
1861
1922
|
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
|
|
1862
1923
|
return streamSSE(c, async (stream) => {
|
|
1863
1924
|
const streamState = {
|
|
@@ -1910,7 +1971,7 @@ messageRoutes.post("/count_tokens", async (c) => {
|
|
|
1910
1971
|
try {
|
|
1911
1972
|
const anthropicBeta = c.req.header("anthropic-beta");
|
|
1912
1973
|
const anthropicPayload = await c.req.json();
|
|
1913
|
-
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1974
|
+
const openAIPayload = translateToOpenAI(anthropicPayload, await getClaudeSettings());
|
|
1914
1975
|
const selectedModel = resolveModel(openAIPayload.model);
|
|
1915
1976
|
if (!selectedModel) {
|
|
1916
1977
|
consola.warn(`Model ${openAIPayload.model} not found in registry, returning fallback token count`);
|
|
@@ -2294,6 +2355,15 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2294
2355
|
...incoming ?? {},
|
|
2295
2356
|
effort: clamped.effort
|
|
2296
2357
|
};
|
|
2358
|
+
const text = isPlainObject(next.text) ? next.text : void 0;
|
|
2359
|
+
if (text && capability.textVerbosity && "verbosity" in text) {
|
|
2360
|
+
const { supported, default: defaultVerbosity } = capability.textVerbosity;
|
|
2361
|
+
const verbosity = text.verbosity;
|
|
2362
|
+
next.text = {
|
|
2363
|
+
...text,
|
|
2364
|
+
verbosity: isTextVerbosity(verbosity) && supported.includes(verbosity) ? verbosity : defaultVerbosity
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2297
2367
|
return next;
|
|
2298
2368
|
};
|
|
2299
2369
|
|
|
@@ -2482,24 +2552,29 @@ const start = defineCommand({
|
|
|
2482
2552
|
default: false,
|
|
2483
2553
|
description: "Print GitHub and Copilot tokens during startup."
|
|
2484
2554
|
},
|
|
2485
|
-
"
|
|
2555
|
+
"codex-setup": {
|
|
2486
2556
|
type: "boolean",
|
|
2487
|
-
default:
|
|
2557
|
+
default: true,
|
|
2488
2558
|
description: "Skip writing the bridge provider into ~/.codex/config.toml."
|
|
2489
2559
|
},
|
|
2490
|
-
"
|
|
2560
|
+
"claude-setup": {
|
|
2491
2561
|
type: "boolean",
|
|
2492
|
-
default:
|
|
2562
|
+
default: true,
|
|
2493
2563
|
description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
|
|
2494
2564
|
},
|
|
2565
|
+
"claude-code": {
|
|
2566
|
+
type: "boolean",
|
|
2567
|
+
default: false,
|
|
2568
|
+
description: "Print a one-shot `export ... && claude` command and do not modify user config files."
|
|
2569
|
+
},
|
|
2495
2570
|
"select-model": {
|
|
2496
2571
|
type: "boolean",
|
|
2497
2572
|
default: false,
|
|
2498
2573
|
description: "Force the model picker even when ~/.codex/config.toml already has a model."
|
|
2499
2574
|
},
|
|
2500
|
-
|
|
2575
|
+
prompt: {
|
|
2501
2576
|
type: "boolean",
|
|
2502
|
-
default:
|
|
2577
|
+
default: true,
|
|
2503
2578
|
description: "Never prompt; use the existing model from ~/.codex/config.toml as-is."
|
|
2504
2579
|
},
|
|
2505
2580
|
"rate-limit": {
|
|
@@ -2538,7 +2613,9 @@ const start = defineCommand({
|
|
|
2538
2613
|
consola.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2539
2614
|
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2540
2615
|
const codexConfigPath = CODEX_DEFAULTS.configPath;
|
|
2541
|
-
|
|
2616
|
+
const isClaudeCodeMode = Boolean(args["claude-code"]);
|
|
2617
|
+
if (isClaudeCodeMode) consola.info("--claude-code mode: not modifying ~/.claude/settings.json or ~/.codex/config.toml");
|
|
2618
|
+
if (!isClaudeCodeMode && args["claude-setup"]) try {
|
|
2542
2619
|
const claudeResult = await applyClaudeConfig({
|
|
2543
2620
|
baseUrl,
|
|
2544
2621
|
configPath: claudeConfigPath
|
|
@@ -2553,7 +2630,8 @@ const start = defineCommand({
|
|
|
2553
2630
|
const codexUserConfig = await readCodexUserConfigFromDisk(codexConfigPath);
|
|
2554
2631
|
let chosenModel = codexUserConfig.model;
|
|
2555
2632
|
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2556
|
-
if (!
|
|
2633
|
+
if (chosenModel && !chosenEffort) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
|
|
2634
|
+
if (!isClaudeCodeMode && args["codex-setup"]) try {
|
|
2557
2635
|
const result = await applyCodexConfig({
|
|
2558
2636
|
baseUrl: `${baseUrl}/v1`,
|
|
2559
2637
|
configPath: codexConfigPath,
|
|
@@ -2580,7 +2658,7 @@ const start = defineCommand({
|
|
|
2580
2658
|
``,
|
|
2581
2659
|
` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
|
|
2582
2660
|
].join("\n"));
|
|
2583
|
-
if (!args
|
|
2661
|
+
if (!isClaudeCodeMode && args.prompt && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
|
|
2584
2662
|
const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
|
|
2585
2663
|
const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
|
|
2586
2664
|
type: "select",
|
|
@@ -2591,7 +2669,7 @@ const start = defineCommand({
|
|
|
2591
2669
|
chosenModel = selected;
|
|
2592
2670
|
const supported = getModelCapability(selected)?.reasoning?.supported;
|
|
2593
2671
|
if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
|
|
2594
|
-
if (
|
|
2672
|
+
if (args["codex-setup"]) try {
|
|
2595
2673
|
const result = await applyCodexConfig({
|
|
2596
2674
|
baseUrl: `${baseUrl}/v1`,
|
|
2597
2675
|
configPath: codexConfigPath,
|
|
@@ -2605,6 +2683,26 @@ const start = defineCommand({
|
|
|
2605
2683
|
}
|
|
2606
2684
|
}
|
|
2607
2685
|
}
|
|
2686
|
+
if (isClaudeCodeMode) {
|
|
2687
|
+
const defaults = pickClaudeLaunchDefaults(finalPickable, chosenModel);
|
|
2688
|
+
let selectedModel = defaults.model;
|
|
2689
|
+
if (args.prompt && finalPickable.length > 0) selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
2690
|
+
type: "select",
|
|
2691
|
+
options: finalPickable,
|
|
2692
|
+
initial: defaults.model
|
|
2693
|
+
});
|
|
2694
|
+
const command = buildClaudeLaunchCommand({
|
|
2695
|
+
baseUrl,
|
|
2696
|
+
model: selectedModel
|
|
2697
|
+
});
|
|
2698
|
+
consola.box([
|
|
2699
|
+
"Claude one-shot command",
|
|
2700
|
+
"",
|
|
2701
|
+
` ${command}`,
|
|
2702
|
+
"",
|
|
2703
|
+
" The bridge verified this path with explicit --model; current Claude CLI did not reliably honor ANTHROPIC_MODEL env defaults."
|
|
2704
|
+
].join("\n"));
|
|
2705
|
+
}
|
|
2608
2706
|
await new Promise((resolve, reject) => {
|
|
2609
2707
|
server.on("close", resolve);
|
|
2610
2708
|
server.on("error", reject);
|