github-router 0.3.270 → 0.3.273
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/dist/{attribution-settings-of5gJM5a.js → attribution-settings-4nEQq_tn.js} +3 -3
- package/dist/{attribution-settings-of5gJM5a.js.map → attribution-settings-4nEQq_tn.js.map} +1 -1
- package/dist/{auth-DIFbHOu0.js → auth-3so8RT91.js} +2 -2
- package/dist/{auth-DIFbHOu0.js.map → auth-3so8RT91.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-DfnrR-d_.js → check-usage-goXyc3qS.js} +3 -3
- package/dist/{check-usage-DfnrR-d_.js.map → check-usage-goXyc3qS.js.map} +1 -1
- package/dist/{claude-BlzCUbVq.js → claude-BNZD-bQN.js} +7 -7
- package/dist/{claude-BlzCUbVq.js.map → claude-BNZD-bQN.js.map} +1 -1
- package/dist/{codex-BaFcXOyg.js → codex-C_MA9uy5.js} +5 -5
- package/dist/{codex-BaFcXOyg.js.map → codex-C_MA9uy5.js.map} +1 -1
- package/dist/engine-CHN8j6BV.js +2 -0
- package/dist/{gate-discovery-AUAjqSpV.js → gate-discovery-DvA4xObh.js} +2 -2
- package/dist/{gate-discovery-AUAjqSpV.js.map → gate-discovery-DvA4xObh.js.map} +1 -1
- package/dist/{get-copilot-usage-Ds09xSmH.js → get-copilot-usage-nZXbvSi8.js} +2 -2
- package/dist/{get-copilot-usage-Ds09xSmH.js.map → get-copilot-usage-nZXbvSi8.js.map} +1 -1
- package/dist/{internal-stop-hook-B1MIwmTR.js → internal-stop-hook-WVgnwt5U.js} +2 -2
- package/dist/{internal-stop-hook-B1MIwmTR.js.map → internal-stop-hook-WVgnwt5U.js.map} +1 -1
- package/dist/main.js +8 -8
- package/dist/{models-BFQUp0ov.js → models-BmGGH6pP.js} +2 -2
- package/dist/{models-BFQUp0ov.js.map → models-BmGGH6pP.js.map} +1 -1
- package/dist/{peer-mcp-personas-pWP3Eyt7.js → peer-mcp-personas-DAZU1OfN.js} +331 -20
- package/dist/peer-mcp-personas-DAZU1OfN.js.map +1 -0
- package/dist/{provision-CsYUVdMT.js → provision-C3UIA1Tf.js} +2 -2
- package/dist/{provision-CsYUVdMT.js.map → provision-C3UIA1Tf.js.map} +1 -1
- package/dist/{serve--c9n9hk4.js → serve-DPgawWWq.js} +6 -6
- package/dist/{serve--c9n9hk4.js.map → serve-DPgawWWq.js.map} +1 -1
- package/dist/{server-setup-BmyWS4WU.js → server-setup-I5KYkuxE.js} +570 -121
- package/dist/server-setup-I5KYkuxE.js.map +1 -0
- package/dist/{start-CXnLBouJ.js → start-CWWqjjwm.js} +3 -3
- package/dist/{start-CXnLBouJ.js.map → start-CWWqjjwm.js.map} +1 -1
- package/dist/{token-iaSGp76k.js → token-Bz7iN8US.js} +74 -6
- package/dist/token-Bz7iN8US.js.map +1 -0
- package/package.json +2 -1
- package/dist/engine-mwIgHJoi.js +0 -2
- package/dist/peer-mcp-personas-pWP3Eyt7.js.map +0 -1
- package/dist/server-setup-BmyWS4WU.js.map +0 -1
- package/dist/token-iaSGp76k.js.map +0 -1
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { A as searchWeb, B as
|
|
1
|
+
import { A as searchWeb, B as buildAnthropicErrorEvent, Bt as upstreamAllowH2, F as isAdvisorRequested, G as relayAnthropicStream, H as isControllerClosedError, Ht as withOneMSuffix, I as formatThinkingRepairDecline, It as UPSTREAM_FETCH_TIMEOUT_MS, J as agentToolsEnabled, K as handleMcpDelete, L as rememberThinkingHistoryRepair, Lt as UPSTREAM_INACTIVITY_TIMEOUT_MS, M as ADVISOR_TOOL_INSTRUCTIONS, N as buildAdvisorStream, P as injectAdvisorTool, R as repairKnownThinkingHistory, Rt as generateRandomPort, U as logStreamError, Ut as withInstallLock, V as buildOpenAIErrorEvent, Vt as upstreamMaxConnections, W as readIteratorWithTimeout, _t as MAX_RESPONSE_BODY_BYTES, ct as shimDefaultsToXhigh, dt as getTokenCount, ft as assembleResponsesPayload, gt as createChatCompletions, ht as createResponses, j as ADVISOR_INTERNAL_TOOL_NAME, jt as toolbeltPathOverride, lt as countTokens, mt as pickEndpoint, pt as resolveMcpToolTimeoutMs, q as handleMcpPost, r as assertMcpToolSurfaceConsistent, ut as createMessages, vt as readResponseBodyCapped, w as toolbeltEnabled, yt as parseJsonOrDiagnose, z as repairRejectedThinkingHistory } from "./peer-mcp-personas-DAZU1OfN.js";
|
|
2
2
|
import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
|
|
3
3
|
import { i as ensurePaths, t as PATHS } from "./paths-Bg1_DWhi.js";
|
|
4
|
-
import { A as state, T as copilotHeaders, _ as forwardError, a as cacheCopilotVersion, c as filterBetaHeader, d as resolveModel, f as sleep, g as HTTPError, i as tryRefreshAndRetry, l as isNullish, n as setupGitHubAgentToken, o as cacheModels, r as setupGitHubToken, s as cacheVSCodeVersion, t as setupCopilotToken, w as copilotBaseUrl, y as fetchWithTransientRetry } from "./token-
|
|
5
|
-
import { t as getCopilotUsage } from "./get-copilot-usage-
|
|
4
|
+
import { A as state, T as copilotHeaders, _ as forwardError, a as cacheCopilotVersion, c as filterBetaHeader, d as resolveModel, f as sleep, g as HTTPError, i as tryRefreshAndRetry, l as isNullish, n as setupGitHubAgentToken, o as cacheModels, r as setupGitHubToken, s as cacheVSCodeVersion, t as setupCopilotToken, w as copilotBaseUrl, y as fetchWithTransientRetry } from "./token-Bz7iN8US.js";
|
|
5
|
+
import { t as getCopilotUsage } from "./get-copilot-usage-nZXbvSi8.js";
|
|
6
6
|
import { a as resolveExecutable, n as killManagedTree, o as runCommandCapture, r as parseBoolEnv, s as runCommandVoid } from "./exec-y8C_MU8A.js";
|
|
7
7
|
import consola from "consola";
|
|
8
8
|
import * as fs$2 from "node:fs";
|
|
9
9
|
import fs, { existsSync } from "node:fs";
|
|
10
10
|
import * as nodePath from "node:path";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { createHash,
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import fs$1 from "node:fs/promises";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import process$1 from "node:process";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
|
-
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
17
|
+
import { Agent, ProxyAgent, buildConnector, setGlobalDispatcher } from "undici";
|
|
18
18
|
import { Writable } from "node:stream";
|
|
19
19
|
import { serve } from "srvx";
|
|
20
20
|
import { createBodyTooLargeError, limitRequestBody } from "srvx/body-limit";
|
|
@@ -1081,10 +1081,79 @@ function wireDaemonTeardown(handle, opts = {}) {
|
|
|
1081
1081
|
}
|
|
1082
1082
|
//#endregion
|
|
1083
1083
|
//#region src/lib/proxy.ts
|
|
1084
|
+
let socketCounter = 0;
|
|
1085
|
+
/**
|
|
1086
|
+
* Wrap undici's connector so every upstream socket records its identity.
|
|
1087
|
+
* Purely observational: it forwards the original callback unchanged, and any
|
|
1088
|
+
* failure to read a property must not break connection setup.
|
|
1089
|
+
*
|
|
1090
|
+
* `allowH2` MUST be threaded in here, not just onto the Agent. ALPN is chosen
|
|
1091
|
+
* by the connector (`undici/lib/core/connect.js` — `ALPNProtocols: allowH2 ?
|
|
1092
|
+
* ['h2','http/1.1'] : ['http/1.1']`), so supplying a custom `connect` built
|
|
1093
|
+
* from `buildConnector({})` silently restores h2 no matter what the Agent
|
|
1094
|
+
* says. Verified against the live upstream, which negotiated h2 with
|
|
1095
|
+
* `allowH2:false` on the Agent until this argument was added.
|
|
1096
|
+
*/
|
|
1097
|
+
function instrumentedConnector(allowH2) {
|
|
1098
|
+
const base = buildConnector({ allowH2 });
|
|
1099
|
+
return ((options, callback) => {
|
|
1100
|
+
return base(options, (err, socket) => {
|
|
1101
|
+
if (err || !socket) {
|
|
1102
|
+
callback(err ?? /* @__PURE__ */ new Error("upstream connect failed"), null);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
const tls = socket;
|
|
1107
|
+
++socketCounter, String(options.hostname ?? ""), typeof tls.alpnProtocol === "string" && tls.alpnProtocol, tls.isSessionReused?.();
|
|
1108
|
+
} catch {}
|
|
1109
|
+
callback(null, socket);
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Options every upstream dispatcher this module builds must carry, so the
|
|
1115
|
+
* direct agent and the per-proxy agents cannot drift apart on transport policy.
|
|
1116
|
+
*/
|
|
1117
|
+
function upstreamAgentOptions() {
|
|
1118
|
+
return {
|
|
1119
|
+
allowH2: upstreamAllowH2(),
|
|
1120
|
+
connections: upstreamMaxConnections()
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Pin the transport policy for every upstream `fetch()` in the process.
|
|
1125
|
+
*
|
|
1126
|
+
* This runs UNCONDITIONALLY, unlike `initProxyFromEnv` below, which is gated on
|
|
1127
|
+
* `--proxy-env` (default false) and therefore never executes in a normal
|
|
1128
|
+
* launch. That gating is why an earlier attempt to set `allowH2` on the agent
|
|
1129
|
+
* inside `initProxyFromEnv` would have changed nothing in production: the
|
|
1130
|
+
* dispatcher actually serving `/v1/messages` is the one undici installs for
|
|
1131
|
+
* itself on import (`lib/global.js`), not anything this module built.
|
|
1132
|
+
*
|
|
1133
|
+
* No-op under Bun, which does not route `fetch()` through undici dispatchers
|
|
1134
|
+
* and negotiates HTTP/1.1 regardless.
|
|
1135
|
+
*/
|
|
1136
|
+
function initUpstreamTransport() {
|
|
1137
|
+
if (typeof Bun !== "undefined") return;
|
|
1138
|
+
try {
|
|
1139
|
+
const options = upstreamAgentOptions();
|
|
1140
|
+
setGlobalDispatcher(new Agent({
|
|
1141
|
+
...options,
|
|
1142
|
+
connect: instrumentedConnector(options.allowH2)
|
|
1143
|
+
}));
|
|
1144
|
+
consola.debug(`Upstream transport: allowH2=${options.allowH2} connections=${options.connections}`);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
consola.debug("Upstream transport setup skipped:", err);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1084
1149
|
function initProxyFromEnv() {
|
|
1085
1150
|
if (typeof Bun !== "undefined") return;
|
|
1086
1151
|
try {
|
|
1087
|
-
const
|
|
1152
|
+
const options = upstreamAgentOptions();
|
|
1153
|
+
const direct = new Agent({
|
|
1154
|
+
...options,
|
|
1155
|
+
connect: instrumentedConnector(options.allowH2)
|
|
1156
|
+
});
|
|
1088
1157
|
const proxies = /* @__PURE__ */ new Map();
|
|
1089
1158
|
setGlobalDispatcher({
|
|
1090
1159
|
dispatch(options, handler) {
|
|
@@ -1098,7 +1167,10 @@ function initProxyFromEnv() {
|
|
|
1098
1167
|
}
|
|
1099
1168
|
let agent = proxies.get(proxyUrl);
|
|
1100
1169
|
if (!agent) {
|
|
1101
|
-
agent = new ProxyAgent(
|
|
1170
|
+
agent = new ProxyAgent({
|
|
1171
|
+
uri: proxyUrl,
|
|
1172
|
+
...upstreamAgentOptions()
|
|
1173
|
+
});
|
|
1102
1174
|
proxies.set(proxyUrl, agent);
|
|
1103
1175
|
}
|
|
1104
1176
|
let label = proxyUrl;
|
|
@@ -1357,7 +1429,7 @@ function collectToolFieldKeys(body) {
|
|
|
1357
1429
|
//#endregion
|
|
1358
1430
|
//#region package.json
|
|
1359
1431
|
var name = "github-router";
|
|
1360
|
-
var version = "0.3.
|
|
1432
|
+
var version = "0.3.273";
|
|
1361
1433
|
//#endregion
|
|
1362
1434
|
//#region src/lib/approval.ts
|
|
1363
1435
|
const awaitApproval = async () => {
|
|
@@ -1409,6 +1481,471 @@ async function doCheck(state, ticket) {
|
|
|
1409
1481
|
consola.info("Rate limit wait completed, proceeding with request");
|
|
1410
1482
|
}
|
|
1411
1483
|
//#endregion
|
|
1484
|
+
//#region src/lib/tool-loop-guard.ts
|
|
1485
|
+
/**
|
|
1486
|
+
* Tool results a CLIENT emits to say "you already made this exact call". These
|
|
1487
|
+
* are the strongest loop signal available: the client has already adjudicated
|
|
1488
|
+
* that the call was redundant, so no inference is needed and there is no
|
|
1489
|
+
* polling ambiguity (a status poll never produces one of these).
|
|
1490
|
+
*
|
|
1491
|
+
* Matched by WHOLE-RESULT equality, never substring — the text below also lives
|
|
1492
|
+
* in this repository's docs and in stored session transcripts, so a substring
|
|
1493
|
+
* match would fire on an agent reading its own project. `tests/canaries/`
|
|
1494
|
+
* pins this set against the installed client so a reword fails loudly instead
|
|
1495
|
+
* of silently switching Tier A off.
|
|
1496
|
+
*/
|
|
1497
|
+
const CLIENT_REDUNDANCY_MARKERS = /* @__PURE__ */ new Set(["Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead."]);
|
|
1498
|
+
const DEFAULT_NUDGE_AT = 4;
|
|
1499
|
+
const DEFAULT_ABORT_AT = 7;
|
|
1500
|
+
/**
|
|
1501
|
+
* Results are compared by equality, and a single `read` can return megabytes.
|
|
1502
|
+
* Anything longer than this is compared by digest instead so a comparison stays
|
|
1503
|
+
* cheap and bounded; below it the raw text is kept so short results stay
|
|
1504
|
+
* readable in a failing test.
|
|
1505
|
+
*/
|
|
1506
|
+
const RESULT_DIGEST_THRESHOLD = 4096;
|
|
1507
|
+
/**
|
|
1508
|
+
* Strict env parse. Modelled on `envInt` in `~/lib/port.ts` (digits only, warn
|
|
1509
|
+
* rather than silently misconfigure) with one deliberate difference: `0` is a
|
|
1510
|
+
* MEANINGFUL value here — it disables a stage — so it is accepted instead of
|
|
1511
|
+
* being treated as absent.
|
|
1512
|
+
*/
|
|
1513
|
+
function envThreshold(key, fallback) {
|
|
1514
|
+
const raw = process.env[key];
|
|
1515
|
+
if (!raw) return fallback;
|
|
1516
|
+
if (!/^[0-9]+$/.test(raw.trim())) {
|
|
1517
|
+
consola.warn(`${key}=${JSON.stringify(raw)} is not a non-negative integer; using fallback ${fallback}`);
|
|
1518
|
+
return fallback;
|
|
1519
|
+
}
|
|
1520
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
1521
|
+
return Number.isSafeInteger(parsed) ? parsed : fallback;
|
|
1522
|
+
}
|
|
1523
|
+
function loopNudgeAt() {
|
|
1524
|
+
return envThreshold("GH_ROUTER_LOOP_NUDGE_AT", DEFAULT_NUDGE_AT);
|
|
1525
|
+
}
|
|
1526
|
+
function loopAbortAt() {
|
|
1527
|
+
return envThreshold("GH_ROUTER_LOOP_ABORT_AT", DEFAULT_ABORT_AT);
|
|
1528
|
+
}
|
|
1529
|
+
const NO_LOOP = {
|
|
1530
|
+
action: "none",
|
|
1531
|
+
tier: null,
|
|
1532
|
+
repeats: 0
|
|
1533
|
+
};
|
|
1534
|
+
function isRecord(value) {
|
|
1535
|
+
return typeof value === "object" && value !== null;
|
|
1536
|
+
}
|
|
1537
|
+
function digest(value) {
|
|
1538
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
1539
|
+
}
|
|
1540
|
+
function boundedText(value) {
|
|
1541
|
+
return value.length > RESULT_DIGEST_THRESHOLD ? `#${digest(value)}` : value;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Canonical string for a tool result.
|
|
1545
|
+
*
|
|
1546
|
+
* Under-specifying this is a correctness bug in both directions: dropping
|
|
1547
|
+
* non-text blocks makes DIFFERENT results compare equal (an image that changed
|
|
1548
|
+
* every turn would look like a loop), and serializing whole wire objects makes
|
|
1549
|
+
* IDENTICAL results compare unequal (a re-encrypted id would mask a real loop).
|
|
1550
|
+
* So every block contributes, identified by type, and `is_error` participates —
|
|
1551
|
+
* a call that starts failing is not the same result as one that succeeded.
|
|
1552
|
+
*/
|
|
1553
|
+
function normalizeResult(content, isError) {
|
|
1554
|
+
return `${isError === true ? "E" : "N"}|${normalizeResultBody(content)}`;
|
|
1555
|
+
}
|
|
1556
|
+
function normalizeResultBody(content) {
|
|
1557
|
+
if (content === null || content === void 0) return "∅";
|
|
1558
|
+
if (typeof content === "string") return `s:${boundedText(content)}`;
|
|
1559
|
+
if (Array.isArray(content)) return content.map((block) => normalizeResultBlock(block)).join("");
|
|
1560
|
+
return `j:${boundedText(safeJson(content))}`;
|
|
1561
|
+
}
|
|
1562
|
+
function normalizeResultBlock(block) {
|
|
1563
|
+
if (typeof block === "string") return `s:${boundedText(block)}`;
|
|
1564
|
+
if (!isRecord(block)) return `j:${boundedText(safeJson(block))}`;
|
|
1565
|
+
const type = typeof block.type === "string" ? block.type : "unknown";
|
|
1566
|
+
if (type === "text" && typeof block.text === "string") return `t:${boundedText(block.text)}`;
|
|
1567
|
+
return `${type}:${digest(safeJson(block))}`;
|
|
1568
|
+
}
|
|
1569
|
+
function safeJson(value) {
|
|
1570
|
+
try {
|
|
1571
|
+
return JSON.stringify(value) ?? "";
|
|
1572
|
+
} catch {
|
|
1573
|
+
return "";
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
function argsKeyFor(name, args) {
|
|
1577
|
+
return `${name}:${safeJson(args)}`;
|
|
1578
|
+
}
|
|
1579
|
+
function buildCall(name, args, content, isError) {
|
|
1580
|
+
const raw = typeof content === "string" ? content : void 0;
|
|
1581
|
+
let cached;
|
|
1582
|
+
return {
|
|
1583
|
+
name,
|
|
1584
|
+
argsKey: argsKeyFor(name, args),
|
|
1585
|
+
resultKey: () => cached ??= normalizeResult(content, isError),
|
|
1586
|
+
isMarker: raw !== void 0 && CLIENT_REDUNDANCY_MARKERS.has(raw)
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Signature of a whole turn: every (call, result) pair, sorted so the order the
|
|
1591
|
+
* client happened to serialize parallel calls and their results in cannot
|
|
1592
|
+
* change the answer.
|
|
1593
|
+
*/
|
|
1594
|
+
function turnSignature(turn) {
|
|
1595
|
+
return turn.calls.map((call) => `${call.argsKey}${call.resultKey()}`).sort().join("");
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Decide whether the tail of a conversation is a stuck tool loop.
|
|
1599
|
+
*
|
|
1600
|
+
* Compares WHOLE TURNS, not individual calls. Flattening a parallel-tool turn
|
|
1601
|
+
* into a call sequence is wrong in both directions: a turn issuing calls A and
|
|
1602
|
+
* B flattens to `A,B,A,B,…`, where no two neighbours match, so a repeating
|
|
1603
|
+
* parallel batch never registers; and seven identical calls inside ONE turn
|
|
1604
|
+
* look like a run of seven even though the model has observed no results at all
|
|
1605
|
+
* and completed zero feedback cycles.
|
|
1606
|
+
*
|
|
1607
|
+
* Only an immediately-repeating turn counts. A cycle with a period longer than
|
|
1608
|
+
* one turn (turn A, turn B, turn A, …) is NOT detected — a deliberate scope
|
|
1609
|
+
* limit, matching the single-slot reset semantics of the in-process worker
|
|
1610
|
+
* guard rather than trying to be a general cycle finder.
|
|
1611
|
+
*/
|
|
1612
|
+
function detectToolLoop(turns, options = {}) {
|
|
1613
|
+
const nudgeAt = options.nudgeAt ?? loopNudgeAt();
|
|
1614
|
+
const abortAt = options.abortAt ?? loopAbortAt();
|
|
1615
|
+
const active = [nudgeAt, abortAt].filter((n) => n > 0);
|
|
1616
|
+
if (active.length === 0 || turns.length === 0) return NO_LOOP;
|
|
1617
|
+
const limit = Math.max(...active);
|
|
1618
|
+
const last = turns[turns.length - 1];
|
|
1619
|
+
if (!last || last.calls.length === 0) return NO_LOOP;
|
|
1620
|
+
const signature = turnSignature(last);
|
|
1621
|
+
const run = [last];
|
|
1622
|
+
for (let i = turns.length - 2; i >= 0 && run.length < limit; i--) {
|
|
1623
|
+
const turn = turns[i];
|
|
1624
|
+
if (!turn || turn.calls.length === 0) break;
|
|
1625
|
+
if (turnSignature(turn) !== signature) break;
|
|
1626
|
+
run.push(turn);
|
|
1627
|
+
}
|
|
1628
|
+
const repeats = run.length;
|
|
1629
|
+
const toolName = uniqueToolName(last);
|
|
1630
|
+
const markerRun = run.every((turn) => turn.calls.every((call) => call.isMarker));
|
|
1631
|
+
if (abortAt > 0 && repeats >= abortAt) {
|
|
1632
|
+
if (markerRun) return {
|
|
1633
|
+
action: "abort",
|
|
1634
|
+
tier: "A",
|
|
1635
|
+
repeats,
|
|
1636
|
+
toolName
|
|
1637
|
+
};
|
|
1638
|
+
if (!run.some((turn) => turn.hasNarration)) return {
|
|
1639
|
+
action: "abort",
|
|
1640
|
+
tier: "B",
|
|
1641
|
+
repeats,
|
|
1642
|
+
toolName
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
if (nudgeAt > 0 && repeats >= nudgeAt) return {
|
|
1646
|
+
action: "nudge",
|
|
1647
|
+
tier: markerRun ? "A" : "B",
|
|
1648
|
+
repeats,
|
|
1649
|
+
toolName
|
|
1650
|
+
};
|
|
1651
|
+
return NO_LOOP;
|
|
1652
|
+
}
|
|
1653
|
+
function uniqueToolName(turn) {
|
|
1654
|
+
const names = new Set(turn.calls.map((call) => call.name));
|
|
1655
|
+
return names.size === 1 ? [...names][0] : void 0;
|
|
1656
|
+
}
|
|
1657
|
+
/** Text injected as a sibling block when a loop is suspected but not certain. */
|
|
1658
|
+
function nudgeText(verdict) {
|
|
1659
|
+
return `[github-router] The same ${verdict.toolName ? `\`${verdict.toolName}\` call` : "tool call"} has now been repeated ${verdict.repeats}× in a row with an identical result. Nothing has changed and nothing will change by repeating it. Use the result you already have, try a materially different approach, or stop and report what you found.`;
|
|
1660
|
+
}
|
|
1661
|
+
/** Message returned to the client when the loop is aborted. */
|
|
1662
|
+
function abortMessage(verdict) {
|
|
1663
|
+
return `Request blocked by github-router: the same ${verdict.toolName ? `\`${verdict.toolName}\` call` : "tool call"} has been repeated ${verdict.repeats}× consecutively with an identical result and no intervening progress. Continuing would burn inference with no possible change in outcome. Vary the call or end the turn. Tune or disable this with GH_ROUTER_LOOP_ABORT_AT.`;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Cheap pre-check so a request with no tool traffic never pays for a parse.
|
|
1667
|
+
* Substring test against the raw body: false positives merely cost the parse
|
|
1668
|
+
* the guard would have done anyway, and there are no false negatives because
|
|
1669
|
+
* every wire format spells its result marker literally.
|
|
1670
|
+
*/
|
|
1671
|
+
function mayContainToolTraffic(rawBody) {
|
|
1672
|
+
return rawBody.includes("tool_result") || rawBody.includes("tool_call_id") || rawBody.includes("function_call_output");
|
|
1673
|
+
}
|
|
1674
|
+
function blockType(block) {
|
|
1675
|
+
return isRecord(block) && typeof block.type === "string" ? block.type : void 0;
|
|
1676
|
+
}
|
|
1677
|
+
/** Anthropic Messages: assistant `tool_use` blocks ↔ user `tool_result` blocks. */
|
|
1678
|
+
function extractAnthropicTurns(body) {
|
|
1679
|
+
if (!isRecord(body) || !Array.isArray(body.messages)) return [];
|
|
1680
|
+
const turns = [];
|
|
1681
|
+
for (let i = 0; i < body.messages.length; i++) {
|
|
1682
|
+
const message = body.messages[i];
|
|
1683
|
+
if (!isRecord(message) || message.role !== "assistant") continue;
|
|
1684
|
+
if (!Array.isArray(message.content)) continue;
|
|
1685
|
+
const uses = message.content.filter((b) => blockType(b) === "tool_use");
|
|
1686
|
+
if (uses.length === 0) {
|
|
1687
|
+
turns.push({
|
|
1688
|
+
calls: [],
|
|
1689
|
+
hasNarration: anthropicHasNarration(message.content)
|
|
1690
|
+
});
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const results = /* @__PURE__ */ new Map();
|
|
1694
|
+
for (let j = i + 1; j < body.messages.length; j++) {
|
|
1695
|
+
const next = body.messages[j];
|
|
1696
|
+
if (!isRecord(next) || next.role !== "user") break;
|
|
1697
|
+
if (!Array.isArray(next.content)) break;
|
|
1698
|
+
for (const block of next.content) {
|
|
1699
|
+
if (!isRecord(block) || blockType(block) !== "tool_result") continue;
|
|
1700
|
+
const id = block.tool_use_id;
|
|
1701
|
+
if (typeof id === "string") results.set(id, block);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const calls = [];
|
|
1705
|
+
for (const use of uses) {
|
|
1706
|
+
if (!isRecord(use)) continue;
|
|
1707
|
+
const name = typeof use.name === "string" ? use.name : "unknown";
|
|
1708
|
+
const id = typeof use.id === "string" ? use.id : void 0;
|
|
1709
|
+
const result = id === void 0 ? void 0 : results.get(id);
|
|
1710
|
+
if (!result) continue;
|
|
1711
|
+
calls.push(buildCall(name, use.input, result.content, result.is_error));
|
|
1712
|
+
}
|
|
1713
|
+
if (calls.length === 0) continue;
|
|
1714
|
+
turns.push({
|
|
1715
|
+
calls,
|
|
1716
|
+
hasNarration: anthropicHasNarration(message.content)
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
return turns;
|
|
1720
|
+
}
|
|
1721
|
+
function anthropicHasNarration(content) {
|
|
1722
|
+
return content.some((block) => {
|
|
1723
|
+
const type = blockType(block);
|
|
1724
|
+
if (type === "thinking" || type === "redacted_thinking") return true;
|
|
1725
|
+
return type === "text" && isRecord(block) && typeof block.text === "string" && block.text.trim() !== "";
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
/** OpenAI Chat Completions: assistant `tool_calls[]` ↔ `role:"tool"` messages. */
|
|
1729
|
+
function extractChatTurns(body) {
|
|
1730
|
+
if (!isRecord(body) || !Array.isArray(body.messages)) return [];
|
|
1731
|
+
const turns = [];
|
|
1732
|
+
for (let i = 0; i < body.messages.length; i++) {
|
|
1733
|
+
const message = body.messages[i];
|
|
1734
|
+
if (!isRecord(message) || message.role !== "assistant") continue;
|
|
1735
|
+
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
|
|
1736
|
+
turns.push({
|
|
1737
|
+
calls: [],
|
|
1738
|
+
hasNarration: chatHasNarration(message.content)
|
|
1739
|
+
});
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const results = /* @__PURE__ */ new Map();
|
|
1743
|
+
for (let j = i + 1; j < body.messages.length; j++) {
|
|
1744
|
+
const next = body.messages[j];
|
|
1745
|
+
if (!isRecord(next) || next.role !== "tool") break;
|
|
1746
|
+
const id = next.tool_call_id;
|
|
1747
|
+
if (typeof id === "string") results.set(id, next);
|
|
1748
|
+
}
|
|
1749
|
+
const calls = [];
|
|
1750
|
+
for (const call of message.tool_calls) {
|
|
1751
|
+
if (!isRecord(call)) continue;
|
|
1752
|
+
const fn = isRecord(call.function) ? call.function : void 0;
|
|
1753
|
+
const name = typeof fn?.name === "string" ? fn.name : "unknown";
|
|
1754
|
+
const id = typeof call.id === "string" ? call.id : void 0;
|
|
1755
|
+
const result = id === void 0 ? void 0 : results.get(id);
|
|
1756
|
+
if (!result) continue;
|
|
1757
|
+
calls.push(buildCall(name, fn?.arguments, result.content));
|
|
1758
|
+
}
|
|
1759
|
+
if (calls.length === 0) continue;
|
|
1760
|
+
turns.push({
|
|
1761
|
+
calls,
|
|
1762
|
+
hasNarration: chatHasNarration(message.content)
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
return turns;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* OpenAI allows assistant `content` to be either a string or an array of
|
|
1769
|
+
* multimodal parts. Treating only the string form as narration would classify
|
|
1770
|
+
* an array-narrating model as silent and expose it to the Tier B abort.
|
|
1771
|
+
*/
|
|
1772
|
+
function chatHasNarration(content) {
|
|
1773
|
+
if (typeof content === "string") return content.trim() !== "";
|
|
1774
|
+
if (!Array.isArray(content)) return false;
|
|
1775
|
+
return content.some((part) => isRecord(part) && typeof part.text === "string" && part.text.trim() !== "");
|
|
1776
|
+
}
|
|
1777
|
+
/** OpenAI Responses: `function_call` items ↔ `function_call_output` items. */
|
|
1778
|
+
function extractResponsesTurns(body) {
|
|
1779
|
+
if (!isRecord(body) || !Array.isArray(body.input)) return [];
|
|
1780
|
+
const items = body.input;
|
|
1781
|
+
const results = /* @__PURE__ */ new Map();
|
|
1782
|
+
for (const item of items) {
|
|
1783
|
+
if (!isRecord(item) || item.type !== "function_call_output") continue;
|
|
1784
|
+
const id = item.call_id;
|
|
1785
|
+
if (typeof id === "string") results.set(id, item);
|
|
1786
|
+
}
|
|
1787
|
+
const turns = [];
|
|
1788
|
+
let i = 0;
|
|
1789
|
+
while (i < items.length) {
|
|
1790
|
+
const item = items[i];
|
|
1791
|
+
if (!isRecord(item) || item.type !== "function_call") {
|
|
1792
|
+
i++;
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const batch = [];
|
|
1796
|
+
while (i < items.length) {
|
|
1797
|
+
const candidate = items[i];
|
|
1798
|
+
if (!isRecord(candidate) || candidate.type !== "function_call") break;
|
|
1799
|
+
batch.push(candidate);
|
|
1800
|
+
i++;
|
|
1801
|
+
}
|
|
1802
|
+
const calls = [];
|
|
1803
|
+
for (const call of batch) {
|
|
1804
|
+
const name = typeof call.name === "string" ? call.name : "unknown";
|
|
1805
|
+
const id = typeof call.call_id === "string" ? call.call_id : void 0;
|
|
1806
|
+
const result = id === void 0 ? void 0 : results.get(id);
|
|
1807
|
+
if (!result) continue;
|
|
1808
|
+
calls.push(buildCall(name, call.arguments, result.output));
|
|
1809
|
+
}
|
|
1810
|
+
if (calls.length === 0) continue;
|
|
1811
|
+
turns.push({
|
|
1812
|
+
calls,
|
|
1813
|
+
hasNarration: responsesHasNarration(items, i - batch.length)
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return turns;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Responses has no turn envelope, so narration is whatever the model emitted
|
|
1820
|
+
* immediately before the call batch: an assistant/message item or a reasoning
|
|
1821
|
+
* item.
|
|
1822
|
+
*/
|
|
1823
|
+
function responsesHasNarration(items, batchStart) {
|
|
1824
|
+
for (let i = batchStart - 1; i >= 0; i--) {
|
|
1825
|
+
const item = items[i];
|
|
1826
|
+
if (!isRecord(item)) return false;
|
|
1827
|
+
if (item.type === "function_call_output") continue;
|
|
1828
|
+
if (item.type === "reasoning") return true;
|
|
1829
|
+
if (item.type === "message" || item.role === "assistant") return responsesItemHasText(item);
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
function responsesItemHasText(item) {
|
|
1835
|
+
if (typeof item.content === "string") return item.content.trim() !== "";
|
|
1836
|
+
if (!Array.isArray(item.content)) return false;
|
|
1837
|
+
return item.content.some((block) => isRecord(block) && typeof block.text === "string" && block.text.trim() !== "");
|
|
1838
|
+
}
|
|
1839
|
+
function injectAnthropicNudge(body, text) {
|
|
1840
|
+
if (!isRecord(body) || !Array.isArray(body.messages)) return false;
|
|
1841
|
+
const messages = body.messages;
|
|
1842
|
+
const last = messages[messages.length - 1];
|
|
1843
|
+
if (!isRecord(last) || last.role !== "user") return false;
|
|
1844
|
+
if (!Array.isArray(last.content)) return false;
|
|
1845
|
+
messages[messages.length - 1] = {
|
|
1846
|
+
...last,
|
|
1847
|
+
content: [...last.content, {
|
|
1848
|
+
type: "text",
|
|
1849
|
+
text
|
|
1850
|
+
}]
|
|
1851
|
+
};
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
function injectChatNudge(body, text) {
|
|
1855
|
+
if (!isRecord(body) || !Array.isArray(body.messages)) return false;
|
|
1856
|
+
body.messages = [...body.messages, {
|
|
1857
|
+
role: "user",
|
|
1858
|
+
content: text
|
|
1859
|
+
}];
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
function injectResponsesNudge(body, text) {
|
|
1863
|
+
if (!isRecord(body) || !Array.isArray(body.input)) return false;
|
|
1864
|
+
body.input = [...body.input, {
|
|
1865
|
+
role: "user",
|
|
1866
|
+
content: [{
|
|
1867
|
+
type: "input_text",
|
|
1868
|
+
text
|
|
1869
|
+
}]
|
|
1870
|
+
}];
|
|
1871
|
+
return true;
|
|
1872
|
+
}
|
|
1873
|
+
const NO_ACTION = {
|
|
1874
|
+
action: "none",
|
|
1875
|
+
verdict: NO_LOOP
|
|
1876
|
+
};
|
|
1877
|
+
function report(verdict, route) {
|
|
1878
|
+
consola.warn(`[LOOP-GUARD] ${verdict.action} on ${route}: tier=${verdict.tier} repeats=${verdict.repeats} tool=${verdict.toolName ?? "multiple"}`);
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Anthropic Messages. Takes and returns the raw body string because that path
|
|
1882
|
+
* is otherwise a passthrough; the body is re-serialized ONLY when a nudge is
|
|
1883
|
+
* injected, mirroring `resolveModelInBody`'s `modified ? stringify : raw`
|
|
1884
|
+
* contract so detection alone can never perturb values JavaScript cannot round
|
|
1885
|
+
* -trip exactly (`9007199254740993` becomes `…992`).
|
|
1886
|
+
*
|
|
1887
|
+
* Injecting a nudge inherently requires re-serializing, so on the rare request
|
|
1888
|
+
* that would otherwise pass through raw a nudge does extend that exposure. It
|
|
1889
|
+
* is not a NEW class of hazard — `resolveModelInBody` already re-serializes
|
|
1890
|
+
* whenever it changed anything, which for Claude Code is essentially every
|
|
1891
|
+
* request, since the model slug is rewritten.
|
|
1892
|
+
*/
|
|
1893
|
+
function guardAnthropicBody(rawBody) {
|
|
1894
|
+
if (!mayContainToolTraffic(rawBody)) return NO_ACTION;
|
|
1895
|
+
let parsed;
|
|
1896
|
+
try {
|
|
1897
|
+
parsed = JSON.parse(rawBody);
|
|
1898
|
+
} catch {
|
|
1899
|
+
return NO_ACTION;
|
|
1900
|
+
}
|
|
1901
|
+
const verdict = detectToolLoop(extractAnthropicTurns(parsed));
|
|
1902
|
+
if (verdict.action === "none") return NO_ACTION;
|
|
1903
|
+
report(verdict, "/v1/messages");
|
|
1904
|
+
if (verdict.action === "abort") return {
|
|
1905
|
+
action: "abort",
|
|
1906
|
+
message: abortMessage(verdict),
|
|
1907
|
+
verdict
|
|
1908
|
+
};
|
|
1909
|
+
if (!injectAnthropicNudge(parsed, nudgeText(verdict))) return NO_ACTION;
|
|
1910
|
+
return {
|
|
1911
|
+
action: "nudge",
|
|
1912
|
+
body: JSON.stringify(parsed),
|
|
1913
|
+
verdict
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
/** OpenAI Chat Completions. Mutates the already-parsed payload in place. */
|
|
1917
|
+
function guardChatPayload(payload) {
|
|
1918
|
+
const verdict = detectToolLoop(extractChatTurns(payload));
|
|
1919
|
+
if (verdict.action === "none") return NO_ACTION;
|
|
1920
|
+
report(verdict, "/v1/chat/completions");
|
|
1921
|
+
if (verdict.action === "abort") return {
|
|
1922
|
+
action: "abort",
|
|
1923
|
+
message: abortMessage(verdict),
|
|
1924
|
+
verdict
|
|
1925
|
+
};
|
|
1926
|
+
if (!injectChatNudge(payload, nudgeText(verdict))) return NO_ACTION;
|
|
1927
|
+
return {
|
|
1928
|
+
action: "nudge",
|
|
1929
|
+
verdict
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
/** OpenAI Responses. Mutates the already-parsed payload in place. */
|
|
1933
|
+
function guardResponsesPayload(payload) {
|
|
1934
|
+
const verdict = detectToolLoop(extractResponsesTurns(payload));
|
|
1935
|
+
if (verdict.action === "none") return NO_ACTION;
|
|
1936
|
+
report(verdict, "/v1/responses");
|
|
1937
|
+
if (verdict.action === "abort") return {
|
|
1938
|
+
action: "abort",
|
|
1939
|
+
message: abortMessage(verdict),
|
|
1940
|
+
verdict
|
|
1941
|
+
};
|
|
1942
|
+
if (!injectResponsesNudge(payload, nudgeText(verdict))) return NO_ACTION;
|
|
1943
|
+
return {
|
|
1944
|
+
action: "nudge",
|
|
1945
|
+
verdict
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
//#endregion
|
|
1412
1949
|
//#region src/routes/chat-completions/handler.ts
|
|
1413
1950
|
const ENCODER$1 = new TextEncoder();
|
|
1414
1951
|
function formatSSE$1(chunk) {
|
|
@@ -1425,6 +1962,11 @@ async function handleCompletion$1(c) {
|
|
|
1425
1962
|
const debugEnabled = consola.level >= 4;
|
|
1426
1963
|
if (debugEnabled) consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
1427
1964
|
if (state.manualApprove) await awaitApproval();
|
|
1965
|
+
const loopGuard = guardChatPayload(payload);
|
|
1966
|
+
if (loopGuard.action === "abort") return c.json({ error: {
|
|
1967
|
+
type: "invalid_request_error",
|
|
1968
|
+
message: loopGuard.message
|
|
1969
|
+
} }, 400, { "x-should-retry": "false" });
|
|
1428
1970
|
await injectWebSearchIfNeeded$1(payload);
|
|
1429
1971
|
const originalModel = payload.model;
|
|
1430
1972
|
const resolvedModel = resolveModel(payload.model);
|
|
@@ -3654,117 +4196,6 @@ async function handleNonClaudeChat(c, opts) {
|
|
|
3654
4196
|
return c.json(anthropic, 200);
|
|
3655
4197
|
}
|
|
3656
4198
|
//#endregion
|
|
3657
|
-
//#region src/lib/thinking-history-repair.ts
|
|
3658
|
-
const THINKING_INTEGRITY_ERROR_RES = [/messages\.(\d+)\.content\.\d+:\s*`thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified/, /messages\.(\d+)\.content\.\d+:\s*Invalid `signature` in `thinking` block/];
|
|
3659
|
-
const MAX_REMEMBERED_REPAIRS = 1e3;
|
|
3660
|
-
const OMITTED_THINKING_PLACEHOLDER = "[prior reasoning omitted]";
|
|
3661
|
-
const repairSalt = randomBytes(32);
|
|
3662
|
-
const rememberedRepairs = /* @__PURE__ */ new Set();
|
|
3663
|
-
function isRecord(value) {
|
|
3664
|
-
return typeof value === "object" && value !== null;
|
|
3665
|
-
}
|
|
3666
|
-
function signedBlockFingerprint(_messageIndex, content) {
|
|
3667
|
-
const hash = createHash("sha256");
|
|
3668
|
-
hash.update(repairSalt);
|
|
3669
|
-
let signedBlocks = 0;
|
|
3670
|
-
for (let index = 0; index < content.length; index++) {
|
|
3671
|
-
const block = content[index];
|
|
3672
|
-
if (!isRecord(block)) continue;
|
|
3673
|
-
if (block.type !== "thinking" && block.type !== "redacted_thinking") continue;
|
|
3674
|
-
signedBlocks++;
|
|
3675
|
-
hash.update(String(index));
|
|
3676
|
-
hash.update(String(block.type));
|
|
3677
|
-
if (typeof block.signature === "string") hash.update(block.signature);
|
|
3678
|
-
if (typeof block.data === "string") hash.update(block.data);
|
|
3679
|
-
}
|
|
3680
|
-
return signedBlocks > 0 ? hash.digest("hex") : void 0;
|
|
3681
|
-
}
|
|
3682
|
-
function repairMessageAt(parsed, messageIndex) {
|
|
3683
|
-
if (!Array.isArray(parsed.messages)) return void 0;
|
|
3684
|
-
const messages = [...parsed.messages];
|
|
3685
|
-
const message = messages[messageIndex];
|
|
3686
|
-
if (!isRecord(message) || message.role !== "assistant") return void 0;
|
|
3687
|
-
if (!Array.isArray(message.content)) return void 0;
|
|
3688
|
-
const fingerprint = signedBlockFingerprint(messageIndex, message.content);
|
|
3689
|
-
if (!fingerprint) return void 0;
|
|
3690
|
-
const repairedContent = message.content.filter((block) => !isRecord(block) || block.type !== "thinking" && block.type !== "redacted_thinking");
|
|
3691
|
-
const removedBlocks = message.content.length - repairedContent.length;
|
|
3692
|
-
if (removedBlocks === 0) return void 0;
|
|
3693
|
-
if (repairedContent.length === 0) repairedContent.push({
|
|
3694
|
-
type: "text",
|
|
3695
|
-
text: OMITTED_THINKING_PLACEHOLDER
|
|
3696
|
-
});
|
|
3697
|
-
messages[messageIndex] = {
|
|
3698
|
-
...message,
|
|
3699
|
-
content: repairedContent
|
|
3700
|
-
};
|
|
3701
|
-
parsed.messages = messages;
|
|
3702
|
-
return {
|
|
3703
|
-
body: JSON.stringify(parsed),
|
|
3704
|
-
fingerprint,
|
|
3705
|
-
messageIndex,
|
|
3706
|
-
removedBlocks
|
|
3707
|
-
};
|
|
3708
|
-
}
|
|
3709
|
-
/**
|
|
3710
|
-
* Build a one-shot repair only for Copilot's known signed-thinking integrity
|
|
3711
|
-
* rejections. The upstream error is the oracle: valid omitted-display thinking
|
|
3712
|
-
* also has an empty `thinking` string, so request shape alone cannot identify a
|
|
3713
|
-
* corrupt signature safely.
|
|
3714
|
-
*/
|
|
3715
|
-
function repairRejectedThinkingHistory(rawBody, upstreamErrorText) {
|
|
3716
|
-
const match = THINKING_INTEGRITY_ERROR_RES.map((pattern) => pattern.exec(upstreamErrorText)).find((candidate) => candidate !== null);
|
|
3717
|
-
if (!match) return void 0;
|
|
3718
|
-
const messageIndex = Number(match[1]);
|
|
3719
|
-
if (!Number.isSafeInteger(messageIndex) || messageIndex < 0) return void 0;
|
|
3720
|
-
let parsed;
|
|
3721
|
-
try {
|
|
3722
|
-
parsed = JSON.parse(rawBody);
|
|
3723
|
-
} catch {
|
|
3724
|
-
return;
|
|
3725
|
-
}
|
|
3726
|
-
return repairMessageAt(parsed, messageIndex);
|
|
3727
|
-
}
|
|
3728
|
-
/**
|
|
3729
|
-
* Reapply a repair already proven successful in this process. Only salted
|
|
3730
|
-
* digests are retained; no message, thinking, signature, or tool content is
|
|
3731
|
-
* stored.
|
|
3732
|
-
*/
|
|
3733
|
-
function repairKnownThinkingHistory(rawBody) {
|
|
3734
|
-
if (rememberedRepairs.size === 0) return void 0;
|
|
3735
|
-
let parsed;
|
|
3736
|
-
try {
|
|
3737
|
-
parsed = JSON.parse(rawBody);
|
|
3738
|
-
} catch {
|
|
3739
|
-
return;
|
|
3740
|
-
}
|
|
3741
|
-
if (!Array.isArray(parsed.messages)) return void 0;
|
|
3742
|
-
let repaired;
|
|
3743
|
-
let totalRemovedBlocks = 0;
|
|
3744
|
-
for (let index = parsed.messages.length - 1; index >= 0; index--) {
|
|
3745
|
-
const message = parsed.messages[index];
|
|
3746
|
-
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
3747
|
-
const fingerprint = signedBlockFingerprint(index, message.content);
|
|
3748
|
-
if (!fingerprint || !rememberedRepairs.has(fingerprint)) continue;
|
|
3749
|
-
const attempt = repairMessageAt(parsed, index);
|
|
3750
|
-
if (!attempt) continue;
|
|
3751
|
-
totalRemovedBlocks += attempt.removedBlocks;
|
|
3752
|
-
repaired = {
|
|
3753
|
-
...attempt,
|
|
3754
|
-
removedBlocks: totalRemovedBlocks
|
|
3755
|
-
};
|
|
3756
|
-
}
|
|
3757
|
-
return repaired;
|
|
3758
|
-
}
|
|
3759
|
-
function rememberThinkingHistoryRepair(fingerprint) {
|
|
3760
|
-
if (rememberedRepairs.has(fingerprint)) return;
|
|
3761
|
-
if (rememberedRepairs.size >= MAX_REMEMBERED_REPAIRS) {
|
|
3762
|
-
const oldest = rememberedRepairs.values().next().value;
|
|
3763
|
-
if (typeof oldest === "string") rememberedRepairs.delete(oldest);
|
|
3764
|
-
}
|
|
3765
|
-
rememberedRepairs.add(fingerprint);
|
|
3766
|
-
}
|
|
3767
|
-
//#endregion
|
|
3768
4199
|
//#region src/routes/messages/handler.ts
|
|
3769
4200
|
const MAX_THINKING_REPAIR_ATTEMPTS = 5;
|
|
3770
4201
|
const isWebSearchTool$1 = (tool) => !!tool && typeof tool === "object" && (typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search");
|
|
@@ -3931,6 +4362,15 @@ async function handleCompletion(c) {
|
|
|
3931
4362
|
const advisorEnabled = isAdvisorRequested(incomingBeta);
|
|
3932
4363
|
let finalBody = await processWebSearch(rawBody);
|
|
3933
4364
|
finalBody = sanitizeAnthropicBody(finalBody);
|
|
4365
|
+
const loopGuard = guardAnthropicBody(finalBody);
|
|
4366
|
+
if (loopGuard.action === "abort") return c.json({
|
|
4367
|
+
type: "error",
|
|
4368
|
+
error: {
|
|
4369
|
+
type: "invalid_request_error",
|
|
4370
|
+
message: loopGuard.message
|
|
4371
|
+
}
|
|
4372
|
+
}, 400, { "x-should-retry": "false" });
|
|
4373
|
+
if (loopGuard.body !== void 0) finalBody = loopGuard.body;
|
|
3934
4374
|
if (advisorEnabled) {
|
|
3935
4375
|
finalBody = injectAdvisorTool(finalBody);
|
|
3936
4376
|
consola.info("ADVISOR enabled for this request — injecting __anthropic_advisor tool; will translate tool_use → server_tool_use{advisor} on the SSE stream");
|
|
@@ -3985,8 +4425,11 @@ async function handleCompletion(c) {
|
|
|
3985
4425
|
} catch (error) {
|
|
3986
4426
|
if (!(error instanceof HTTPError)) throw error;
|
|
3987
4427
|
const errorBody = await error.response.clone().text().catch(() => "");
|
|
3988
|
-
const
|
|
4428
|
+
const outcome = attempt < MAX_THINKING_REPAIR_ATTEMPTS ? repairRejectedThinkingHistory(nativeBody, errorBody) : void 0;
|
|
4429
|
+
const thinkingRepair = outcome?.ok ? outcome.repair : void 0;
|
|
3989
4430
|
if (!thinkingRepair || repairedMessageIndices.has(thinkingRepair.messageIndex)) {
|
|
4431
|
+
if (outcome && !outcome.ok) consola.warn(`Thinking-history repair declined: ${formatThinkingRepairDecline(outcome.decline)}`);
|
|
4432
|
+
else if (thinkingRepair) consola.warn(`Thinking-history repair made no progress; upstream re-named message=${thinkingRepair.messageIndex} after it was already repaired`);
|
|
3990
4433
|
logRequest({
|
|
3991
4434
|
method: "POST",
|
|
3992
4435
|
path: c.req.path,
|
|
@@ -4563,6 +5006,11 @@ async function handleResponses(c) {
|
|
|
4563
5006
|
const payload = await c.req.json();
|
|
4564
5007
|
const debugEnabled = consola.level >= 4;
|
|
4565
5008
|
if (debugEnabled) consola.debug("Responses request payload:", JSON.stringify(payload).slice(-400));
|
|
5009
|
+
const loopGuard = guardResponsesPayload(payload);
|
|
5010
|
+
if (loopGuard.action === "abort") return c.json({ error: {
|
|
5011
|
+
type: "invalid_request_error",
|
|
5012
|
+
message: loopGuard.message
|
|
5013
|
+
} }, 400, { "x-should-retry": "false" });
|
|
4566
5014
|
const originalModel = payload.model;
|
|
4567
5015
|
const resolvedModel = resolveModel(payload.model);
|
|
4568
5016
|
if (resolvedModel !== payload.model) payload.model = resolvedModel;
|
|
@@ -5119,6 +5567,7 @@ function buildServeOptions(fetchHandler, silent) {
|
|
|
5119
5567
|
};
|
|
5120
5568
|
}
|
|
5121
5569
|
async function setupAndServe(options) {
|
|
5570
|
+
initUpstreamTransport();
|
|
5122
5571
|
if (options.proxyEnv) initProxyFromEnv();
|
|
5123
5572
|
if (options.verbose) {
|
|
5124
5573
|
consola.level = 5;
|
|
@@ -5557,4 +6006,4 @@ function getCodexEnvVars(serverUrl) {
|
|
|
5557
6006
|
//#endregion
|
|
5558
6007
|
export { sharedServerArgs as a, listModelsForEndpoint as c, checkClaudeVersion as d, updateClaude as f, setupAndServe as i, enableFileLogging as l, getCodexEnvVars as n, startKeepAwake as o, parseSharedArgs as r, stopKeepAwake as s, getClaudeCodeEnvVars as t, runSelfUpdate as u };
|
|
5559
6008
|
|
|
5560
|
-
//# sourceMappingURL=server-setup-
|
|
6009
|
+
//# sourceMappingURL=server-setup-I5KYkuxE.js.map
|