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 +6 -21
- package/dist/main.js +46 -16
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
506
|
-
|
|
507
|
-
const re = new RegExp(`^\\s*${key}\\s
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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);
|