betahi-copilot-bridge 0.1.1 → 0.1.3

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
@@ -5,7 +5,7 @@ endpoint, so [Codex CLI](https://github.com/openai/codex) and
5
5
  [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) can
6
6
  talk to Copilot transparently.
7
7
 
8
- **Supports:** Codex CLI and Claude Code out of the box.
8
+ **Supports:** **Codex CLI** and **Claude Code** out of the box.
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
@@ -112,7 +117,13 @@ limit (about 900k succeeds; around 1,000,046 is rejected as too long).
112
117
 
113
118
  Bridge endpoints: `POST /v1/messages`, `POST /v1/messages/count_tokens`.
114
119
 
115
- Point Claude Code at the bridge through **`~/.claude/settings.json`**:
120
+ `copilot-bridge start` automatically writes `ANTHROPIC_BASE_URL` (and a dummy
121
+ `ANTHROPIC_AUTH_TOKEN` if absent) into **`~/.claude/settings.json`**, so
122
+ `claude` works the moment the bridge is listening — even on a different host
123
+ or port. Pass `--no-claude-setup` to skip this. All other keys in your
124
+ settings file (model overrides, plugins, marketplaces) are preserved.
125
+
126
+ If you prefer to wire it yourself:
116
127
 
117
128
  ```json
118
129
  {
@@ -122,15 +133,20 @@ Point Claude Code at the bridge through **`~/.claude/settings.json`**:
122
133
  "ANTHROPIC_MODEL": "claude-opus-4.7",
123
134
  "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
124
135
  "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4.5",
125
- "COPILOT_REASONING_EFFORT": "medium"
136
+ "MODEL_REASONING_EFFORT": "medium"
126
137
  },
127
138
  "model": "claude-opus-4.7"
128
139
  }
129
140
  ```
130
141
 
131
- `COPILOT_REASONING_EFFORT` is also read from the project-local
132
- `.claude/settings.json` and `.claude/settings.local.json` and applied to the
133
- upstream call when the model supports reasoning.
142
+ `MODEL_REASONING_EFFORT` (case-insensitive key lookup) is also read from the
143
+ 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).
134
150
 
135
151
  ## Environment overrides
136
152
 
@@ -140,7 +156,10 @@ upstream call when the model supports reasoning.
140
156
  | `COPILOT_ACCOUNT_TYPE` | `individual` \| `business` \| `enterprise`. |
141
157
  | `COPILOT_BASE_URL` | Override the upstream Copilot base URL. |
142
158
  | `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
143
- | `COPILOT_REASONING_EFFORT` | Default reasoning effort when the model supports it. |
159
+ | `MODEL_REASONING_EFFORT` | Claude-side default reasoning effort (preferred key). |
160
+ | `COPILOT_REASONING_EFFORT` | Alias of `MODEL_REASONING_EFFORT` (backward compatible). |
161
+
162
+
144
163
 
145
164
  ## Supported models
146
165
 
@@ -189,7 +208,8 @@ accepts upstream.
189
208
  If an unsupported value is sent for a reasoning-capable model, the bridge
190
209
  falls back to the model's default (`medium` for the GPT-5 / Claude Opus 4.7
191
210
  families) instead of forwarding an invalid request upstream. Set the default
192
- globally via `COPILOT_REASONING_EFFORT` (env, or `env` in
211
+ globally via `MODEL_REASONING_EFFORT` (or `COPILOT_REASONING_EFFORT` alias)
212
+ (env, or `env` in
193
213
  `~/.claude/settings.json`); per-request `reasoning_effort` (or Anthropic
194
214
  `thinking.budget_tokens`) takes precedence.
195
215
 
@@ -211,3 +231,7 @@ bun run ./src/main.ts start --host 127.0.0.1 --port 4141 --no-prompt
211
231
  Adding another CLI: drop a new translator under `src/bridges/<client>/`,
212
232
  reuse `src/services/copilot/` for upstream calls, register routes in
213
233
  `src/server.ts`, and add tests under `tests/`.
234
+
235
+ ## License
236
+
237
+ MIT — see [LICENSE](./LICENSE).
package/dist/main.js CHANGED
@@ -281,6 +281,111 @@ const auth = defineCommand({
281
281
  }
282
282
  });
283
283
 
284
+ //#endregion
285
+ //#region src/lib/claude-settings.ts
286
+ const getClaudeSettingsPaths = () => {
287
+ const cwd = process.cwd();
288
+ const home = process.env.HOME ?? os.homedir();
289
+ return [
290
+ path.join(home, ".claude", "settings.json"),
291
+ path.join(cwd, ".claude", "settings.json"),
292
+ path.join(cwd, ".claude", "settings.local.json")
293
+ ];
294
+ };
295
+ const readClaudeSettingsFile = async (filePath) => {
296
+ try {
297
+ const content = await fs.readFile(filePath, "utf8");
298
+ return JSON.parse(content);
299
+ } catch (error) {
300
+ if (error.code === "ENOENT") return;
301
+ consola.warn(`Failed to read Claude settings from ${filePath}:`, error);
302
+ return;
303
+ }
304
+ };
305
+ /**
306
+ * Read just the `ANTHROPIC_BASE_URL` from a single Claude settings file.
307
+ * Used at startup so the user can pin the bridge port by editing one place
308
+ * instead of passing `--port` every time.
309
+ */
310
+ const readClaudeBaseUrl = async (configPath) => {
311
+ const value = (await readClaudeSettingsFile(configPath))?.env?.ANTHROPIC_BASE_URL;
312
+ return typeof value === "string" ? value : void 0;
313
+ };
314
+ /**
315
+ * Parse the port from an `ANTHROPIC_BASE_URL`-style string. Returns
316
+ * `undefined` if the URL is malformed or has no explicit port.
317
+ */
318
+ const parsePortFromBaseUrl = (baseUrl) => {
319
+ if (!baseUrl) return void 0;
320
+ try {
321
+ const url = new URL(baseUrl);
322
+ if (!url.port) return void 0;
323
+ const port = Number.parseInt(url.port, 10);
324
+ return Number.isFinite(port) && port > 0 ? port : void 0;
325
+ } catch {
326
+ return;
327
+ }
328
+ };
329
+ const getClaudeSettingsEnv = async () => {
330
+ const merged = {};
331
+ for (const filePath of getClaudeSettingsPaths()) {
332
+ const settings = await readClaudeSettingsFile(filePath);
333
+ if (!settings?.env) continue;
334
+ for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
335
+ }
336
+ return merged;
337
+ };
338
+ /**
339
+ * Update `~/.claude/settings.json` so its env block points Claude Code at the
340
+ * running bridge. Preserves all unrelated keys (model overrides, plugins,
341
+ * marketplaces, etc.). Sets a dummy auth token only if none is present.
342
+ */
343
+ async function applyClaudeConfig(input) {
344
+ const { configPath, baseUrl } = input;
345
+ let raw = "";
346
+ let created = false;
347
+ try {
348
+ raw = await fs.readFile(configPath, "utf8");
349
+ } catch (error) {
350
+ if (error.code === "ENOENT") created = true;
351
+ else throw error;
352
+ }
353
+ let parsed = {};
354
+ if (raw.trim().length > 0) try {
355
+ parsed = JSON.parse(raw);
356
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) parsed = {};
357
+ } catch {
358
+ return {
359
+ configPath,
360
+ changed: false,
361
+ created: false
362
+ };
363
+ }
364
+ const env = typeof parsed.env === "object" && parsed.env !== null && !Array.isArray(parsed.env) ? { ...parsed.env } : {};
365
+ const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
366
+ env.ANTHROPIC_BASE_URL = baseUrl;
367
+ if (typeof env.ANTHROPIC_AUTH_TOKEN !== "string" || !env.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = "dummy";
368
+ const next = {
369
+ ...parsed,
370
+ env
371
+ };
372
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
373
+ if (serialized === raw) return {
374
+ configPath,
375
+ changed: false,
376
+ created: false,
377
+ previousBaseUrl
378
+ };
379
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
380
+ await fs.writeFile(configPath, serialized);
381
+ return {
382
+ configPath,
383
+ changed: true,
384
+ created,
385
+ previousBaseUrl
386
+ };
387
+ }
388
+
284
389
  //#endregion
285
390
  //#region src/lib/codex-config.ts
286
391
  const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>";
@@ -395,6 +500,18 @@ async function readCodexUserConfigFromDisk(configPath) {
395
500
  }
396
501
  }
397
502
 
503
+ //#endregion
504
+ //#region src/lib/defaults.ts
505
+ const DEFAULT_HOST = "127.0.0.1";
506
+ const DEFAULT_PORT = 4142;
507
+ const CODEX_DEFAULTS = {
508
+ providerId: "bridge",
509
+ providerName: "Copilot Bridge",
510
+ setAsDefault: true,
511
+ configPath: path.join(os.homedir(), ".codex", "config.toml")
512
+ };
513
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude", "settings.json");
514
+
398
515
  //#endregion
399
516
  //#region src/lib/model-capabilities.ts
400
517
  const MODEL_CAPABILITIES = [
@@ -615,49 +732,6 @@ const clampReasoningEffort = (modelId, requested) => {
615
732
  };
616
733
  };
617
734
 
618
- //#endregion
619
- //#region src/lib/settings.ts
620
- const SETTINGS_DIR = path.join(os.homedir(), ".config", "copilot-bridge");
621
- const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
622
- const DEFAULT_SETTINGS = {
623
- host: "127.0.0.1",
624
- port: 4142,
625
- codex: {
626
- enabled: true,
627
- providerId: "bridge",
628
- providerName: "Copilot Bridge",
629
- setAsDefault: true,
630
- configPath: path.join(os.homedir(), ".codex", "config.toml")
631
- }
632
- };
633
- function deepMerge(base, override) {
634
- if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) return override ?? base;
635
- const out = { ...base };
636
- for (const [key, value] of Object.entries(override)) out[key] = deepMerge(base[key], value);
637
- return out;
638
- }
639
- async function loadSettings() {
640
- try {
641
- const raw = await fs.readFile(SETTINGS_FILE, "utf8");
642
- return deepMerge(DEFAULT_SETTINGS, JSON.parse(raw));
643
- } catch {
644
- return {
645
- ...DEFAULT_SETTINGS,
646
- codex: { ...DEFAULT_SETTINGS.codex }
647
- };
648
- }
649
- }
650
- async function ensureSettingsFile() {
651
- const settings = await loadSettings();
652
- try {
653
- await fs.access(SETTINGS_FILE);
654
- } catch {
655
- await fs.mkdir(SETTINGS_DIR, { recursive: true });
656
- await fs.writeFile(SETTINGS_FILE, `${JSON.stringify(settings, null, 2)}\n`);
657
- }
658
- return settings;
659
- }
660
-
661
735
  //#endregion
662
736
  //#region src/lib/rate-limit.ts
663
737
  const sleep = (ms) => new Promise((resolve) => {
@@ -702,37 +776,6 @@ var RateLimitError = class extends Error {
702
776
  }
703
777
  };
704
778
 
705
- //#endregion
706
- //#region src/lib/claude-settings.ts
707
- const getClaudeSettingsPaths = () => {
708
- const cwd = process.cwd();
709
- const home = process.env.HOME ?? os.homedir();
710
- return [
711
- path.join(home, ".claude", "settings.json"),
712
- path.join(cwd, ".claude", "settings.json"),
713
- path.join(cwd, ".claude", "settings.local.json")
714
- ];
715
- };
716
- const readClaudeSettingsFile = async (filePath) => {
717
- try {
718
- const content = await fs.readFile(filePath, "utf8");
719
- return JSON.parse(content);
720
- } catch (error) {
721
- if (error.code === "ENOENT") return;
722
- consola.warn(`Failed to read Claude settings from ${filePath}:`, error);
723
- return;
724
- }
725
- };
726
- const getClaudeSettingsEnv = async () => {
727
- const merged = {};
728
- for (const filePath of getClaudeSettingsPaths()) {
729
- const settings = await readClaudeSettingsFile(filePath);
730
- if (!settings?.env) continue;
731
- for (const [key, value] of Object.entries(settings.env)) if (typeof value === "string") merged[key] = value;
732
- }
733
- return merged;
734
- };
735
-
736
779
  //#endregion
737
780
  //#region src/services/copilot/responses.ts
738
781
  const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.4-mini)(?:-|$)/i;
@@ -961,7 +1004,7 @@ function getFinishReason(response, hasFunctionCalls) {
961
1004
  //#endregion
962
1005
  //#region src/services/copilot/create-chat-completions.ts
963
1006
  const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
964
- const isClaudeOpus47Model$1 = (modelId) => modelId === "claude-opus-4.7";
1007
+ const isClaudeOpus47Model$1 = (modelId) => modelId.startsWith("claude-opus-4.7");
965
1008
  const MAX_USER_LENGTH = 64;
966
1009
  const defaultReasoningEffort = (modelId) => usesMaxCompletionTokens(modelId) ? "medium" : void 0;
967
1010
  const getAllowedReasoningEfforts = (modelId) => {
@@ -1016,33 +1059,47 @@ const normalizeClaudeOpus47Effort = (value) => {
1016
1059
  default: return;
1017
1060
  }
1018
1061
  };
1019
- const getRequestedReasoningEffort = (payload, claudeSettingsEnv) => {
1020
- const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(process.env.COPILOT_REASONING_EFFORT) ?? normalizeReasoningEffort$1(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
1021
- return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultReasoningEffort(payload.model);
1062
+ const getEnvValueCaseInsensitive = (env, key) => {
1063
+ const direct = env[key];
1064
+ if (typeof direct === "string") return direct;
1065
+ const lower = key.toLowerCase();
1066
+ return Object.entries(env).find(([k]) => k.toLowerCase() === lower)?.[1];
1067
+ };
1068
+ const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
1069
+ 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");
1071
+ };
1072
+ const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
1073
+ const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
1074
+ const defaultEffort = client === "claude" ? "medium" : defaultReasoningEffort(payload.model);
1075
+ const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
1076
+ return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) ?? defaultEffort;
1022
1077
  };
1023
- const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv) => {
1078
+ const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
1024
1079
  if (!isClaudeOpus47Model$1(payload.model)) return;
1025
- return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(process.env.COPILOT_REASONING_EFFORT) ?? normalizeClaudeOpus47Effort(claudeSettingsEnv.COPILOT_REASONING_EFFORT);
1080
+ const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
1081
+ return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort) ?? (client === "claude" ? "medium" : void 0);
1026
1082
  };
1027
1083
  const sanitizeUserIdentifier = (user) => {
1028
1084
  if (!user) return;
1029
1085
  return user.slice(0, MAX_USER_LENGTH);
1030
1086
  };
1031
- const buildRequestPayload = (payload, claudeSettingsEnv) => {
1032
- const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
1033
- const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv);
1087
+ const buildRequestPayload = (payload, claudeSettingsEnv, client) => {
1088
+ const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
1089
+ const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv, client);
1034
1090
  const reasoningEffort = usesMaxCompletionTokens(payload.model) && payload.tools !== null && payload.tools !== void 0 && payload.tools.length > 0 ? void 0 : requestedReasoningEffort;
1035
1091
  if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) {
1092
+ const shouldAttachReasoningEffort = !isClaudeOpus47Model$1(payload.model);
1036
1093
  const sanitizedPayload = {
1037
1094
  ...payload,
1038
1095
  output_config: requestedClaudeOpus47Effort ? {
1039
1096
  ...payload.output_config,
1040
1097
  effort: requestedClaudeOpus47Effort
1041
1098
  } : payload.output_config,
1042
- reasoning_effort: isClaudeOpus47Model$1(payload.model) ? void 0 : payload.reasoning_effort,
1099
+ reasoning_effort: shouldAttachReasoningEffort ? payload.reasoning_effort : void 0,
1043
1100
  user: sanitizeUserIdentifier(payload.user)
1044
1101
  };
1045
- return reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
1102
+ return !shouldAttachReasoningEffort || reasoningEffort === null || reasoningEffort === void 0 ? sanitizedPayload : {
1046
1103
  ...sanitizedPayload,
1047
1104
  reasoning_effort: reasoningEffort
1048
1105
  };
@@ -1057,13 +1114,14 @@ const buildRequestPayload = (payload, claudeSettingsEnv) => {
1057
1114
  };
1058
1115
  const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
1059
1116
  const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
1060
- const createChatCompletions = async (config, payload) => {
1117
+ const createChatCompletions = async (config, payload, options) => {
1061
1118
  const provider = getCopilotProviderContext(config);
1062
1119
  const enableVision = messagesIncludeImage(payload.messages);
1063
1120
  const initiator = isAgentInitiator(payload.messages);
1064
- const claudeSettingsEnv = await getClaudeSettingsEnv();
1065
- const requestPayload = buildRequestPayload(payload, claudeSettingsEnv);
1066
- if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, {
1121
+ const client = options?.client ?? "generic";
1122
+ const claudeSettingsEnv = client === "claude" ? await getClaudeSettingsEnv() : {};
1123
+ const requestPayload = buildRequestPayload(payload, claudeSettingsEnv, client);
1124
+ if (shouldUseResponsesApiForModel(payload.model)) return createResponses(provider, payload, claudeSettingsEnv, client, {
1067
1125
  vision: enableVision,
1068
1126
  initiator
1069
1127
  });
@@ -1079,7 +1137,7 @@ const createChatCompletions = async (config, payload) => {
1079
1137
  initiator
1080
1138
  });
1081
1139
  if (!response.ok) {
1082
- if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, {
1140
+ if (await shouldRetryWithResponses(response)) return createResponses(provider, payload, claudeSettingsEnv, client, {
1083
1141
  vision: enableVision,
1084
1142
  initiator
1085
1143
  });
@@ -1089,8 +1147,8 @@ const createChatCompletions = async (config, payload) => {
1089
1147
  if (payload.stream) return events(response);
1090
1148
  return await response.json();
1091
1149
  };
1092
- async function createResponses(provider, payload, claudeSettingsEnv, options) {
1093
- const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv);
1150
+ async function createResponses(provider, payload, claudeSettingsEnv, client, options) {
1151
+ const reasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
1094
1152
  const response = await fetchCopilot(provider, "/responses", {
1095
1153
  method: "POST",
1096
1154
  headers: {
@@ -1125,7 +1183,7 @@ chatCompletionRoutes.post("/", async (c) => {
1125
1183
  try {
1126
1184
  await checkRateLimit();
1127
1185
  const payload = await c.req.json();
1128
- const response = await createChatCompletions(c.var.config, payload);
1186
+ const response = await createChatCompletions(c.var.config, payload, { client: "generic" });
1129
1187
  if (isNonStreaming(response)) return c.json(response);
1130
1188
  return streamSSE(c, async (stream) => {
1131
1189
  for await (const chunk of response) await stream.writeSSE(chunk);
@@ -1272,7 +1330,7 @@ function isClaudeModel(modelId) {
1272
1330
  return modelId.startsWith("claude-");
1273
1331
  }
1274
1332
  function isClaudeOpus47Model(modelId) {
1275
- return modelId === "claude-opus-4.7";
1333
+ return modelId.startsWith("claude-opus-4.7");
1276
1334
  }
1277
1335
  function normalizeClaudeEffort(value) {
1278
1336
  switch (value?.toLowerCase()) {
@@ -1814,7 +1872,7 @@ messageRoutes.post("/", async (c) => {
1814
1872
  }
1815
1873
  const openAIPayload = translateToOpenAI(await c.req.json());
1816
1874
  try {
1817
- const response = await createChatCompletions(config, openAIPayload);
1875
+ const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
1818
1876
  if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
1819
1877
  return streamSSE(c, async (stream) => {
1820
1878
  const streamState = {
@@ -2428,11 +2486,11 @@ const start = defineCommand({
2428
2486
  args: {
2429
2487
  host: {
2430
2488
  type: "string",
2431
- description: "Host to bind (overrides settings.json)."
2489
+ description: `Host to bind (default: ${DEFAULT_HOST}).`
2432
2490
  },
2433
2491
  port: {
2434
2492
  type: "string",
2435
- description: "Port to listen on (overrides settings.json)."
2493
+ description: `Port to listen on (default: ${DEFAULT_PORT}).`
2436
2494
  },
2437
2495
  "show-token": {
2438
2496
  type: "boolean",
@@ -2444,15 +2502,20 @@ const start = defineCommand({
2444
2502
  default: false,
2445
2503
  description: "Skip writing the bridge provider into ~/.codex/config.toml."
2446
2504
  },
2505
+ "no-claude-setup": {
2506
+ type: "boolean",
2507
+ default: false,
2508
+ description: "Skip writing ANTHROPIC_BASE_URL into ~/.claude/settings.json."
2509
+ },
2447
2510
  "select-model": {
2448
2511
  type: "boolean",
2449
2512
  default: false,
2450
- description: "Force the model picker even when codex.model is set."
2513
+ description: "Force the model picker even when ~/.codex/config.toml already has a model."
2451
2514
  },
2452
2515
  "no-prompt": {
2453
2516
  type: "boolean",
2454
2517
  default: false,
2455
- description: "Never prompt; use codex.model from settings.json as-is."
2518
+ description: "Never prompt; use the existing model from ~/.codex/config.toml as-is."
2456
2519
  },
2457
2520
  "rate-limit": {
2458
2521
  type: "string",
@@ -2465,10 +2528,13 @@ const start = defineCommand({
2465
2528
  }
2466
2529
  },
2467
2530
  async run({ args }) {
2468
- const settings = await ensureSettingsFile();
2531
+ const claudeConfigPath = CLAUDE_CONFIG_PATH;
2532
+ const claudePort = parsePortFromBaseUrl(await readClaudeBaseUrl(claudeConfigPath));
2533
+ const envPortRaw = process.env.PORT;
2534
+ const envPort = envPortRaw && Number.isFinite(Number(envPortRaw)) ? Number(envPortRaw) : void 0;
2469
2535
  const config = readBridgeConfig({
2470
- host: args.host ? String(args.host) : settings.host,
2471
- port: args.port ? Number(args.port) : settings.port
2536
+ host: args.host ? String(args.host) : DEFAULT_HOST,
2537
+ port: args.port ? Number(args.port) : envPort !== void 0 ? envPort : claudePort !== void 0 ? claudePort : DEFAULT_PORT
2472
2538
  });
2473
2539
  const rateLimitRaw = args["rate-limit"];
2474
2540
  if (rateLimitRaw !== void 0) {
@@ -2485,48 +2551,76 @@ const start = defineCommand({
2485
2551
  const server = startServer(config);
2486
2552
  consola.success(`copilot-bridge listening on http://${config.host}:${config.port}`);
2487
2553
  consola.info(`copilot base url: ${config.copilotBaseUrl}`);
2554
+ const baseUrl = `http://${config.host}:${config.port}`;
2555
+ const codexConfigPath = CODEX_DEFAULTS.configPath;
2556
+ if (!args["no-claude-setup"]) try {
2557
+ const claudeResult = await applyClaudeConfig({
2558
+ baseUrl,
2559
+ configPath: claudeConfigPath
2560
+ });
2561
+ if (claudeResult.changed) {
2562
+ consola.success(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
2563
+ if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) consola.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
2564
+ } else consola.info(`claude settings already up to date: ${claudeResult.configPath}`);
2565
+ } catch (error) {
2566
+ consola.warn(`Could not update claude settings (${claudeConfigPath}):`, error);
2567
+ }
2568
+ const codexUserConfig = await readCodexUserConfigFromDisk(codexConfigPath);
2569
+ let chosenModel = codexUserConfig.model;
2570
+ let chosenEffort = codexUserConfig.modelReasoningEffort;
2571
+ if (chosenModel && !chosenEffort) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
2572
+ if (!args["no-codex-setup"]) try {
2573
+ const result = await applyCodexConfig({
2574
+ baseUrl: `${baseUrl}/v1`,
2575
+ configPath: codexConfigPath,
2576
+ settings: CODEX_DEFAULTS,
2577
+ model: chosenModel,
2578
+ modelReasoningEffort: chosenEffort
2579
+ });
2580
+ if (result.changed) {
2581
+ consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
2582
+ if (CODEX_DEFAULTS.setAsDefault) consola.info(`codex now defaults to provider "${CODEX_DEFAULTS.providerId}". Run \`codex exec "..."\` directly.`);
2583
+ else consola.info(`provider "${CODEX_DEFAULTS.providerId}" registered. Use \`codex -c model_provider="${CODEX_DEFAULTS.providerId}" ...\``);
2584
+ } else consola.info(`codex config already up to date: ${result.configPath}`);
2585
+ } catch (error) {
2586
+ consola.warn(`Could not update codex config (${codexConfigPath}):`, error);
2587
+ }
2488
2588
  const models = await fetchAvailableModels(config);
2489
2589
  const supportedIds = new Set(MODEL_CAPABILITIES.map((m) => m.id));
2490
2590
  const pickable = models.length > 0 ? models.filter((id) => supportedIds.has(id)) : MODEL_CAPABILITIES.map((m) => m.id);
2491
2591
  const finalPickable = pickable.length > 0 ? pickable : MODEL_CAPABILITIES.map((m) => m.id);
2492
2592
  if (models.length > 0) consola.info(`Available models:\n${models.map((id) => `- ${id}${supportedIds.has(id) ? " (bridge-supported)" : ""}`).join("\n")}`);
2493
2593
  else consola.warn("Could not fetch model list from upstream Copilot API");
2494
- const codexUserConfig = await readCodexUserConfigFromDisk(settings.codex.configPath);
2495
- let chosenModel = codexUserConfig.model;
2496
- let chosenEffort = codexUserConfig.modelReasoningEffort;
2594
+ consola.box([
2595
+ `🌐 Usage viewer`,
2596
+ ``,
2597
+ ` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
2598
+ ].join("\n"));
2497
2599
  if (!args["no-prompt"] && finalPickable.length > 0 && (args["select-model"] || !chosenModel)) {
2498
2600
  const defaultId = chosenModel && finalPickable.includes(chosenModel) ? chosenModel : finalPickable.includes("gpt-5.3-codex") ? "gpt-5.3-codex" : finalPickable[0];
2499
- const selected = await consola.prompt(`Select a model for codex (writes to ${settings.codex.configPath})`, {
2601
+ const selected = await consola.prompt(`Select a model for codex (writes to ${codexConfigPath})`, {
2500
2602
  type: "select",
2501
2603
  options: finalPickable,
2502
2604
  initial: defaultId
2503
2605
  });
2504
- if (selected) {
2606
+ if (selected && selected !== chosenModel) {
2505
2607
  chosenModel = selected;
2506
2608
  const supported = getModelCapability(selected)?.reasoning?.supported;
2507
2609
  if (chosenEffort && supported && !supported.includes(chosenEffort)) chosenEffort = void 0;
2610
+ if (!args["no-codex-setup"]) try {
2611
+ const result = await applyCodexConfig({
2612
+ baseUrl: `${baseUrl}/v1`,
2613
+ configPath: codexConfigPath,
2614
+ settings: CODEX_DEFAULTS,
2615
+ model: chosenModel,
2616
+ modelReasoningEffort: chosenEffort
2617
+ });
2618
+ if (result.changed) consola.success(`codex config updated: ${result.configPath}`);
2619
+ } catch (error) {
2620
+ consola.warn(`Could not update codex config (${codexConfigPath}):`, error);
2621
+ }
2508
2622
  }
2509
2623
  }
2510
- const baseUrl = `http://${config.host}:${config.port}`;
2511
- consola.box([
2512
- `🌐 Usage viewer`,
2513
- ``,
2514
- ` https://betahi.github.io/copilot-bridge?endpoint=${baseUrl}/usage`
2515
- ].join("\n"));
2516
- if (settings.codex.enabled && !args["no-codex-setup"]) {
2517
- const result = await applyCodexConfig({
2518
- baseUrl: `${baseUrl}/v1`,
2519
- configPath: settings.codex.configPath,
2520
- settings: settings.codex,
2521
- model: chosenModel,
2522
- modelReasoningEffort: chosenEffort
2523
- });
2524
- if (result.changed) {
2525
- consola.success(`codex config ${result.created ? "created" : "updated"}: ${result.configPath}`);
2526
- if (settings.codex.setAsDefault) consola.info(`codex now defaults to provider "${settings.codex.providerId}". Run \`codex exec "..."\` directly.`);
2527
- else consola.info(`provider "${settings.codex.providerId}" registered. Use \`codex -c model_provider="${settings.codex.providerId}" ...\``);
2528
- } else consola.info(`codex config already up to date: ${result.configPath}`);
2529
- }
2530
2624
  await new Promise((resolve, reject) => {
2531
2625
  server.on("close", resolve);
2532
2626
  server.on("error", reject);