betahi-copilot-bridge 0.1.3 → 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
@@ -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,8 +147,7 @@ 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
 
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()) {
@@ -1071,7 +1074,7 @@ const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
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" ? clampReasoningEffort(payload.model, void 0)?.effort : defaultReasoningEffort(payload.model);
1075
1078
  const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
1076
1079
  return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultEffort;
1077
1080
  };
@@ -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,8 +1420,8 @@ function normalizeReasoningEffort(value) {
1374
1420
  default: return;
1375
1421
  }
1376
1422
  }
1377
- function translateReasoningEffort(payload) {
1378
- const modelId = translateModelName(payload.model);
1423
+ function translateReasoningEffort(payload, settings) {
1424
+ const modelId = translateModelName(payload.model, settings);
1379
1425
  if (isClaudeModel(modelId)) return;
1380
1426
  if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
1381
1427
  if (payload.thinking?.type !== "enabled") return;
@@ -1386,18 +1432,18 @@ function translateReasoningEffort(payload) {
1386
1432
  if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
1387
1433
  return sanitizeReasoningEffortForModel(modelId, "xhigh");
1388
1434
  }
1389
- function translateToOpenAI(payload) {
1435
+ function translateToOpenAI(payload, settings) {
1390
1436
  return {
1391
- model: translateModelName(payload.model),
1437
+ model: translateModelName(payload.model, settings),
1392
1438
  messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
1393
1439
  max_tokens: payload.max_tokens,
1394
1440
  stop: payload.stop_sequences,
1395
1441
  stream: payload.stream,
1396
1442
  temperature: payload.temperature,
1397
1443
  top_p: payload.top_p,
1398
- thinking: translateThinking(payload),
1399
- output_config: translateOutputConfig(payload),
1400
- reasoning_effort: translateReasoningEffort(payload),
1444
+ thinking: translateThinking(payload, settings),
1445
+ output_config: translateOutputConfig(payload, settings),
1446
+ reasoning_effort: translateReasoningEffort(payload, settings),
1401
1447
  user: payload.metadata?.user_id,
1402
1448
  tools: translateAnthropicToolsToOpenAI(payload.tools),
1403
1449
  tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
@@ -1870,7 +1916,7 @@ messageRoutes.post("/", async (c) => {
1870
1916
  if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
1871
1917
  throw error;
1872
1918
  }
1873
- const openAIPayload = translateToOpenAI(await c.req.json());
1919
+ const openAIPayload = translateToOpenAI(await c.req.json(), await getClaudeSettings());
1874
1920
  try {
1875
1921
  const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
1876
1922
  if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
@@ -1925,7 +1971,7 @@ messageRoutes.post("/count_tokens", async (c) => {
1925
1971
  try {
1926
1972
  const anthropicBeta = c.req.header("anthropic-beta");
1927
1973
  const anthropicPayload = await c.req.json();
1928
- const openAIPayload = translateToOpenAI(anthropicPayload);
1974
+ const openAIPayload = translateToOpenAI(anthropicPayload, await getClaudeSettings());
1929
1975
  const selectedModel = resolveModel(openAIPayload.model);
1930
1976
  if (!selectedModel) {
1931
1977
  consola.warn(`Model ${openAIPayload.model} not found in registry, returning fallback token count`);
@@ -2309,6 +2355,15 @@ const normalizeCodexResponsesRequest = (payload) => {
2309
2355
  ...incoming ?? {},
2310
2356
  effort: clamped.effort
2311
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
+ }
2312
2367
  return next;
2313
2368
  };
2314
2369
 
@@ -2497,24 +2552,29 @@ const start = defineCommand({
2497
2552
  default: false,
2498
2553
  description: "Print GitHub and Copilot tokens during startup."
2499
2554
  },
2500
- "no-codex-setup": {
2555
+ "codex-setup": {
2501
2556
  type: "boolean",
2502
- default: false,
2557
+ default: true,
2503
2558
  description: "Skip writing the bridge provider into ~/.codex/config.toml."
2504
2559
  },
2505
- "no-claude-setup": {
2560
+ "claude-setup": {
2506
2561
  type: "boolean",
2507
- default: false,
2562
+ default: true,
2508
2563
  description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
2509
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
+ },
2510
2570
  "select-model": {
2511
2571
  type: "boolean",
2512
2572
  default: false,
2513
2573
  description: "Force the model picker even when ~/.codex/config.toml already has a model."
2514
2574
  },
2515
- "no-prompt": {
2575
+ prompt: {
2516
2576
  type: "boolean",
2517
- default: false,
2577
+ default: true,
2518
2578
  description: "Never prompt; use the existing model from ~/.codex/config.toml as-is."
2519
2579
  },
2520
2580
  "rate-limit": {
@@ -2553,7 +2613,9 @@ const start = defineCommand({
2553
2613
  consola.info(`copilot base url: ${config.copilotBaseUrl}`);
2554
2614
  const baseUrl = `http://${config.host}:${config.port}`;
2555
2615
  const codexConfigPath = CODEX_DEFAULTS.configPath;
2556
- 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 {
2557
2619
  const claudeResult = await applyClaudeConfig({
2558
2620
  baseUrl,
2559
2621
  configPath: claudeConfigPath
@@ -2569,7 +2631,7 @@ const start = defineCommand({
2569
2631
  let chosenModel = codexUserConfig.model;
2570
2632
  let chosenEffort = codexUserConfig.modelReasoningEffort;
2571
2633
  if (chosenModel && !chosenEffort) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
2572
- if (!args["no-codex-setup"]) try {
2634
+ if (!isClaudeCodeMode && args["codex-setup"]) try {
2573
2635
  const result = await applyCodexConfig({
2574
2636
  baseUrl: `${baseUrl}/v1`,
2575
2637
  configPath: codexConfigPath,
@@ -2596,7 +2658,7 @@ const start = defineCommand({
2596
2658
  ``,
2597
2659
  ` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
2598
2660
  ].join("\n"));
2599
- if (!args["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
2661
+ if (!isClaudeCodeMode && args.prompt && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
2600
2662
  const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
2601
2663
  const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
2602
2664
  type: "select",
@@ -2607,7 +2669,7 @@ const start = defineCommand({
2607
2669
  chosenModel = selected;
2608
2670
  const supported = getModelCapability(selected)?.reasoning?.supported;
2609
2671
  if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
2610
- if (!args["no-codex-setup"]) try {
2672
+ if (args["codex-setup"]) try {
2611
2673
  const result = await applyCodexConfig({
2612
2674
  baseUrl: `${baseUrl}/v1`,
2613
2675
  configPath: codexConfigPath,
@@ -2621,6 +2683,26 @@ const start = defineCommand({
2621
2683
  }
2622
2684
  }
2623
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
+ }
2624
2706
  await new Promise((resolve, reject) => {
2625
2707
  server.on("close", resolve);
2626
2708
  server.on("error", reject);