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 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://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
- 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:
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
- "COPILOT_REASONING_EFFORT": "medium"
132
- },
133
- "model": "claude-opus-4.7"
149
+ "MODEL_REASONING_EFFORT": "medium"
150
+ }
134
151
  }
135
152
  ```
136
153
 
137
- `COPILOT_REASONING_EFFORT` is also read from the project-local
138
- `.claude/settings.json` and `.claude/settings.local.json` and applied to the
139
- upstream call when the model supports reasoning.
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
- | `COPILOT_REASONING_EFFORT` | Default reasoning effort when the model supports it. |
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 `COPILOT_REASONING_EFFORT` (env, or `env` in
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 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];
@@ -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()) {
@@ -1059,36 +1062,46 @@ const normalizeClaudeOpus47Effort = (value) => {
1059
1062
  default: return;
1060
1063
  }
1061
1064
  };
1062
- const getRequestedReasoningEffort = (payload, claudeSettingsEnv) => {
1063
- const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(process.env.COPILOT_REASONING_EFFORT) ?? normalizeReasoningEffort$1(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
1064
- return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultReasoningEffort(payload.model);
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 getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv) => {
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
- return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(process.env.COPILOT_REASONING_EFFORT) ?? normalizeClaudeOpus47Effort(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
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 sanitizedPayload = {
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: isClaudeOpus47Model$1(payload.model) ? void 0 : payload.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 claudeSettingsEnv = await getClaudeSettingsEnv();
1108
- const requestPayload = buildRequestPayload(payload, claudeSettingsEnv);
1109
- if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, {
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
- 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
+ });
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
- consola.error("Failed to create responses", response);
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
- if (id.startsWith("claude-sonnet-4-")) return "claude-sonnet-4";
1236
- if (/^claude-opus-4-7-\d{8}$/.test(id)) return "claude-opus-4.7";
1237
- 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
+ }
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 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";
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
- function translateModelName(model) {
1312
- 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)));
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
- "no-codex-setup": {
2555
+ "codex-setup": {
2486
2556
  type: "boolean",
2487
- default: false,
2557
+ default: true,
2488
2558
  description: "Skip writing the bridge provider into ~/.codex/config.toml."
2489
2559
  },
2490
- "no-claude-setup": {
2560
+ "claude-setup": {
2491
2561
  type: "boolean",
2492
- default: false,
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
- "no-prompt": {
2575
+ prompt: {
2501
2576
  type: "boolean",
2502
- default: false,
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
- if (!args["no-claude-setup"]) try {
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 (!args["no-codex-setup"]) try {
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["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
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 (!args["no-codex-setup"]) try {
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);