betahi-copilot-bridge 0.19.0 → 0.20.1

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.19.0" 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.20.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
 
@@ -83,6 +83,7 @@ this block; the bridge regenerates it on every start.
83
83
  ```toml
84
84
  # >>> copilot-bridge managed block — auto-generated, do not edit between markers >>>
85
85
  model_provider = "bridge"
86
+ model_supports_reasoning_summaries = true
86
87
 
87
88
  [model_providers.bridge]
88
89
  name = "Copilot Bridge"
@@ -100,23 +101,8 @@ these keys across rewrites:
100
101
  ```toml
101
102
  model = "gpt-5.3-codex"
102
103
  model_reasoning_effort = "high"
103
- model_supports_reasoning_summaries = true
104
104
  ```
105
105
 
106
- If `model_reasoning_effort` is omitted, the bridge leaves it unset and does not
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`.
111
-
112
- `model_reasoning_effort` only affects Codex requests; it does not affect Claude.
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
-
120
106
  That's it — `codex exec '...'` will now route through the bridge to Copilot.
121
107
 
122
108
  Use `--no-codex-setup` to skip the managed-block writer entirely (e.g. if you
@@ -153,6 +139,18 @@ Minimal recommended config is:
153
139
  }
154
140
  ```
155
141
 
142
+ For Claude Code 1M context, use the `-[1m]` display form in
143
+ `ANTHROPIC_MODEL`. Claude Code shows a 1M context window for this form, while
144
+ the bridge maps it to the real Copilot `-1m` model upstream:
145
+
146
+ ```json
147
+ {
148
+ "env": {
149
+ "ANTHROPIC_MODEL": "claude-opus-4.7-[1m]",
150
+ }
151
+ }
152
+ ```
153
+
156
154
  Optional slot-specific:
157
155
 
158
156
  ```json
@@ -173,11 +171,6 @@ project-local `.claude/settings.json` and `.claude/settings.local.json` and
173
171
  applied to Claude requests only when the model supports reasoning. If it is not
174
172
  configured, Claude requests do not infer or attach a reasoning effort.
175
173
 
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
-
181
174
  ## Environment overrides
182
175
 
183
176
  | Variable | Purpose |
@@ -223,6 +216,15 @@ accepts upstream.
223
216
  | `claude-sonnet-4` | — | Reasoning not accepted upstream. |
224
217
  | `claude-haiku-4.5` | — | Reasoning not accepted upstream. |
225
218
 
219
+ For Claude Opus 4.7, both Codex CLI and Claude Code can use
220
+ `claude-opus-4.7` with reasoning effort `high` or `xhigh`; the bridge routes the
221
+ request to the matching upstream reasoning variant.
222
+
223
+ For Claude Code settings, prefer `claude-opus-4.7-[1m]` or
224
+ `claude-opus-4.6-[1m]` when you want the CLI `/context` UI and the upstream
225
+ model to both use 1M context. Direct API clients can use
226
+ `claude-opus-4.7-1m` or `claude-opus-4.6-1m`.
227
+
226
228
  ### Gemini family — translated to chat completions
227
229
 
228
230
  | Model | Aliases |
package/dist/main.js CHANGED
@@ -439,6 +439,18 @@ const BEGIN_MARK = "# >>> copilot-bridge managed block — auto-generated, do no
439
439
  const END_MARK = "# <<< copilot-bridge managed block — edits outside this block are preserved <<<";
440
440
  const LEGACY_BEGIN_MARK = "# >>> copilot-bridge managed (do not edit) >>>";
441
441
  const LEGACY_END_MARK = "# <<< copilot-bridge managed (do not edit) <<<";
442
+ function normalizeCodexConfigReasoningEffort(value) {
443
+ switch (value?.toLowerCase()) {
444
+ case "none":
445
+ case "minimal":
446
+ case "low":
447
+ case "medium":
448
+ case "high":
449
+ case "xhigh": return value.toLowerCase();
450
+ case "max": return "xhigh";
451
+ default: return;
452
+ }
453
+ }
442
454
  function tomlEscape(value) {
443
455
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
444
456
  }
@@ -447,6 +459,7 @@ function buildManagedBlock(input) {
447
459
  const lines = [];
448
460
  lines.push(BEGIN_MARK);
449
461
  if (settings.setAsDefault) lines.push(`model_provider = "${tomlEscape(settings.providerId)}"`);
462
+ lines.push("model_supports_reasoning_summaries = true");
450
463
  lines.push("");
451
464
  lines.push(`[model_providers.${settings.providerId}]`);
452
465
  lines.push(`name = "${tomlEscape(settings.providerName)}"`);
@@ -502,20 +515,25 @@ function setTopScalar(topSection, key, value) {
502
515
  if (topSection.length === 0) return `${line}\n`;
503
516
  return `${line}\n${topSection.startsWith("\n") ? "" : ""}${topSection}`;
504
517
  }
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}`;
518
+ function removeTopKey(topSection, key) {
519
+ const lines = topSection.split("\n");
520
+ const re = /* @__PURE__ */ new RegExp(`^\\s*${key}\\s*=`);
521
+ return lines.filter((line) => !re.test(line)).join("\n");
522
+ }
523
+ function sanitizeTopReasoningEffort(topSection) {
524
+ const match = topSection.match(scalarRegex("model_reasoning_effort"));
525
+ if (!match) return topSection;
526
+ const normalized = normalizeCodexConfigReasoningEffort(match[1]);
527
+ if (!normalized) return removeTopKey(topSection, "model_reasoning_effort");
528
+ if (normalized === match[1]) return topSection;
529
+ return topSection.replace(scalarRegex("model_reasoning_effort"), `model_reasoning_effort = "${tomlEscape(normalized)}"`);
512
530
  }
513
531
  function applyUserScalars(content, input) {
514
532
  const { top, rest } = splitTopSection(content);
515
- let nextTop = top;
533
+ let nextTop = removeTopKey(top, "model_supports_reasoning_summaries");
534
+ nextTop = sanitizeTopReasoningEffort(nextTop);
516
535
  nextTop = setTopScalar(nextTop, "model", input.model);
517
- nextTop = setTopScalar(nextTop, "model_reasoning_effort", input.modelReasoningEffort);
518
- nextTop = setTopBoolean(nextTop, "model_supports_reasoning_summaries", input.modelReasoningEffort === void 0 ? void 0 : true);
536
+ nextTop = setTopScalar(nextTop, "model_reasoning_effort", normalizeCodexConfigReasoningEffort(input.modelReasoningEffort));
519
537
  if (nextTop === top) return content;
520
538
  if (rest.length === 0) return nextTop.endsWith("\n") ? nextTop : `${nextTop}\n`;
521
539
  const sep = nextTop.endsWith("\n") ? "" : "\n";
@@ -1558,7 +1576,17 @@ const createAnthropicToolNameMapper = (tools, options = {}) => {
1558
1576
  //#region src/bridges/claude/non-stream-translation.ts
1559
1577
  const normalizeClaudeModelAlias = (model) => {
1560
1578
  const trimmed = model.trim().toLowerCase();
1561
- if (trimmed === "opus[1m]") return "claude-opus-4.6-1m";
1579
+ if (trimmed === "opus[1m]") return "claude-opus-4.7-1m";
1580
+ const claudeOpusOneMillionMatch = trimmed.match(/^claude-opus-(\d)[.-](\d)(?:-\d{8})?-?\[1m\]$/);
1581
+ if (claudeOpusOneMillionMatch) {
1582
+ const [, major, minor] = claudeOpusOneMillionMatch;
1583
+ return `claude-opus-${major}.${minor}-1m`;
1584
+ }
1585
+ const claudeOpusOneMillionStrippedByClaudeCodeMatch = trimmed.match(/^claude-opus-(\d)[.-](\d)-$/);
1586
+ if (claudeOpusOneMillionStrippedByClaudeCodeMatch) {
1587
+ const [, major, minor] = claudeOpusOneMillionStrippedByClaudeCodeMatch;
1588
+ return `claude-opus-${major}.${minor}-1m`;
1589
+ }
1562
1590
  const normalized = trimmed.replace(/\[1m\]$/, "");
1563
1591
  const prefixed = normalized.startsWith("opus-") ? `claude-${normalized}` : normalized;
1564
1592
  if (prefixed === "opus") return "claude-opus";
@@ -2605,9 +2633,9 @@ const routeClaudeOpus47ByReasoningEffort = (model, effort) => {
2605
2633
  default: return model;
2606
2634
  }
2607
2635
  };
2608
- const normalizeCodexResponsesRequest = (payload) => {
2636
+ const normalizeCodexResponsesRequest = (payload, configuredReasoningEffort) => {
2609
2637
  const parsed = codexResponsesRequestSchema.parse(payload);
2610
- const requestedReasoningEffort = isPlainObject(parsed.reasoning) ? parsed.reasoning.effort : void 0;
2638
+ const requestedReasoningEffort = (isPlainObject(parsed.reasoning) ? parsed.reasoning : void 0)?.effort ?? configuredReasoningEffort;
2611
2639
  const canonical = routeClaudeOpus47ByReasoningEffort(resolveModelId(parsed.model), requestedReasoningEffort);
2612
2640
  const capability = getModelCapability(canonical);
2613
2641
  if (!capability) return parsed;
@@ -2619,21 +2647,30 @@ const normalizeCodexResponsesRequest = (payload) => {
2619
2647
  if ("reasoning" in next) delete next.reasoning;
2620
2648
  return next;
2621
2649
  }
2622
- if ("reasoning" in next) if (!isPlainObject(next.reasoning)) delete next.reasoning;
2650
+ if ("reasoning" in next) if (!isPlainObject(next.reasoning)) if (configuredReasoningEffort === void 0 || configuredReasoningEffort === null) delete next.reasoning;
2651
+ else {
2652
+ const clamped = clampReasoningEffort(canonical, configuredReasoningEffort);
2653
+ if (clamped) next.reasoning = { effort: clamped.effort };
2654
+ }
2623
2655
  else {
2624
2656
  const incoming = next.reasoning;
2625
- if (incoming.effort === void 0 || incoming.effort === null) {
2657
+ const effort = incoming.effort ?? configuredReasoningEffort;
2658
+ if (effort === void 0 || effort === null) {
2626
2659
  const reasoning = removeReasoningEffort(incoming);
2627
2660
  if (reasoning) next.reasoning = reasoning;
2628
2661
  else delete next.reasoning;
2629
2662
  } else {
2630
- const clamped = clampReasoningEffort(canonical, incoming.effort);
2663
+ const clamped = clampReasoningEffort(canonical, effort);
2631
2664
  if (clamped) next.reasoning = {
2632
2665
  ...incoming,
2633
2666
  effort: clamped.effort
2634
2667
  };
2635
2668
  }
2636
2669
  }
2670
+ else if (configuredReasoningEffort !== void 0 && configuredReasoningEffort !== null) {
2671
+ const clamped = clampReasoningEffort(canonical, configuredReasoningEffort);
2672
+ if (clamped) next.reasoning = { effort: clamped.effort };
2673
+ }
2637
2674
  const text = isPlainObject(next.text) ? next.text : void 0;
2638
2675
  if (text && capability.textVerbosity && "verbosity" in text) {
2639
2676
  const { supported, default: defaultVerbosity } = capability.textVerbosity;
@@ -2671,6 +2708,9 @@ const logResponsesUpstreamError = async (message, response, context) => {
2671
2708
  request: JSON.stringify(summarizeResponsesRequestForDiagnostics(context.request))
2672
2709
  });
2673
2710
  };
2711
+ const readConfiguredCodexReasoningEffort = async () => {
2712
+ return normalizeCodexConfigReasoningEffort((await readCodexUserConfigFromDisk(process.env.CODEX_CONFIG_PATH ?? CODEX_DEFAULTS.configPath)).modelReasoningEffort);
2713
+ };
2674
2714
  responsesRoutes.post("/", async (c) => {
2675
2715
  try {
2676
2716
  await checkRateLimit();
@@ -2678,7 +2718,7 @@ responsesRoutes.post("/", async (c) => {
2678
2718
  if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
2679
2719
  throw error;
2680
2720
  }
2681
- const payload = normalizeCodexResponsesRequest(await c.req.json());
2721
+ const payload = normalizeCodexResponsesRequest(await c.req.json(), await readConfiguredCodexReasoningEffort());
2682
2722
  const provider = getCopilotProviderContext(c.get("config"));
2683
2723
  const search = new URL(c.req.url).search;
2684
2724
  const capability = getModelCapability(payload.model);