betahi-copilot-bridge 0.1.3 → 0.1.5
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 +29 -24
- package/dist/main.js +165 -80
- 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://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
10
|
> [!WARNING]
|
|
11
11
|
> This is a reverse-engineered bridge for the GitHub Copilot API. It is not
|
|
@@ -123,7 +123,20 @@ Bridge endpoints: `POST /v1/messages`, `POST /v1/messages/count_tokens`.
|
|
|
123
123
|
or port. Pass `--no-claude-setup` to skip this. All other keys in your
|
|
124
124
|
settings file (model overrides, plugins, marketplaces) are preserved.
|
|
125
125
|
|
|
126
|
-
|
|
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:
|
|
127
140
|
|
|
128
141
|
```json
|
|
129
142
|
{
|
|
@@ -134,19 +147,14 @@ If you prefer to wire it yourself:
|
|
|
134
147
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
|
135
148
|
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4.5",
|
|
136
149
|
"MODEL_REASONING_EFFORT": "medium"
|
|
137
|
-
}
|
|
138
|
-
"model": "claude-opus-4.7"
|
|
150
|
+
}
|
|
139
151
|
}
|
|
140
152
|
```
|
|
141
153
|
|
|
142
154
|
`MODEL_REASONING_EFFORT` (case-insensitive key lookup) is also read from the
|
|
143
155
|
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).
|
|
156
|
+
applied to Claude requests only when the model supports reasoning. If it is not
|
|
157
|
+
configured, Claude requests do not infer or attach a reasoning effort.
|
|
150
158
|
|
|
151
159
|
## Environment overrides
|
|
152
160
|
|
|
@@ -156,8 +164,7 @@ If Claude-side reasoning is not configured, Claude requests fall back to
|
|
|
156
164
|
| `COPILOT_ACCOUNT_TYPE` | `individual` \| `business` \| `enterprise`. |
|
|
157
165
|
| `COPILOT_BASE_URL` | Override the upstream Copilot base URL. |
|
|
158
166
|
| `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
|
|
159
|
-
| `MODEL_REASONING_EFFORT` | Claude-side
|
|
160
|
-
| `COPILOT_REASONING_EFFORT` | Alias of `MODEL_REASONING_EFFORT` (backward compatible). |
|
|
167
|
+
| `MODEL_REASONING_EFFORT` | Claude-side reasoning effort override. |
|
|
161
168
|
|
|
162
169
|
|
|
163
170
|
|
|
@@ -205,13 +212,11 @@ accepts upstream.
|
|
|
205
212
|
|
|
206
213
|
### Reasoning effort
|
|
207
214
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`~/.claude/settings.json`); per-request `reasoning_effort` (or Anthropic
|
|
214
|
-
`thinking.budget_tokens`) takes precedence.
|
|
215
|
+
For OpenAI-compatible clients, unsupported reasoning values are clamped to the
|
|
216
|
+
model capability table instead of being forwarded upstream. Claude-side
|
|
217
|
+
reasoning can be set globally via `MODEL_REASONING_EFFORT` (env, or `env` in
|
|
218
|
+
`~/.claude/settings.json`); invalid Claude-side values are ignored, and
|
|
219
|
+
per-request `reasoning_effort` takes precedence.
|
|
215
220
|
|
|
216
221
|
## Development
|
|
217
222
|
|
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];
|
|
@@ -778,7 +808,7 @@ var RateLimitError = class extends Error {
|
|
|
778
808
|
|
|
779
809
|
//#endregion
|
|
780
810
|
//#region src/services/copilot/responses.ts
|
|
781
|
-
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.
|
|
811
|
+
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.5|gpt-5\.4-mini|gpt-5\.3-codex|gpt-5\.2-codex)(?:-|$)/i;
|
|
782
812
|
function shouldUseResponsesApiForModel(model) {
|
|
783
813
|
return RESPONSES_ONLY_MODEL_PATTERN.test(model);
|
|
784
814
|
}
|
|
@@ -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()) {
|
|
@@ -1067,18 +1070,18 @@ const getEnvValueCaseInsensitive = (env, key) => {
|
|
|
1067
1070
|
};
|
|
1068
1071
|
const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
|
|
1069
1072
|
if (client !== "claude") return;
|
|
1070
|
-
return process.env.MODEL_REASONING_EFFORT ??
|
|
1073
|
+
return process.env.MODEL_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT");
|
|
1071
1074
|
};
|
|
1072
1075
|
const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
|
|
1073
1076
|
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1074
|
-
const defaultEffort = client === "claude" ?
|
|
1077
|
+
const defaultEffort = client === "claude" ? void 0 : defaultReasoningEffort(payload.model);
|
|
1075
1078
|
const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
|
|
1076
1079
|
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultEffort;
|
|
1077
1080
|
};
|
|
1078
1081
|
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
|
|
1079
1082
|
if (!isClaudeOpus47Model$1(payload.model)) return;
|
|
1080
1083
|
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1081
|
-
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort)
|
|
1084
|
+
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort);
|
|
1082
1085
|
};
|
|
1083
1086
|
const sanitizeUserIdentifier = (user) => {
|
|
1084
1087
|
if (!user) return;
|
|
@@ -1090,19 +1093,15 @@ const buildRequestPayload = (payload, claudeSettingsEnv, client) => {
|
|
|
1090
1093
|
const reasoningEffort = usesMaxCompletionTokens(payload.model) && payload.tools !== null && payload.tools !== void 0 && payload.tools.length > 0 ? void 0 : requestedReasoningEffort;
|
|
1091
1094
|
if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) {
|
|
1092
1095
|
const shouldAttachReasoningEffort = !isClaudeOpus47Model$1(payload.model);
|
|
1093
|
-
|
|
1096
|
+
return {
|
|
1094
1097
|
...payload,
|
|
1095
1098
|
output_config: requestedClaudeOpus47Effort ? {
|
|
1096
1099
|
...payload.output_config,
|
|
1097
1100
|
effort: requestedClaudeOpus47Effort
|
|
1098
1101
|
} : payload.output_config,
|
|
1099
|
-
reasoning_effort: shouldAttachReasoningEffort ?
|
|
1102
|
+
reasoning_effort: shouldAttachReasoningEffort ? reasoningEffort : void 0,
|
|
1100
1103
|
user: sanitizeUserIdentifier(payload.user)
|
|
1101
1104
|
};
|
|
1102
|
-
return !shouldAttachReasoningEffort || reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
|
|
1103
|
-
...sanitizedPayload,
|
|
1104
|
-
reasoning_effort: reasoningEffort
|
|
1105
|
-
};
|
|
1106
1105
|
}
|
|
1107
1106
|
return {
|
|
1108
1107
|
...payload,
|
|
@@ -1141,7 +1140,10 @@ const createChatCompletions = async (config, payload, options) => {
|
|
|
1141
1140
|
vision: enableVision,
|
|
1142
1141
|
initiator
|
|
1143
1142
|
});
|
|
1144
|
-
|
|
1143
|
+
await logUpstreamError("Failed to create chat completions", response, {
|
|
1144
|
+
model: payload.model,
|
|
1145
|
+
route: "/chat/completions"
|
|
1146
|
+
});
|
|
1145
1147
|
throw new HTTPError("Failed to create chat completions", response);
|
|
1146
1148
|
}
|
|
1147
1149
|
if (payload.stream) return events(response);
|
|
@@ -1161,12 +1163,25 @@ async function createResponses(provider, payload, claudeSettingsEnv, client, opt
|
|
|
1161
1163
|
initiator: options.initiator
|
|
1162
1164
|
});
|
|
1163
1165
|
if (!response.ok) {
|
|
1164
|
-
|
|
1166
|
+
await logUpstreamError("Failed to create responses", response, {
|
|
1167
|
+
model: payload.model,
|
|
1168
|
+
route: "/responses"
|
|
1169
|
+
});
|
|
1165
1170
|
throw new HTTPError("Failed to create responses", response);
|
|
1166
1171
|
}
|
|
1167
1172
|
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1168
1173
|
return translateResponsesToChatCompletion(await response.json());
|
|
1169
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
|
+
}
|
|
1170
1185
|
async function shouldRetryWithResponses(response) {
|
|
1171
1186
|
try {
|
|
1172
1187
|
return (await response.clone().json()).error?.code === "unsupported_api_for_model";
|
|
@@ -1247,14 +1262,22 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
1247
1262
|
const normalizeModelId = (modelId) => modelId.trim().toLowerCase().replaceAll(/[\s._-]+/g, "");
|
|
1248
1263
|
const stripSnapshotSuffix = (modelId) => {
|
|
1249
1264
|
const id = modelId.trim().toLowerCase().replace(/\[1m\]$/, "");
|
|
1250
|
-
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
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
|
+
}
|
|
1253
1270
|
return id;
|
|
1254
1271
|
};
|
|
1255
1272
|
const getAliasCandidates = (modelId) => {
|
|
1256
1273
|
const canonicalModelId = stripSnapshotSuffix(modelId);
|
|
1257
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
|
+
}
|
|
1258
1281
|
const familyMatch = canonicalModelId.match(/^[a-z]+(?:-[a-z]+)*-\d+(?:\.\d+)?/);
|
|
1259
1282
|
if (familyMatch) {
|
|
1260
1283
|
aliases.add(familyMatch[0]);
|
|
@@ -1318,13 +1341,36 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
1318
1341
|
//#endregion
|
|
1319
1342
|
//#region src/bridges/claude/non-stream-translation.ts
|
|
1320
1343
|
const normalizeClaudeModelAlias = (model) => {
|
|
1321
|
-
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";
|
|
1322
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");
|
|
1323
1351
|
if (/^claude-opus-4-7-\d{8}$/.test(normalized)) return "claude-opus-4.7";
|
|
1324
1352
|
return normalized;
|
|
1325
1353
|
};
|
|
1326
|
-
|
|
1327
|
-
|
|
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)));
|
|
1328
1374
|
}
|
|
1329
1375
|
function isClaudeModel(modelId) {
|
|
1330
1376
|
return modelId.startsWith("claude-");
|
|
@@ -1353,13 +1399,13 @@ function getClaudeOpus47Effort(payload) {
|
|
|
1353
1399
|
if (budgetTokens <= 24576) return "high";
|
|
1354
1400
|
return "xhigh";
|
|
1355
1401
|
}
|
|
1356
|
-
function translateThinking(payload) {
|
|
1357
|
-
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1402
|
+
function translateThinking(payload, settings) {
|
|
1403
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
|
|
1358
1404
|
if (payload.thinking?.type === "adaptive") return { type: "adaptive" };
|
|
1359
1405
|
return payload.thinking?.type === "enabled" ? { type: "adaptive" } : void 0;
|
|
1360
1406
|
}
|
|
1361
|
-
function translateOutputConfig(payload) {
|
|
1362
|
-
if (!isClaudeOpus47Model(translateModelName(payload.model))) return;
|
|
1407
|
+
function translateOutputConfig(payload, settings) {
|
|
1408
|
+
if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
|
|
1363
1409
|
const effort = getClaudeOpus47Effort(payload);
|
|
1364
1410
|
return effort ? { effort } : void 0;
|
|
1365
1411
|
}
|
|
@@ -1374,10 +1420,11 @@ function normalizeReasoningEffort(value) {
|
|
|
1374
1420
|
default: return;
|
|
1375
1421
|
}
|
|
1376
1422
|
}
|
|
1377
|
-
function translateReasoningEffort(payload) {
|
|
1378
|
-
const modelId = translateModelName(payload.model);
|
|
1379
|
-
if (
|
|
1423
|
+
function translateReasoningEffort(payload, settings) {
|
|
1424
|
+
const modelId = translateModelName(payload.model, settings);
|
|
1425
|
+
if (isClaudeOpus47Model(modelId)) return;
|
|
1380
1426
|
if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
|
|
1427
|
+
if (isClaudeModel(modelId)) return;
|
|
1381
1428
|
if (payload.thinking?.type !== "enabled") return;
|
|
1382
1429
|
const budgetTokens = payload.thinking.budget_tokens;
|
|
1383
1430
|
if (budgetTokens === void 0) return sanitizeReasoningEffortForModel(modelId, "medium");
|
|
@@ -1386,21 +1433,23 @@ function translateReasoningEffort(payload) {
|
|
|
1386
1433
|
if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
|
|
1387
1434
|
return sanitizeReasoningEffortForModel(modelId, "xhigh");
|
|
1388
1435
|
}
|
|
1389
|
-
function translateToOpenAI(payload) {
|
|
1436
|
+
function translateToOpenAI(payload, settings) {
|
|
1437
|
+
const model = translateModelName(payload.model, settings);
|
|
1438
|
+
const tools = translateAnthropicToolsToOpenAI(payload.tools);
|
|
1390
1439
|
return {
|
|
1391
|
-
model
|
|
1440
|
+
model,
|
|
1392
1441
|
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1393
1442
|
max_tokens: payload.max_tokens,
|
|
1394
1443
|
stop: payload.stop_sequences,
|
|
1395
1444
|
stream: payload.stream,
|
|
1396
1445
|
temperature: payload.temperature,
|
|
1397
1446
|
top_p: payload.top_p,
|
|
1398
|
-
thinking: translateThinking(payload),
|
|
1399
|
-
output_config: translateOutputConfig(payload),
|
|
1400
|
-
reasoning_effort: translateReasoningEffort(payload),
|
|
1447
|
+
thinking: translateThinking(payload, settings),
|
|
1448
|
+
output_config: translateOutputConfig(payload, settings),
|
|
1449
|
+
reasoning_effort: translateReasoningEffort(payload, settings),
|
|
1401
1450
|
user: payload.metadata?.user_id,
|
|
1402
|
-
tools
|
|
1403
|
-
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1451
|
+
tools,
|
|
1452
|
+
tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice) : void 0
|
|
1404
1453
|
};
|
|
1405
1454
|
}
|
|
1406
1455
|
function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
|
|
@@ -1492,7 +1541,7 @@ function mapContent(content) {
|
|
|
1492
1541
|
return contentParts;
|
|
1493
1542
|
}
|
|
1494
1543
|
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1495
|
-
if (!anthropicTools) return;
|
|
1544
|
+
if (!anthropicTools || anthropicTools.length === 0) return;
|
|
1496
1545
|
return anthropicTools.map((tool) => ({
|
|
1497
1546
|
type: "function",
|
|
1498
1547
|
function: {
|
|
@@ -1870,7 +1919,7 @@ messageRoutes.post("/", async (c) => {
|
|
|
1870
1919
|
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1871
1920
|
throw error;
|
|
1872
1921
|
}
|
|
1873
|
-
const openAIPayload = translateToOpenAI(await c.req.json());
|
|
1922
|
+
const openAIPayload = translateToOpenAI(await c.req.json(), await getClaudeSettings());
|
|
1874
1923
|
try {
|
|
1875
1924
|
const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
|
|
1876
1925
|
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
|
|
@@ -1925,7 +1974,7 @@ messageRoutes.post("/count_tokens", async (c) => {
|
|
|
1925
1974
|
try {
|
|
1926
1975
|
const anthropicBeta = c.req.header("anthropic-beta");
|
|
1927
1976
|
const anthropicPayload = await c.req.json();
|
|
1928
|
-
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1977
|
+
const openAIPayload = translateToOpenAI(anthropicPayload, await getClaudeSettings());
|
|
1929
1978
|
const selectedModel = resolveModel(openAIPayload.model);
|
|
1930
1979
|
if (!selectedModel) {
|
|
1931
1980
|
consola.warn(`Model ${openAIPayload.model} not found in registry, returning fallback token count`);
|
|
@@ -2309,6 +2358,15 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2309
2358
|
...incoming ?? {},
|
|
2310
2359
|
effort: clamped.effort
|
|
2311
2360
|
};
|
|
2361
|
+
const text = isPlainObject(next.text) ? next.text : void 0;
|
|
2362
|
+
if (text && capability.textVerbosity && "verbosity" in text) {
|
|
2363
|
+
const { supported, default: defaultVerbosity } = capability.textVerbosity;
|
|
2364
|
+
const verbosity = text.verbosity;
|
|
2365
|
+
next.text = {
|
|
2366
|
+
...text,
|
|
2367
|
+
verbosity: isTextVerbosity(verbosity) && supported.includes(verbosity) ? verbosity : defaultVerbosity
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2312
2370
|
return next;
|
|
2313
2371
|
};
|
|
2314
2372
|
|
|
@@ -2497,24 +2555,29 @@ const start = defineCommand({
|
|
|
2497
2555
|
default: false,
|
|
2498
2556
|
description: "Print GitHub and Copilot tokens during startup."
|
|
2499
2557
|
},
|
|
2500
|
-
"
|
|
2558
|
+
"codex-setup": {
|
|
2501
2559
|
type: "boolean",
|
|
2502
|
-
default:
|
|
2560
|
+
default: true,
|
|
2503
2561
|
description: "Skip writing the bridge provider into ~/.codex/config.toml."
|
|
2504
2562
|
},
|
|
2505
|
-
"
|
|
2563
|
+
"claude-setup": {
|
|
2506
2564
|
type: "boolean",
|
|
2507
|
-
default:
|
|
2565
|
+
default: true,
|
|
2508
2566
|
description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
|
|
2509
2567
|
},
|
|
2568
|
+
"claude-code": {
|
|
2569
|
+
type: "boolean",
|
|
2570
|
+
default: false,
|
|
2571
|
+
description: "Print a one-shot `export ... && claude` command and do not modify user config files."
|
|
2572
|
+
},
|
|
2510
2573
|
"select-model": {
|
|
2511
2574
|
type: "boolean",
|
|
2512
2575
|
default: false,
|
|
2513
2576
|
description: "Force the model picker even when ~/.codex/config.toml already has a model."
|
|
2514
2577
|
},
|
|
2515
|
-
|
|
2578
|
+
prompt: {
|
|
2516
2579
|
type: "boolean",
|
|
2517
|
-
default:
|
|
2580
|
+
default: true,
|
|
2518
2581
|
description: "Never prompt; use the existing model from ~/.codex/config.toml as-is."
|
|
2519
2582
|
},
|
|
2520
2583
|
"rate-limit": {
|
|
@@ -2553,7 +2616,9 @@ const start = defineCommand({
|
|
|
2553
2616
|
consola.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2554
2617
|
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2555
2618
|
const codexConfigPath = CODEX_DEFAULTS.configPath;
|
|
2556
|
-
|
|
2619
|
+
const isClaudeCodeMode = Boolean(args["claude-code"]);
|
|
2620
|
+
if (isClaudeCodeMode) consola.info("--claude-code mode: not modifying ~/.claude/settings.json or ~/.codex/config.toml");
|
|
2621
|
+
if (!isClaudeCodeMode && args["claude-setup"]) try {
|
|
2557
2622
|
const claudeResult = await applyClaudeConfig({
|
|
2558
2623
|
baseUrl,
|
|
2559
2624
|
configPath: claudeConfigPath
|
|
@@ -2569,7 +2634,7 @@ const start = defineCommand({
|
|
|
2569
2634
|
let chosenModel = codexUserConfig.model;
|
|
2570
2635
|
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2571
2636
|
if (chosenModel && !chosenEffort) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
|
|
2572
|
-
if (!args["
|
|
2637
|
+
if (!isClaudeCodeMode && args["codex-setup"]) try {
|
|
2573
2638
|
const result = await applyCodexConfig({
|
|
2574
2639
|
baseUrl: `${baseUrl}/v1`,
|
|
2575
2640
|
configPath: codexConfigPath,
|
|
@@ -2596,7 +2661,7 @@ const start = defineCommand({
|
|
|
2596
2661
|
``,
|
|
2597
2662
|
` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
|
|
2598
2663
|
].join("\n"));
|
|
2599
|
-
if (!args
|
|
2664
|
+
if (!isClaudeCodeMode && args.prompt && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
|
|
2600
2665
|
const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
|
|
2601
2666
|
const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
|
|
2602
2667
|
type: "select",
|
|
@@ -2607,7 +2672,7 @@ const start = defineCommand({
|
|
|
2607
2672
|
chosenModel = selected;
|
|
2608
2673
|
const supported = getModelCapability(selected)?.reasoning?.supported;
|
|
2609
2674
|
if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
|
|
2610
|
-
if (
|
|
2675
|
+
if (args["codex-setup"]) try {
|
|
2611
2676
|
const result = await applyCodexConfig({
|
|
2612
2677
|
baseUrl: `${baseUrl}/v1`,
|
|
2613
2678
|
configPath: codexConfigPath,
|
|
@@ -2621,6 +2686,26 @@ const start = defineCommand({
|
|
|
2621
2686
|
}
|
|
2622
2687
|
}
|
|
2623
2688
|
}
|
|
2689
|
+
if (isClaudeCodeMode) {
|
|
2690
|
+
const defaults = pickClaudeLaunchDefaults(finalPickable, chosenModel);
|
|
2691
|
+
let selectedModel = defaults.model;
|
|
2692
|
+
if (args.prompt && finalPickable.length > 0) selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
2693
|
+
type: "select",
|
|
2694
|
+
options: finalPickable,
|
|
2695
|
+
initial: defaults.model
|
|
2696
|
+
});
|
|
2697
|
+
const command = buildClaudeLaunchCommand({
|
|
2698
|
+
baseUrl,
|
|
2699
|
+
model: selectedModel
|
|
2700
|
+
});
|
|
2701
|
+
consola.box([
|
|
2702
|
+
"Claude one-shot command",
|
|
2703
|
+
"",
|
|
2704
|
+
` ${command}`,
|
|
2705
|
+
"",
|
|
2706
|
+
" The bridge verified this path with explicit --model; current Claude CLI did not reliably honor ANTHROPIC_MODEL env defaults."
|
|
2707
|
+
].join("\n"));
|
|
2708
|
+
}
|
|
2624
2709
|
await new Promise((resolve, reject) => {
|
|
2625
2710
|
server.on("close", resolve);
|
|
2626
2711
|
server.on("error", reject);
|