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 CHANGED
@@ -1,11 +1,11 @@
1
- # copilot-bridge
1
+ <h1 align="center">copilot-bridge</h1>
2
2
 
3
- Run GitHub Copilot as a local OpenAI Responses– and Anthropic-compatible
4
- endpoint, so [Codex CLI](https://github.com/openai/codex) and
5
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) can
6
- talk to Copilot transparently.
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
- **Supports:** **Codex CLI** and **Claude Code** out of the box.
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
- If you prefer to wire it yourself:
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 default reasoning effort (preferred key). |
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
- If an unsupported value is sent for a reasoning-capable model, the bridge
209
- falls back to the model's default (`medium` for the GPT-5 / Claude Opus 4.7
210
- families) instead of forwarding an invalid request upstream. Set the default
211
- globally via `MODEL_REASONING_EFFORT` (or `COPILOT_REASONING_EFFORT` alias)
212
- (env, or `env` in
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 getClaudeSettingsEnv = async () => {
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 merged;
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
- if (!isReasoningEffort(requested)) return {
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(requested)) return {
713
- effort: requested,
714
- changed: false
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(requested);
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\.4-mini)(?:-|$)/i;
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 getAllowedReasoningEfforts(modelId).includes(reasoningEffort) ? reasoningEffort : void 0;
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 ?? process.env.COPILOT_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT") ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "COPILOT_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" ? "medium" : defaultReasoningEffort(payload.model);
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) ?? (client === "claude" ? "medium" : void 0);
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
- const sanitizedPayload = {
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 ? payload.reasoning_effort : void 0,
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
- consola.error("Failed to create chat completions", response);
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
- consola.error("Failed to create responses", response);
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
- if (id.startsWith("claude-sonnet-4-")) return "claude-sonnet-4";
1251
- if (/^claude-opus-4-7-\d{8}$/.test(id)) return "claude-opus-4.7";
1252
- if (id.startsWith("claude-opus-4-")) return "claude-opus-4";
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 normalized = model.trim().toLowerCase().replace(/\[1m\]$/, "");
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
- function translateModelName(model) {
1327
- return resolveUpstreamModelId(normalizeClaudeModelAlias(model));
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 (isClaudeModel(modelId)) return;
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: translateModelName(payload.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: translateAnthropicToolsToOpenAI(payload.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
- "no-codex-setup": {
2558
+ "codex-setup": {
2501
2559
  type: "boolean",
2502
- default: false,
2560
+ default: true,
2503
2561
  description: "Skip writing the bridge provider into ~/.codex/config.toml."
2504
2562
  },
2505
- "no-claude-setup": {
2563
+ "claude-setup": {
2506
2564
  type: "boolean",
2507
- default: false,
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
- "no-prompt": {
2578
+ prompt: {
2516
2579
  type: "boolean",
2517
- default: false,
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
- if (!args["no-claude-setup"]) try {
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["no-codex-setup"]) try {
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["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
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 (!args["no-codex-setup"]) try {
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);