betahi-copilot-bridge 0.19.0 → 0.20.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.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
@@ -173,11 +159,6 @@ project-local `.claude/settings.json` and `.claude/settings.local.json` and
173
159
  applied to Claude requests only when the model supports reasoning. If it is not
174
160
  configured, Claude requests do not infer or attach a reasoning effort.
175
161
 
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
162
  ## Environment overrides
182
163
 
183
164
  | Variable | Purpose |
@@ -223,6 +204,10 @@ accepts upstream.
223
204
  | `claude-sonnet-4` | — | Reasoning not accepted upstream. |
224
205
  | `claude-haiku-4.5` | — | Reasoning not accepted upstream. |
225
206
 
207
+ For Claude Opus 4.7, both Codex CLI and Claude Code can use
208
+ `claude-opus-4.7` with reasoning effort `high` or `xhigh`; the bridge routes the
209
+ request to the matching upstream reasoning variant.
210
+
226
211
  ### Gemini family — translated to chat completions
227
212
 
228
213
  | 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";
@@ -2605,9 +2623,9 @@ const routeClaudeOpus47ByReasoningEffort = (model, effort) => {
2605
2623
  default: return model;
2606
2624
  }
2607
2625
  };
2608
- const normalizeCodexResponsesRequest = (payload) => {
2626
+ const normalizeCodexResponsesRequest = (payload, configuredReasoningEffort) => {
2609
2627
  const parsed = codexResponsesRequestSchema.parse(payload);
2610
- const requestedReasoningEffort = isPlainObject(parsed.reasoning) ? parsed.reasoning.effort : void 0;
2628
+ const requestedReasoningEffort = (isPlainObject(parsed.reasoning) ? parsed.reasoning : void 0)?.effort ?? configuredReasoningEffort;
2611
2629
  const canonical = routeClaudeOpus47ByReasoningEffort(resolveModelId(parsed.model), requestedReasoningEffort);
2612
2630
  const capability = getModelCapability(canonical);
2613
2631
  if (!capability) return parsed;
@@ -2619,21 +2637,30 @@ const normalizeCodexResponsesRequest = (payload) => {
2619
2637
  if ("reasoning" in next) delete next.reasoning;
2620
2638
  return next;
2621
2639
  }
2622
- if ("reasoning" in next) if (!isPlainObject(next.reasoning)) delete next.reasoning;
2640
+ if ("reasoning" in next) if (!isPlainObject(next.reasoning)) if (configuredReasoningEffort === void 0 || configuredReasoningEffort === null) delete next.reasoning;
2641
+ else {
2642
+ const clamped = clampReasoningEffort(canonical, configuredReasoningEffort);
2643
+ if (clamped) next.reasoning = { effort: clamped.effort };
2644
+ }
2623
2645
  else {
2624
2646
  const incoming = next.reasoning;
2625
- if (incoming.effort === void 0 || incoming.effort === null) {
2647
+ const effort = incoming.effort ?? configuredReasoningEffort;
2648
+ if (effort === void 0 || effort === null) {
2626
2649
  const reasoning = removeReasoningEffort(incoming);
2627
2650
  if (reasoning) next.reasoning = reasoning;
2628
2651
  else delete next.reasoning;
2629
2652
  } else {
2630
- const clamped = clampReasoningEffort(canonical, incoming.effort);
2653
+ const clamped = clampReasoningEffort(canonical, effort);
2631
2654
  if (clamped) next.reasoning = {
2632
2655
  ...incoming,
2633
2656
  effort: clamped.effort
2634
2657
  };
2635
2658
  }
2636
2659
  }
2660
+ else if (configuredReasoningEffort !== void 0 && configuredReasoningEffort !== null) {
2661
+ const clamped = clampReasoningEffort(canonical, configuredReasoningEffort);
2662
+ if (clamped) next.reasoning = { effort: clamped.effort };
2663
+ }
2637
2664
  const text = isPlainObject(next.text) ? next.text : void 0;
2638
2665
  if (text && capability.textVerbosity && "verbosity" in text) {
2639
2666
  const { supported, default: defaultVerbosity } = capability.textVerbosity;
@@ -2671,6 +2698,9 @@ const logResponsesUpstreamError = async (message, response, context) => {
2671
2698
  request: JSON.stringify(summarizeResponsesRequestForDiagnostics(context.request))
2672
2699
  });
2673
2700
  };
2701
+ const readConfiguredCodexReasoningEffort = async () => {
2702
+ return normalizeCodexConfigReasoningEffort((await readCodexUserConfigFromDisk(process.env.CODEX_CONFIG_PATH ?? CODEX_DEFAULTS.configPath)).modelReasoningEffort);
2703
+ };
2674
2704
  responsesRoutes.post("/", async (c) => {
2675
2705
  try {
2676
2706
  await checkRateLimit();
@@ -2678,7 +2708,7 @@ responsesRoutes.post("/", async (c) => {
2678
2708
  if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
2679
2709
  throw error;
2680
2710
  }
2681
- const payload = normalizeCodexResponsesRequest(await c.req.json());
2711
+ const payload = normalizeCodexResponsesRequest(await c.req.json(), await readConfiguredCodexReasoningEffort());
2682
2712
  const provider = getCopilotProviderContext(c.get("config"));
2683
2713
  const search = new URL(c.req.url).search;
2684
2714
  const capability = getModelCapability(payload.model);