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 +23 -21
- package/dist/main.js +57 -17
- 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
|
|
@@ -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
|
|
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";
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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);
|