betahi-copilot-bridge 0.1.8 → 0.19.0

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,7 +1,7 @@
1
1
  <h1 align="center">copilot-bridge</h1>
2
2
 
3
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?v=0.1.8" alt="npm version"></a>
4
+ <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.19.0" alt="npm version"></a>
5
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
6
  </p>
7
7
 
@@ -100,13 +100,23 @@ these keys across rewrites:
100
100
  ```toml
101
101
  model = "gpt-5.3-codex"
102
102
  model_reasoning_effort = "high"
103
+ model_supports_reasoning_summaries = true
103
104
  ```
104
105
 
105
106
  If `model_reasoning_effort` is omitted, the bridge leaves it unset and does not
106
107
  send a reasoning effort for Codex requests (a startup info log also reminds you).
108
+ Codex requires `model_supports_reasoning_summaries = true` for custom providers
109
+ to serialize the `reasoning` object; `start --select-model` writes it for you
110
+ when it writes `model_reasoning_effort`.
107
111
 
108
112
  `model_reasoning_effort` only affects Codex requests; it does not affect Claude.
109
113
 
114
+ For Claude Opus 4.7 through Codex, you can keep `model = "claude-opus-4.7"`
115
+ and set `model_reasoning_effort = "high"` or `"xhigh"`; the bridge routes the
116
+ request to the matching Opus 4.7 reasoning variant upstream. Use
117
+ `model = "claude-opus-4.7-1m"` for the 1M context window; it maps to the upstream
118
+ 1M model without exposing the upstream-only suffix in your config.
119
+
110
120
  That's it — `codex exec '...'` will now route through the bridge to Copilot.
111
121
 
112
122
  Use `--no-codex-setup` to skip the managed-block writer entirely (e.g. if you
@@ -163,6 +173,11 @@ project-local `.claude/settings.json` and `.claude/settings.local.json` and
163
173
  applied to Claude requests only when the model supports reasoning. If it is not
164
174
  configured, Claude requests do not infer or attach a reasoning effort.
165
175
 
176
+ For compatibility, `ANTHROPIC_MODEL=claude-opus-4.7` plus
177
+ `MODEL_REASONING_EFFORT=high` or `xhigh` is automatically routed to the matching
178
+ Opus 4.7 reasoning variant upstream, so existing settings keep the intended
179
+ reasoning level.
180
+
166
181
  ## Environment overrides
167
182
 
168
183
  | Variable | Purpose |
@@ -194,16 +209,19 @@ accepts upstream.
194
209
 
195
210
  ### Claude family — translated to chat completions
196
211
 
197
- | Model | Reasoning efforts | Notes |
198
- | -------------------- | --------------------------------------- | -------------------------------------- |
199
- | `claude-opus-4.7` | `low`, `medium`, `high`, `xhigh`, `max` | Effort sent as `output_config.effort`. |
200
- | `claude-opus-4.6` | `low`, `medium`, `high` | |
201
- | `claude-opus-4.6-1m` | `low`, `medium`, `high` | 1M-token context window. |
202
- | `claude-sonnet-4.6` | `low`, `medium`, `high` | |
203
- | `claude-opus-4.5` | — | Reasoning not accepted upstream. |
204
- | `claude-sonnet-4.5` | | Reasoning not accepted upstream. |
205
- | `claude-sonnet-4` | — | Reasoning not accepted upstream. |
206
- | `claude-haiku-4.5` | — | Reasoning not accepted upstream. |
212
+ | Model | Reasoning efforts | Notes |
213
+ | -------------------------------- | --------------------------------------- | -------------------------------------- |
214
+ | `claude-opus-4.7` | `medium` | Effort sent as `output_config.effort`. |
215
+ | `claude-opus-4.7-1m` | `low`, `medium`, `high`, `xhigh` | 1M-token context window; effort sent as `output_config.effort`. |
216
+ | `claude-opus-4.7-high` | `high` | Fixed high reasoning; effort sent as `output_config.effort`. |
217
+ | `claude-opus-4.7-xhigh` | `xhigh` | Fixed extra-high reasoning; effort sent as `output_config.effort`. |
218
+ | `claude-opus-4.6` | `low`, `medium`, `high` | |
219
+ | `claude-opus-4.6-1m` | `low`, `medium`, `high` | 1M-token context window. |
220
+ | `claude-sonnet-4.6` | `low`, `medium`, `high` | |
221
+ | `claude-opus-4.5` | — | Reasoning not accepted upstream. |
222
+ | `claude-sonnet-4.5` | — | Reasoning not accepted upstream. |
223
+ | `claude-sonnet-4` | — | Reasoning not accepted upstream. |
224
+ | `claude-haiku-4.5` | — | Reasoning not accepted upstream. |
207
225
 
208
226
  ### Gemini family — translated to chat completions
209
227
 
package/dist/main.js CHANGED
@@ -502,11 +502,20 @@ function setTopScalar(topSection, key, value) {
502
502
  if (topSection.length === 0) return `${line}\n`;
503
503
  return `${line}\n${topSection.startsWith("\n") ? "" : ""}${topSection}`;
504
504
  }
505
+ function setTopBoolean(topSection, key, value) {
506
+ if (value === void 0) return topSection;
507
+ const re = new RegExp(`^\\s*${key}\\s*=\\s*(true|false)\\s*$`, "m");
508
+ const line = `${key} = ${value ? "true" : "false"}`;
509
+ if (re.test(topSection)) return topSection.replace(re, line);
510
+ if (topSection.length === 0) return `${line}\n`;
511
+ return `${line}\n${topSection.startsWith("\n") ? "" : ""}${topSection}`;
512
+ }
505
513
  function applyUserScalars(content, input) {
506
514
  const { top, rest } = splitTopSection(content);
507
515
  let nextTop = top;
508
516
  nextTop = setTopScalar(nextTop, "model", input.model);
509
517
  nextTop = setTopScalar(nextTop, "model_reasoning_effort", input.modelReasoningEffort);
518
+ nextTop = setTopBoolean(nextTop, "model_supports_reasoning_summaries", input.modelReasoningEffort === void 0 ? void 0 : true);
510
519
  if (nextTop === top) return content;
511
520
  if (rest.length === 0) return nextTop.endsWith("\n") ? nextTop : `${nextTop}\n`;
512
521
  const sep = nextTop.endsWith("\n") ? "" : "\n";
@@ -649,21 +658,43 @@ const MODEL_CAPABILITIES = [
649
658
  default: "medium"
650
659
  }
651
660
  },
652
- {
653
- id: "claude-opus-4.7",
654
- fallback: "chat-completions",
655
- reasoningField: "output_config.effort",
656
- reasoning: {
661
+ ...[
662
+ {
663
+ id: "claude-opus-4.7",
664
+ supported: ["medium"],
665
+ defaultEffort: "medium"
666
+ },
667
+ {
668
+ id: "claude-opus-4.7-1m-internal",
669
+ aliases: ["claude-opus-4.7-1m"],
657
670
  supported: [
658
671
  "low",
659
672
  "medium",
660
673
  "high",
661
- "xhigh",
662
- "max"
674
+ "xhigh"
663
675
  ],
664
- default: "medium"
676
+ defaultEffort: "medium"
677
+ },
678
+ {
679
+ id: "claude-opus-4.7-high",
680
+ supported: ["high"],
681
+ defaultEffort: "high"
682
+ },
683
+ {
684
+ id: "claude-opus-4.7-xhigh",
685
+ supported: ["xhigh"],
686
+ defaultEffort: "xhigh"
665
687
  }
666
- },
688
+ ].map(({ id, aliases, supported, defaultEffort }) => ({
689
+ id,
690
+ aliases,
691
+ fallback: "chat-completions",
692
+ reasoningField: "output_config.effort",
693
+ reasoning: {
694
+ supported,
695
+ default: defaultEffort
696
+ }
697
+ })),
667
698
  {
668
699
  id: "claude-opus-4.6",
669
700
  fallback: "chat-completions",
@@ -1155,7 +1186,7 @@ const normalizeClaudeOpus47Effort = (value) => {
1155
1186
  default: return;
1156
1187
  }
1157
1188
  };
1158
- const getEnvValueCaseInsensitive = (env, key) => {
1189
+ const getEnvValueCaseInsensitive$1 = (env, key) => {
1159
1190
  const direct = env[key];
1160
1191
  if (typeof direct === "string") return direct;
1161
1192
  const lower = key.toLowerCase();
@@ -1163,7 +1194,7 @@ const getEnvValueCaseInsensitive = (env, key) => {
1163
1194
  };
1164
1195
  const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
1165
1196
  if (client !== "claude") return;
1166
- return process.env.MODEL_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT");
1197
+ return process.env.MODEL_REASONING_EFFORT ?? getEnvValueCaseInsensitive$1(claudeSettingsEnv, "MODEL_REASONING_EFFORT");
1167
1198
  };
1168
1199
  const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
1169
1200
  const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
@@ -1173,7 +1204,9 @@ const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
1173
1204
  const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
1174
1205
  if (!isClaudeOpus47Model$1(payload.model)) return;
1175
1206
  const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
1176
- return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort);
1207
+ const requested = normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort) ?? payload.output_config?.effort;
1208
+ if (!requested) return;
1209
+ return clampReasoningEffort(payload.model, requested)?.effort;
1177
1210
  };
1178
1211
  const sanitizeUserIdentifier = (user) => {
1179
1212
  if (!user) return;
@@ -1366,20 +1399,21 @@ embeddingRoutes.post("/", async (c) => {
1366
1399
  const normalizeModelId = (modelId) => modelId.trim().toLowerCase().replaceAll(/[\s._-]+/g, "");
1367
1400
  const stripSnapshotSuffix = (modelId) => {
1368
1401
  const id = modelId.trim().toLowerCase().replace(/\[1m\]$/, "");
1369
- const claudeSnapshotMatch = id.match(/^claude-(opus|sonnet|haiku)-(\d)-(\d)(-1m)?(?:-\d{8})?$/);
1402
+ if (id === "claude-opus-4.7-1m") return "claude-opus-4.7-1m-internal";
1403
+ const claudeSnapshotMatch = id.match(/^claude-(opus|sonnet|haiku)-(\d)-(\d)((?:-[a-z0-9]+)*?)(?:-\d{8})?$/);
1370
1404
  if (claudeSnapshotMatch) {
1371
- const [, family, major, minor, contextWindow = ""] = claudeSnapshotMatch;
1372
- return `claude-${family}-${major}.${minor}${contextWindow}`;
1405
+ const [, family, major, minor, suffix = ""] = claudeSnapshotMatch;
1406
+ return `claude-${family}-${major}.${minor}${suffix}`;
1373
1407
  }
1374
1408
  return id;
1375
1409
  };
1376
1410
  const getAliasCandidates = (modelId) => {
1377
1411
  const canonicalModelId = stripSnapshotSuffix(modelId);
1378
1412
  const aliases = new Set([canonicalModelId]);
1379
- const canonicalClaudeMatch = canonicalModelId.match(/^claude-(opus|sonnet|haiku)-(\d+)\.(\d+)(-1m)?$/);
1413
+ const canonicalClaudeMatch = canonicalModelId.match(/^claude-(opus|sonnet|haiku)-(\d+)\.(\d+)((?:-[a-z0-9]+)*)$/);
1380
1414
  if (canonicalClaudeMatch) {
1381
- const [, family, major, minor, contextWindow = ""] = canonicalClaudeMatch;
1382
- aliases.add(`claude-${family}-${major}-${minor}${contextWindow}`);
1415
+ const [, family, major, minor, suffix = ""] = canonicalClaudeMatch;
1416
+ aliases.add(`claude-${family}-${major}-${minor}${suffix}`);
1383
1417
  return [...aliases];
1384
1418
  }
1385
1419
  const familyMatch = canonicalModelId.match(/^[a-z]+(?:-[a-z]+)*-\d+(?:\.\d+)?/);
@@ -1526,12 +1560,42 @@ const normalizeClaudeModelAlias = (model) => {
1526
1560
  const trimmed = model.trim().toLowerCase();
1527
1561
  if (trimmed === "opus[1m]") return "claude-opus-4.6-1m";
1528
1562
  const normalized = trimmed.replace(/\[1m\]$/, "");
1529
- if (normalized === "opus") return "claude-opus";
1530
- if (normalized === "sonnet") return "claude-sonnet";
1531
- if (normalized === "haiku") return "claude-haiku";
1532
- 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");
1533
- if (/^claude-opus-4-7-\d{8}$/.test(normalized)) return "claude-opus-4.7";
1534
- return normalized;
1563
+ const prefixed = normalized.startsWith("opus-") ? `claude-${normalized}` : normalized;
1564
+ if (prefixed === "opus") return "claude-opus";
1565
+ if (prefixed === "sonnet") return "claude-sonnet";
1566
+ if (prefixed === "haiku") return "claude-haiku";
1567
+ const claudeSnapshotMatch = prefixed.match(/^claude-(opus|sonnet|haiku)-(\d)-(\d)((?:-[a-z0-9]+)*?)(?:-\d{8})?$/);
1568
+ if (claudeSnapshotMatch) {
1569
+ const [, family, major, minor, suffix = ""] = claudeSnapshotMatch;
1570
+ return `claude-${family}-${major}.${minor}${suffix}`;
1571
+ }
1572
+ return prefixed;
1573
+ };
1574
+ function normalizeClaudeReasoningEffortForRouting(value) {
1575
+ switch (value?.toLowerCase()) {
1576
+ case "low":
1577
+ case "medium":
1578
+ case "high":
1579
+ case "xhigh":
1580
+ case "max": return value.toLowerCase();
1581
+ default: return;
1582
+ }
1583
+ }
1584
+ const getEnvValueCaseInsensitive = (env, key) => {
1585
+ const direct = env[key];
1586
+ if (typeof direct === "string") return direct;
1587
+ const lower = key.toLowerCase();
1588
+ return Object.entries(env).find(([k]) => k.toLowerCase() === lower)?.[1];
1589
+ };
1590
+ const getConfiguredClaudeReasoningEffort = (settings) => process.env.MODEL_REASONING_EFFORT ?? getEnvValueCaseInsensitive(settings?.env ?? {}, "MODEL_REASONING_EFFORT");
1591
+ const routeClaudeOpus47ByEffort = (model, requestedEffort) => {
1592
+ if (model !== "claude-opus-4.7") return model;
1593
+ switch (normalizeClaudeReasoningEffortForRouting(requestedEffort)) {
1594
+ case "high": return "claude-opus-4.7-high";
1595
+ case "xhigh":
1596
+ case "max": return "claude-opus-4.7-xhigh";
1597
+ default: return model;
1598
+ }
1535
1599
  };
1536
1600
  const getConfiguredClaudeDefaultModel = (settings) => {
1537
1601
  if (!settings) return;
@@ -1551,8 +1615,8 @@ const resolveClaudeRequestedModel = (model, settings) => {
1551
1615
  if (model.trim().toLowerCase() !== "definitely-not-a-real-model") return model;
1552
1616
  return getConfiguredClaudeDefaultModel(settings) ?? model;
1553
1617
  };
1554
- function translateModelName(model, settings) {
1555
- return resolveUpstreamModelId(normalizeClaudeModelAlias(resolveClaudeRequestedModel(model, settings)));
1618
+ function translateModelName(model, settings, requestedReasoningEffort) {
1619
+ return resolveUpstreamModelId(routeClaudeOpus47ByEffort(normalizeClaudeModelAlias(resolveClaudeRequestedModel(model, settings)), requestedReasoningEffort ?? getConfiguredClaudeReasoningEffort(settings)));
1556
1620
  }
1557
1621
  function isClaudeModel(modelId) {
1558
1622
  return modelId.startsWith("claude-");
@@ -1582,12 +1646,12 @@ function getClaudeOpus47Effort(payload) {
1582
1646
  return "xhigh";
1583
1647
  }
1584
1648
  function translateThinking(payload, settings) {
1585
- if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
1649
+ if (!isClaudeOpus47Model(translateModelName(payload.model, settings, payload.reasoning_effort))) return;
1586
1650
  if (payload.thinking?.type === "adaptive") return { type: "adaptive" };
1587
1651
  return payload.thinking?.type === "enabled" ? { type: "adaptive" } : void 0;
1588
1652
  }
1589
1653
  function translateOutputConfig(payload, settings) {
1590
- if (!isClaudeOpus47Model(translateModelName(payload.model, settings))) return;
1654
+ if (!isClaudeOpus47Model(translateModelName(payload.model, settings, payload.reasoning_effort))) return;
1591
1655
  const effort = getClaudeOpus47Effort(payload);
1592
1656
  return effort ? { effort } : void 0;
1593
1657
  }
@@ -1603,7 +1667,7 @@ function normalizeReasoningEffort(value) {
1603
1667
  }
1604
1668
  }
1605
1669
  function translateReasoningEffort(payload, settings) {
1606
- const modelId = translateModelName(payload.model, settings);
1670
+ const modelId = translateModelName(payload.model, settings, payload.reasoning_effort);
1607
1671
  if (isClaudeOpus47Model(modelId)) return;
1608
1672
  if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
1609
1673
  if (isClaudeModel(modelId)) return;
@@ -1616,7 +1680,7 @@ function translateReasoningEffort(payload, settings) {
1616
1680
  return sanitizeReasoningEffortForModel(modelId, "xhigh");
1617
1681
  }
1618
1682
  function translateToOpenAI(payload, settings, toolNameMapper) {
1619
- const model = translateModelName(payload.model, settings);
1683
+ const model = translateModelName(payload.model, settings, payload.reasoning_effort);
1620
1684
  const mapper = toolNameMapper ?? createAnthropicToolNameMapper(payload.tools, { ...getToolNameMapperOptionsForModel(model) });
1621
1685
  const tools = translateAnthropicToolsToOpenAI(payload.tools, mapper);
1622
1686
  return {
@@ -2532,9 +2596,19 @@ const removeReasoningEffort = (reasoning) => {
2532
2596
  return Object.keys(next).length > 0 ? next : void 0;
2533
2597
  };
2534
2598
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2599
+ const routeClaudeOpus47ByReasoningEffort = (model, effort) => {
2600
+ if (model !== "claude-opus-4.7") return model;
2601
+ switch (typeof effort === "string" ? effort.toLowerCase() : void 0) {
2602
+ case "high": return "claude-opus-4.7-high";
2603
+ case "xhigh":
2604
+ case "max": return "claude-opus-4.7-xhigh";
2605
+ default: return model;
2606
+ }
2607
+ };
2535
2608
  const normalizeCodexResponsesRequest = (payload) => {
2536
2609
  const parsed = codexResponsesRequestSchema.parse(payload);
2537
- const canonical = resolveModelId(parsed.model);
2610
+ const requestedReasoningEffort = isPlainObject(parsed.reasoning) ? parsed.reasoning.effort : void 0;
2611
+ const canonical = routeClaudeOpus47ByReasoningEffort(resolveModelId(parsed.model), requestedReasoningEffort);
2538
2612
  const capability = getModelCapability(canonical);
2539
2613
  if (!capability) return parsed;
2540
2614
  const next = {
@@ -2758,6 +2832,8 @@ const startServer = (config) => serve({
2758
2832
 
2759
2833
  //#endregion
2760
2834
  //#region src/start.ts
2835
+ const getPublicModelId = (id) => id === "claude-opus-4.7-1m-internal" ? "claude-opus-4.7-1m" : id;
2836
+ const unique = (ids) => [...new Set(ids)];
2761
2837
  const fetchAvailableModels = async (config) => {
2762
2838
  try {
2763
2839
  const response = await fetchCopilot(getCopilotProviderContext(config), "/models", {
@@ -2897,10 +2973,13 @@ const start = defineCommand({
2897
2973
  consola.warn(`Could not update codex config (${codexConfigPath}):`, error);
2898
2974
  }
2899
2975
  const models = await fetchAvailableModels(config);
2900
- const supportedIds = new Set(MODEL_CAPABILITIES.map((m) => m.id));
2901
- const pickable = models.length > 0 ? models.filter((id) => supportedIds.has(id)) : MODEL_CAPABILITIES.map((m) => m.id);
2902
- const finalPickable = pickable.length > 0 ? pickable : MODEL_CAPABILITIES.map((m) => m.id);
2903
- if (models.length > 0) consola.info(`Available models:\n${models.map((id) => `- ${id}${supportedIds.has(id) ? " (bridge-supported)" : ""}`).join("\n")}`);
2976
+ const supportedIds = new Set(MODEL_CAPABILITIES.flatMap((m) => [m.id, ...m.aliases ?? []]));
2977
+ const fallbackModelIds = unique(MODEL_CAPABILITIES.map((m) => getPublicModelId(m.id)));
2978
+ const pickable = models.length > 0 ? unique(models.filter((id) => supportedIds.has(id)).map((id) => getPublicModelId(id))) : fallbackModelIds;
2979
+ const finalPickable = pickable.length > 0 ? pickable : fallbackModelIds;
2980
+ if (models.length > 0) consola.info(`Available models:\n${models.map((id) => {
2981
+ return `- ${getPublicModelId(id)}${supportedIds.has(id) ? " (bridge-supported)" : ""}`;
2982
+ }).join("\n")}`);
2904
2983
  else consola.warn("Could not fetch model list from upstream Copilot API");
2905
2984
  consola.box([
2906
2985
  `🌐 Usage viewer`,