github-router 0.3.271 → 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.
Files changed (38) hide show
  1. package/dist/{attribution-settings-DLRQEKpU.js → attribution-settings-4nEQq_tn.js} +3 -3
  2. package/dist/{attribution-settings-DLRQEKpU.js.map → attribution-settings-4nEQq_tn.js.map} +1 -1
  3. package/dist/{auth-DIFbHOu0.js → auth-3so8RT91.js} +2 -2
  4. package/dist/{auth-DIFbHOu0.js.map → auth-3so8RT91.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-DfnrR-d_.js → check-usage-goXyc3qS.js} +3 -3
  7. package/dist/{check-usage-DfnrR-d_.js.map → check-usage-goXyc3qS.js.map} +1 -1
  8. package/dist/{claude-BpQVQE0T.js → claude-BNZD-bQN.js} +7 -7
  9. package/dist/{claude-BpQVQE0T.js.map → claude-BNZD-bQN.js.map} +1 -1
  10. package/dist/{codex-CGMiGJy1.js → codex-C_MA9uy5.js} +5 -5
  11. package/dist/{codex-CGMiGJy1.js.map → codex-C_MA9uy5.js.map} +1 -1
  12. package/dist/engine-CHN8j6BV.js +2 -0
  13. package/dist/{gate-discovery-BBA7cV1D.js → gate-discovery-DvA4xObh.js} +2 -2
  14. package/dist/{gate-discovery-BBA7cV1D.js.map → gate-discovery-DvA4xObh.js.map} +1 -1
  15. package/dist/{get-copilot-usage-Ds09xSmH.js → get-copilot-usage-nZXbvSi8.js} +2 -2
  16. package/dist/{get-copilot-usage-Ds09xSmH.js.map → get-copilot-usage-nZXbvSi8.js.map} +1 -1
  17. package/dist/{internal-stop-hook-CO2NDmGL.js → internal-stop-hook-WVgnwt5U.js} +2 -2
  18. package/dist/{internal-stop-hook-CO2NDmGL.js.map → internal-stop-hook-WVgnwt5U.js.map} +1 -1
  19. package/dist/main.js +8 -8
  20. package/dist/{models-BFQUp0ov.js → models-BmGGH6pP.js} +2 -2
  21. package/dist/{models-BFQUp0ov.js.map → models-BmGGH6pP.js.map} +1 -1
  22. package/dist/{peer-mcp-personas-CN8xaXcV.js → peer-mcp-personas-DAZU1OfN.js} +111 -8
  23. package/dist/peer-mcp-personas-DAZU1OfN.js.map +1 -0
  24. package/dist/{provision-CiPjSx7J.js → provision-C3UIA1Tf.js} +2 -2
  25. package/dist/{provision-CiPjSx7J.js.map → provision-C3UIA1Tf.js.map} +1 -1
  26. package/dist/{serve-CpTMr5D9.js → serve-DPgawWWq.js} +6 -6
  27. package/dist/{serve-CpTMr5D9.js.map → serve-DPgawWWq.js.map} +1 -1
  28. package/dist/{server-setup-B02Myghc.js → server-setup-I5KYkuxE.js} +565 -8
  29. package/dist/server-setup-I5KYkuxE.js.map +1 -0
  30. package/dist/{start-CIV9eXVb.js → start-CWWqjjwm.js} +3 -3
  31. package/dist/{start-CIV9eXVb.js.map → start-CWWqjjwm.js.map} +1 -1
  32. package/dist/{token-iaSGp76k.js → token-Bz7iN8US.js} +74 -6
  33. package/dist/token-Bz7iN8US.js.map +1 -0
  34. package/package.json +2 -1
  35. package/dist/engine-DUJyXWZn.js +0 -2
  36. package/dist/peer-mcp-personas-CN8xaXcV.js.map +0 -1
  37. package/dist/server-setup-B02Myghc.js.map +0 -1
  38. package/dist/token-iaSGp76k.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { A as searchWeb, B as buildAnthropicErrorEvent, Bt as withOneMSuffix, F as isAdvisorRequested, G as relayAnthropicStream, H as isControllerClosedError, 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, V as buildOpenAIErrorEvent, Vt as withInstallLock, 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-CN8xaXcV.js";
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-iaSGp76k.js";
5
- import { t as getCopilotUsage } from "./get-copilot-usage-Ds09xSmH.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-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";
@@ -14,7 +14,7 @@ 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 direct = new Agent();
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(proxyUrl);
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.271";
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);
@@ -3820,6 +4362,15 @@ async function handleCompletion(c) {
3820
4362
  const advisorEnabled = isAdvisorRequested(incomingBeta);
3821
4363
  let finalBody = await processWebSearch(rawBody);
3822
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;
3823
4374
  if (advisorEnabled) {
3824
4375
  finalBody = injectAdvisorTool(finalBody);
3825
4376
  consola.info("ADVISOR enabled for this request — injecting __anthropic_advisor tool; will translate tool_use → server_tool_use{advisor} on the SSE stream");
@@ -4455,6 +5006,11 @@ async function handleResponses(c) {
4455
5006
  const payload = await c.req.json();
4456
5007
  const debugEnabled = consola.level >= 4;
4457
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" });
4458
5014
  const originalModel = payload.model;
4459
5015
  const resolvedModel = resolveModel(payload.model);
4460
5016
  if (resolvedModel !== payload.model) payload.model = resolvedModel;
@@ -5011,6 +5567,7 @@ function buildServeOptions(fetchHandler, silent) {
5011
5567
  };
5012
5568
  }
5013
5569
  async function setupAndServe(options) {
5570
+ initUpstreamTransport();
5014
5571
  if (options.proxyEnv) initProxyFromEnv();
5015
5572
  if (options.verbose) {
5016
5573
  consola.level = 5;
@@ -5449,4 +6006,4 @@ function getCodexEnvVars(serverUrl) {
5449
6006
  //#endregion
5450
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 };
5451
6008
 
5452
- //# sourceMappingURL=server-setup-B02Myghc.js.map
6009
+ //# sourceMappingURL=server-setup-I5KYkuxE.js.map