github-router 0.3.152 → 0.3.162

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/main.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { $ as logStreamError, $t as copilotBaseUrl, At as UPSTREAM_FETCH_TIMEOUT_MS, B as toolbeltEnabled, Bt as cacheCopilotVersion, C as repoFingerprint, Ct as ArtifactClient, D as trustRepo, Dt as DEFAULT_CODEX_MODEL, E as stopReviewStateDir, Et as DEFAULT_CLAUDE_MODEL_FALLBACKS, Ft as withInstallLock, G as searchWeb, Gt as resolveCodexModel, H as vscodeRipgrepPath, Ht as cacheVSCodeVersion, It as setupCopilotToken, J as buildAdvisorStream, Jt as getModels, K as ADVISOR_INTERNAL_TOOL_NAME, Kt as resolveModel, Lt as setupGitHubAgentToken, Mt as generateRandomPort, Nt as pickClaudeDefault, O as resolveSealedGate, Ot as DEFAULT_CODEX_MODEL_FALLBACKS, Pt as getPackageVersion, Q as isControllerClosedError, Qt as GITHUB_API_BASE_URL, R as availableToolCommands, Rt as setupGitHubToken, S as isSubagentContext, St as shouldUseInsecureTls, T as stopGateEnabledForRepo, Tt as toolbeltPathOverride, U as TOOLBELT_TOOLS, Ut as filterBetaHeader, V as toolbeltSkipSet, Vt as cacheModels, W as assetFor, Wt as isNullish, X as isAdvisorRequested, Xt as HTTPError, Y as injectAdvisorTool, Yt as fetchWithTransientRetry, Z as buildOpenAIErrorEvent, Zt as forwardError, _ as stopReviewEnabled, _t as provisionBrowserAssets, a as buildPeerAwarenessSnippet, at as browserToolsEnabled, b as fileLastPromptStore, bt as extractTarGzMember, c as buildSessionBindHookCommand, ct as workerToolsEnabled, d as decideStopHook, dt as getTokenCount, en as copilotHeaders, et as readIteratorWithTimeout, f as fileBlockBudget, ft as createResponses, g as stopGateId, gt as parseJsonOrDiagnose, h as stopGateDisabled, ht as readResponseBodyCapped, i as buildAgentPrompt, it as agentToolsEnabled, jt as UPSTREAM_INACTIVITY_TIMEOUT_MS, k as liveExec, kt as DEFAULT_PORT, l as buildStopHookCommand, lt as countTokens, m as launchBaselineKey, mt as MAX_RESPONSE_BODY_BYTES, n as MCP_GROUPS, nn as state, nt as handleMcpDelete, o as personasFor, ot as fleetToolsEnabled, p as injectStopHookIntoSettingsFile, pt as createChatCompletions, q as ADVISOR_TOOL_INSTRUCTIONS, qt as sleep, r as assertMcpToolSurfaceConsistent, rt as handleMcpPost, s as buildArtifactOpenHookCommand, st as standInToolEnabled, t as GROUP_META, tn as githubHeaders, tt as relayAnthropicStream, u as captureLaunchBaseline, ut as createMessages, v as fileBaselineStore, vt as hasSupportedBrowserInstalled, w as repoRoot, wt as collapsePathKeys, x as fileReviewDebounce, xt as extractZipMember, y as fileFindingsStore, yt as provisionAndIndexColbert, z as buildToolbeltAwareness, zt as tryRefreshAndRetry } from "./peer-mcp-personas-BQVOxB1i.js";
3
- import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeArtifactCredsToMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS, u as writeRuntimeFileSecure } from "./paths-D0tJ_tms.js";
4
- import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-DyEXZu2z.js";
5
- import { a as sweepRegistry } from "./lifecycle-DGvk4z63.js";
2
+ import { $ as buildOpenAIErrorEvent, $t as resolveModel, At as ArtifactClient, B as buildToolbeltAwareness, Bt as pickClaudeDefault, C as repoFingerprint, Ct as parseJsonOrDiagnose, D as trustRepo, Dt as extractTarGzMember, E as stopReviewStateDir, Et as provisionAndIndexColbert, Ft as DEFAULT_CODEX_MODEL_FALLBACKS, G as assetFor, Gt as setupGitHubToken, H as toolbeltSkipSet, Ht as withInstallLock, It as DEFAULT_PORT, J as ADVISOR_TOOL_INSTRUCTIONS, Jt as cacheModels, K as searchWeb, Kt as tryRefreshAndRetry, Lt as UPSTREAM_FETCH_TIMEOUT_MS, Mt as toolbeltPathOverride, Nt as DEFAULT_CLAUDE_MODEL_FALLBACKS, O as resolveSealedGate, Ot as extractZipMember, Pt as DEFAULT_CODEX_MODEL, Q as buildAnthropicErrorEvent, Qt as resolveCodexModel, Rt as UPSTREAM_INACTIVITY_TIMEOUT_MS, S as isSubagentContext, St as readResponseBodyCapped, T as stopGateEnabledForRepo, Tt as hasSupportedBrowserInstalled, U as vscodeRipgrepPath, Ut as setupCopilotToken, V as toolbeltEnabled, Vt as getPackageVersion, W as TOOLBELT_TOOLS, Wt as setupGitHubAgentToken, X as injectAdvisorTool, Xt as filterBetaHeader, Y as buildAdvisorStream, Yt as cacheVSCodeVersion, Z as isAdvisorRequested, Zt as isNullish, _ as stopReviewEnabled, _t as resolveMcpToolTimeoutMs, a as buildPeerAwarenessSnippet, an as GITHUB_API_BASE_URL, at as handleMcpPost, b as fileLastPromptStore, bt as createChatCompletions, c as buildSessionBindHookCommand, cn as githubHeaders, ct as browserToolsEnabled, d as decideStopHook, dt as standInToolEnabled, en as sleep, et as isControllerClosedError, f as fileBlockBudget, ft as workerToolsEnabled, g as stopGateId, gt as assembleResponsesPayload, h as stopGateDisabled, ht as getTokenCount, i as buildAgentPrompt, in as forwardError, it as handleMcpDelete, jt as collapsePathKeys, k as liveExec, kt as shouldUseInsecureTls, l as buildStopHookCommand, ln as state, lt as fleetToolsEnabled, m as launchBaselineKey, mt as createMessages, n as MCP_GROUPS, nn as fetchWithTransientRetry, nt as readIteratorWithTimeout, o as personasFor, on as copilotBaseUrl, ot as agentToolsEnabled, p as injectStopHookIntoSettingsFile, pt as countTokens, q as ADVISOR_INTERNAL_TOOL_NAME, qt as cacheCopilotVersion, r as assertMcpToolSurfaceConsistent, rn as HTTPError, rt as relayAnthropicStream, s as buildArtifactOpenHookCommand, sn as copilotHeaders, st as browseAgentEnabled, t as GROUP_META, tn as getModels, tt as logStreamError, u as captureLaunchBaseline, ut as implementerSubagentModel, v as fileBaselineStore, vt as pickEndpoint, w as repoRoot, wt as provisionBrowserAssets, x as fileReviewDebounce, xt as MAX_RESPONSE_BODY_BYTES, y as fileFindingsStore, yt as createResponses, z as availableToolCommands, zt as generateRandomPort } from "./peer-mcp-personas-Bfq3FPKc.js";
3
+ import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeArtifactCredsToMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS, u as writeRuntimeFileSecure } from "./paths-CTlT1nTo.js";
4
+ import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-ChPBRt6K.js";
5
+ import { a as sweepRegistry } from "./lifecycle-BUXxiltc.js";
6
6
  import { defineCommand, runMain } from "citty";
7
7
  import consola from "consola";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import fs, { chmod, copyFile, link, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
10
10
  import os, { homedir, tmpdir } from "node:os";
11
+ import * as nodePath$1 from "node:path";
11
12
  import nodePath from "node:path";
12
13
  import process$1 from "node:process";
13
14
  import { execFileSync, spawn } from "node:child_process";
15
+ import * as fs$2 from "node:fs";
14
16
  import fs$1, { existsSync, mkdirSync, promises, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
15
17
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
16
18
  import { Writable } from "node:stream";
@@ -809,6 +811,224 @@ function launchChild(target, server$1, options = {}) {
809
811
  });
810
812
  }
811
813
 
814
+ //#endregion
815
+ //#region src/lib/worker-dispatch.ts
816
+ /**
817
+ * Frozen contract for the NON-BLOCKING workers surface.
818
+ *
819
+ * The `workers` MCP tools (`explore`/`implement`/`review`/`plan`/`test`, and
820
+ * `browse` when the browse agent is enabled) BLOCK the caller for up to 6h
821
+ * (`runWorkerAgent`). The MAIN Claude Code agent must never block on one, so a
822
+ * per-mode `worker-*` DISPATCHER SUBAGENT — which Claude Code runs in the
823
+ * background and reports on via a completion notification — is the only
824
+ * sanctioned way to run a worker. This module is the single source of truth for
825
+ * three things that must never drift:
826
+ *
827
+ * 1. the tool → dispatcher map (`mcp__<workersKey>__<mode>` ↔ `worker-<mode>`),
828
+ * 2. the PreToolUse GUARD decision that denies a raw worker call from the main
829
+ * agent (or any non-dispatcher subagent) and redirects it to the matching
830
+ * `worker-*` agent, allowing it only from the dispatcher itself, and
831
+ * 3. the dispatcher subagent bodies (description / system prompt / `tools:`
832
+ * allowlist).
833
+ *
834
+ * Imported by `codex-mcp-config.ts` (dispatcher `.md` generation + the hook
835
+ * command it bakes into settings.json) AND `internal-worker-guard.ts` (the
836
+ * runtime PreToolUse hook). Keeping the map here means the settings matcher, the
837
+ * redirect target, the dispatcher names, and the `tools:` allowlist are all
838
+ * derived from one place.
839
+ *
840
+ * Discrimination mechanism: a Claude Code PreToolUse payload carries
841
+ * `agent_type` (the invoking subagent's name) ONLY inside a subagent context —
842
+ * absent for the top-level/main agent. So `agent_type === "worker-<mode>"` is
843
+ * the reliable "this call came from the dispatcher, allow it" signal, and its
844
+ * absence (or any other value) means "main agent or an unrelated subagent →
845
+ * deny". This is the SAME field this repo's Stop / prompt-submit hooks already
846
+ * key off (`isSubagentContext` in `orchestration/stop-gate-policy.ts`); the
847
+ * guard here is the inverse (allow-only-dispatcher rather than skip-any-sub).
848
+ */
849
+ /** The five always-available worker modes (gated by `workerToolsEnabled()`). */
850
+ const CORE_WORKER_MODES = [
851
+ "explore",
852
+ "implement",
853
+ "review",
854
+ "plan",
855
+ "test"
856
+ ];
857
+ /** The browse worker mode, gated separately by `browseAgentEnabled()`. */
858
+ const BROWSE_WORKER_MODE = "browse";
859
+ /** Every mode the surface can expose — used to build the sweep-regex allowlist
860
+ * and to validate `--modes`. Order is stable (core first, browse last). */
861
+ const ALL_WORKER_DISPATCH_MODES = [...CORE_WORKER_MODES, BROWSE_WORKER_MODE];
862
+ /** The dispatcher subagent name for a mode, e.g. `implement` → `worker-implement`. */
863
+ function dispatcherAgentName(mode) {
864
+ return `worker-${mode}`;
865
+ }
866
+ /** Every possible dispatcher subagent name (used by the stale-`.md` sweep). */
867
+ const ALL_DISPATCHER_AGENT_NAMES = ALL_WORKER_DISPATCH_MODES.map(dispatcherAgentName);
868
+ /** The MCP tool name the workers server exposes for a mode under the resolved key. */
869
+ function workerToolName(workersKey, mode) {
870
+ return `mcp__${workersKey}__${mode}`;
871
+ }
872
+ /** The active dispatch modes for a launch: the five core modes plus `browse`
873
+ * only when the browse agent is enabled. */
874
+ function activeDispatchModes(opts) {
875
+ return opts.browse ? [...CORE_WORKER_MODES, BROWSE_WORKER_MODE] : [...CORE_WORKER_MODES];
876
+ }
877
+ function escapeRegex(s) {
878
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
879
+ }
880
+ /**
881
+ * The Claude Code PreToolUse `matcher` (a regex over the tool name) that scopes
882
+ * the guard hook to exactly the active worker tools — nothing else invokes the
883
+ * hook. Anchored + exact-alternation so an unrelated `mcp__<key>__status` (a
884
+ * future non-blocking tool with no dispatcher) is never matched, hence never
885
+ * denied.
886
+ */
887
+ function guardToolMatcher(workersKey, modes) {
888
+ const alt = modes.map((m) => escapeRegex(m)).join("|");
889
+ return `^mcp__${escapeRegex(workersKey)}__(${alt})$`;
890
+ }
891
+ /** Parse the worker mode out of a tool name for the resolved key, or null if it
892
+ * isn't one of the recognized worker tools. */
893
+ function parseWorkerToolCall(toolName, workersKey, modes) {
894
+ const prefix = `mcp__${workersKey}__`;
895
+ if (!toolName.startsWith(prefix)) return null;
896
+ const rest = toolName.slice(prefix.length);
897
+ return modes.includes(rest) ? rest : null;
898
+ }
899
+ /** The `permissionDecisionReason` shown to the model on a deny, steering it to
900
+ * the matching background dispatcher. `mode` is null when the payload was
901
+ * unparseable (fail-closed generic message). */
902
+ function guardDenyReason(mode) {
903
+ return `Workers run as background subagents in this session so your turn never blocks. Re-issue this as ${mode ? `Agent(subagent_type: "${dispatcherAgentName(mode)}", prompt: <your worker brief>)` : `the matching background \`worker-*\` agent via the Agent tool`}. It returns immediately and delivers the worker's result as a completion notification — do not call the raw \`mcp__…__\` worker tool from the main thread.`;
904
+ }
905
+ /** The stdout JSON a PreToolUse hook prints to DENY a tool call. */
906
+ function guardDenyOutput(mode) {
907
+ return JSON.stringify({ hookSpecificOutput: {
908
+ hookEventName: "PreToolUse",
909
+ permissionDecision: "deny",
910
+ permissionDecisionReason: guardDenyReason(mode)
911
+ } });
912
+ }
913
+ /**
914
+ * Pure PreToolUse guard decision. Given the raw stdin payload, the resolved
915
+ * workers key, and the active modes, decide whether to DENY the tool call.
916
+ *
917
+ * Rules (fail toward protecting the "main never blocks" invariant):
918
+ * - Payload unparseable / no string `tool_name`: the matcher only fires this
919
+ * hook for worker tools, so a payload we can't read is still a worker call
920
+ * → DENY (fail closed, generic redirect).
921
+ * - `tool_name` is not a recognized worker tool for this key: ALLOW (not a
922
+ * tool we guard — never deny a non-worker tool).
923
+ * - `agent_type` equals one of the active dispatcher names: ALLOW (the call
924
+ * came from the dispatcher subagent that is meant to run the worker).
925
+ * - Otherwise (main agent: `agent_type` absent; or a non-dispatcher subagent):
926
+ * DENY with a redirect to the matching `worker-<mode>` agent.
927
+ */
928
+ function decideWorkerGuard(input) {
929
+ let payload = null;
930
+ try {
931
+ const parsed = JSON.parse(input.stdin);
932
+ if (parsed && typeof parsed === "object") payload = parsed;
933
+ } catch {
934
+ payload = null;
935
+ }
936
+ const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : null;
937
+ if (!toolName) return {
938
+ output: guardDenyOutput(null),
939
+ verdict: "deny-malformed"
940
+ };
941
+ const mode = parseWorkerToolCall(toolName, input.workersKey, input.modes);
942
+ if (mode === null) return {
943
+ output: null,
944
+ verdict: "allow-non-worker"
945
+ };
946
+ const agentType = payload?.agent_type;
947
+ if (typeof agentType === "string" && agentType === dispatcherAgentName(mode)) return {
948
+ output: null,
949
+ verdict: "allow-dispatcher"
950
+ };
951
+ return {
952
+ output: guardDenyOutput(mode),
953
+ verdict: "deny-main"
954
+ };
955
+ }
956
+ /** One-line human description shown to the lead when picking a subagent. Uses
957
+ * the documented "Use proactively" auto-delegation idiom. */
958
+ function dispatcherDescription(mode) {
959
+ return `${{
960
+ explore: "Non-blocking `explore` worker: dispatches a read-only autonomous worker (its own context) in the background and delivers its summary as a completion notification.",
961
+ implement: "Non-blocking `implement` worker: dispatches an autonomous coding worker (read/write/bash, optional git worktree) in the background and delivers its result as a completion notification.",
962
+ review: "Non-blocking `review` worker: dispatches a read-only reviewer that reads the code itself to verify a change or claim, in the background, and delivers findings as a completion notification.",
963
+ plan: "Non-blocking `plan` worker: dispatches a read-only planner that returns an ordered implementation plan, in the background, and delivers it as a completion notification.",
964
+ test: "Non-blocking `test` worker: dispatches an independent test author that writes tests trying to break the implementation, in the background, and delivers pass/fail as a completion notification.",
965
+ browse: "Non-blocking `browse` worker: dispatches an autonomous browser agent in the background and delivers its result as a completion notification."
966
+ }[mode]} Use proactively for any ${mode}-mode worker task so a long run never blocks your turn: it returns immediately and notifies you when done.`;
967
+ }
968
+ /** The dispatcher subagent's full system prompt: call the one worker tool once,
969
+ * relay verbatim, do nothing else. */
970
+ function dispatcherPrompt(mode, workersKey) {
971
+ const tool = workerToolName(workersKey, mode);
972
+ return [
973
+ `# Subagent: ${dispatcherAgentName(mode)}`,
974
+ "",
975
+ `You are a thin DISPATCHER for the \`${mode}\` worker. You run in the background so the`,
976
+ "lead agent's turn is never blocked while the (up-to-6-hour) worker runs.",
977
+ "",
978
+ "## Your only job",
979
+ "",
980
+ `Call the \`${tool}\` tool EXACTLY ONCE, passing through the fields from the lead's brief:`,
981
+ " - `prompt`: the lead's worker brief, copied verbatim",
982
+ " - `workspace` (optional): absolute path, if the lead specified one",
983
+ " - `model` / `thinking` (optional): only if the lead specified them",
984
+ " - `maxWallClockMs` (optional): per-call wall-clock budget in ms, if the lead specified one" + (mode === "implement" || mode === "test" ? "\n - `worktree` (optional): pass `true` if the lead asked for isolated-worktree execution" : ""),
985
+ "",
986
+ "When the tool returns, output its result VERBATIM as your final message. That final",
987
+ "message is what the lead receives in the completion notification — it IS the result.",
988
+ "",
989
+ "## Hard rules",
990
+ "",
991
+ "- Call the worker tool exactly once. Do not retry on a normal (non-error) return.",
992
+ "- Do NOT attempt the task yourself, do NOT read/edit files, do NOT run other tools.",
993
+ "- Do NOT spawn other agents (you have no Agent tool and must not try to gain one).",
994
+ "- Do NOT summarize, paraphrase, or add commentary — relay the worker output verbatim.",
995
+ "- If the worker returns an error, relay that error verbatim (do not mask it)."
996
+ ].join("\n");
997
+ }
998
+ /** The `tools:` frontmatter allowlist for a dispatcher: the workers MCP server
999
+ * wildcard (`mcp__<workersKey>__*`). Claude Code's `tools:` field supports
1000
+ * MCP patterns only at SERVER granularity (`mcp__<server>__*`), not individual
1001
+ * tool names, so this grants exactly the workers tools and NOTHING else — no
1002
+ * Agent/Task (so it cannot spawn further agents → no recursion), no Read/Bash
1003
+ * (so it cannot do extra work). The dispatcher's prompt narrows it to the one
1004
+ * mode; the guard allows any dispatcher-named caller regardless. */
1005
+ function dispatcherTools(_mode, workersKey) {
1006
+ return [`mcp__${workersKey}__*`];
1007
+ }
1008
+ /**
1009
+ * Build the shell command Claude Code runs for the workers `PreToolUse` guard
1010
+ * hook — the running github-router via its node/bun binary so it works
1011
+ * regardless of PATH. Mirrors `buildPromptSubmitHookCommand`.
1012
+ *
1013
+ * The resolved `workersKey` and the active `modes` are baked into the command
1014
+ * ARGS (not env): `mergeStopHookIntoSettings` dedups hooks by the command
1015
+ * string only, so baking the key/modes makes a changed resolution produce a
1016
+ * DISTINCT command — no stale-matcher entry can survive from a prior launch.
1017
+ */
1018
+ function buildWorkerGuardHookCommand(execPath, scriptPath, workersKey, modes) {
1019
+ const q = (s) => `"${s}"`;
1020
+ const args = `internal-worker-guard --workers-key ${q(workersKey)} --modes ${q(modes.join(","))}`;
1021
+ if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} ${args}`;
1022
+ return `${q(execPath)} ${args}`;
1023
+ }
1024
+ /** Parse a `--modes` CSV back into validated modes (drops unknown tokens). */
1025
+ function parseModesCsv(csv) {
1026
+ if (!csv) return [...CORE_WORKER_MODES];
1027
+ const known = new Set(ALL_WORKER_DISPATCH_MODES);
1028
+ const out = csv.split(",").map((s) => s.trim()).filter((s) => known.has(s));
1029
+ return out.length > 0 ? out : [...CORE_WORKER_MODES];
1030
+ }
1031
+
812
1032
  //#endregion
813
1033
  //#region src/lib/codex-mcp-config.ts
814
1034
  /** The `peers` server is always enabled, so its resolved key always exists;
@@ -816,6 +1036,13 @@ function launchChild(target, server$1, options = {}) {
816
1036
  function peersKeyOf(groupKeys) {
817
1037
  return groupKeys.peers ?? GROUP_META.peers.preferredKey;
818
1038
  }
1039
+ /** The resolved `workers` server key (bare `workers`, or the `gh-router-workers`
1040
+ * fallback on collision). Used to name the dispatcher tools and the guard
1041
+ * matcher. Falls back to the preferred bare key when the group is absent (the
1042
+ * caller only builds worker dispatchers when the group is enabled anyway). */
1043
+ function workersKeyOf(groupKeys) {
1044
+ return groupKeys.workers ?? GROUP_META.workers.preferredKey;
1045
+ }
819
1046
  /**
820
1047
  * Decide which MCP backend serves the codex personas.
821
1048
  *
@@ -975,6 +1202,19 @@ function buildPeerAgentDefinitions(opts) {
975
1202
  codexCli: opts.codexCli,
976
1203
  geminiAvailable: opts.geminiAvailable
977
1204
  });
1205
+ if (opts.implementerModel && opts.implementerModel.length > 0) out.implementer = {
1206
+ description: "Bounded implementation subagent running gpt-5.5 (strong non-Claude coder, high reasoning). Use for well-scoped coding tasks — edits, small features, fixes — you want implemented in an integrated subagent. Model is overridable at spawn.",
1207
+ prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
1208
+ model: opts.implementerModel
1209
+ };
1210
+ if (opts.workerToolsAvailable) {
1211
+ const workersKey = workersKeyOf(opts.groupKeys);
1212
+ for (const mode of activeDispatchModes({ browse: opts.browseAvailable === true })) out[dispatcherAgentName(mode)] = {
1213
+ description: dispatcherDescription(mode),
1214
+ prompt: dispatcherPrompt(mode, workersKey),
1215
+ tools: dispatcherTools(mode, workersKey)
1216
+ };
1217
+ }
978
1218
  return out;
979
1219
  }
980
1220
  /**
@@ -1018,17 +1258,25 @@ function escapeYamlString(s) {
1018
1258
  * component.
1019
1259
  */
1020
1260
  const VALID_AGENT_NAME = /^[a-z][a-z0-9-]*$/;
1021
- /** Build a single subagent .md file body (frontmatter + system prompt). */
1261
+ /** Build a single subagent .md file body (frontmatter + system prompt).
1262
+ *
1263
+ * `tools` (optional) becomes a `tools:` frontmatter allowlist RESTRICTING the
1264
+ * subagent to exactly those tools (omission inherits the parent's full toolset,
1265
+ * per Claude Code semantics). Used by the `worker-*` dispatchers to pin each to
1266
+ * its single `mcp__<workersKey>__<mode>` tool — which physically prevents them
1267
+ * from spawning other agents or doing extra work. Names are validated by the
1268
+ * caller (`writePeerAgentMdFiles`) / are proxy-generated, so no escaping needed
1269
+ * beyond the comma-join Claude Code's frontmatter parser expects. */
1022
1270
  function buildAgentMd(spec) {
1023
- return [
1271
+ const lines = [
1024
1272
  "---",
1025
1273
  `name: ${spec.name}`,
1026
- `description: ${escapeYamlString(spec.description)}`,
1027
- "---",
1028
- "",
1029
- spec.prompt,
1030
- ""
1031
- ].join("\n");
1274
+ `description: ${escapeYamlString(spec.description)}`
1275
+ ];
1276
+ if (spec.model) lines.push(`model: ${escapeYamlString(spec.model)}`);
1277
+ if (spec.tools && spec.tools.length > 0) lines.push(`tools: [${spec.tools.map((t) => JSON.stringify(t)).join(", ")}]`);
1278
+ lines.push("---", "", spec.prompt, "");
1279
+ return lines.join("\n");
1032
1280
  }
1033
1281
  /**
1034
1282
  * Write per-launch subagent .md files into the user's `~/.claude/agents/`
@@ -1059,7 +1307,9 @@ async function writePeerAgentMdFiles(agents, opts) {
1059
1307
  await writeRuntimeFileSecure(filePath, buildAgentMd({
1060
1308
  name: name$1,
1061
1309
  description: def.description,
1062
- prompt: def.prompt
1310
+ prompt: def.prompt,
1311
+ model: def.model,
1312
+ tools: def.tools
1063
1313
  }));
1064
1314
  paths.push(filePath);
1065
1315
  }
@@ -1251,6 +1501,9 @@ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
1251
1501
  codexCli: opts.codexCli,
1252
1502
  geminiAvailable: opts.geminiAvailable,
1253
1503
  groupKeys: opts.groupKeys,
1504
+ workerToolsAvailable: opts.workerToolsAvailable,
1505
+ browseAvailable: opts.browseAvailable,
1506
+ implementerModel: opts.implementerModel,
1254
1507
  nonce,
1255
1508
  codexHome
1256
1509
  });
@@ -2036,7 +2289,7 @@ async function discoverGateCommands(cwd, opts) {
2036
2289
  if (files.length === 0) return null;
2037
2290
  let result;
2038
2291
  try {
2039
- const { runWorkerAgent } = await import("./engine-CvY9mR3b.js");
2292
+ const { runWorkerAgent } = await import("./engine-DHFa2_hK.js");
2040
2293
  result = await runWorkerAgent({
2041
2294
  mode: "explore",
2042
2295
  workspace: root,
@@ -2250,9 +2503,9 @@ Use the request's kind and payload as the contract:
2250
2503
 
2251
2504
  Delegate heavy reading to workers:
2252
2505
 
2253
- - Use mcp__workers__explore for focused source or history gathering.
2254
- - Use mcp__workers__review for compact review of a specific plan, PR summary, or suspicious change.
2255
- - Use mcp__workers__test when a missing executable check is the blocker.
2506
+ - Use the worker-explore subagent (Agent tool) for focused source or history gathering.
2507
+ - Use the worker-review subagent (Agent tool) for compact review of a specific plan, PR summary, or suspicious change.
2508
+ - Use the worker-test subagent (Agent tool) when a missing executable check is the blocker.
2256
2509
 
2257
2510
  Do not read a full diff, full CI log, or full transcript in the lead context. Ask workers for narrow facts and compact excerpts. The ledger is durable memory; context is not.
2258
2511
 
@@ -2385,7 +2638,7 @@ Binding rule:
2385
2638
 
2386
2639
  Ask whether changed behavior has executable coverage.
2387
2640
 
2388
- - If behavior changed and no relevant test exists, use mcp__workers__test to author a focused test through a DIFFERENT lab than the implementer when possible.
2641
+ - If behavior changed and no relevant test exists, dispatch the worker-test subagent (Agent tool, subagent_type worker-test) to author a focused test through a DIFFERENT lab than the implementer when possible.
2389
2642
  - Cap missing-test attempts; default to a small number of focused tries.
2390
2643
  - Run the new test and then the relevant existing gate.
2391
2644
  - If creating a large new harness, broad fixture system, or slow integration environment is required, ask the user before proceeding.
@@ -2534,10 +2787,10 @@ Tag every blind spot as executable-checkable or judgment-only.
2534
2787
 
2535
2788
  ## Phase 3 and 4: decompose and plan (run in parallel)
2536
2789
 
2537
- These two are INDEPENDENT: mcp__orchestrate__decompose consumes { ask, context: research brief plus blind-spots }, and mcp__workers__plan consumes the ask, acceptance criteria, research pointer, and blind-spot table. Neither needs the other's output. So issue BOTH calls in a SINGLE parallel batch (same turn) — do not wait for decompose before calling plan.
2790
+ These two are INDEPENDENT: mcp__orchestrate__decompose consumes { ask, context: research brief plus blind-spots }, and the worker-plan subagent consumes the ask, acceptance criteria, research pointer, and blind-spot table. Neither needs the other's output. So issue BOTH in a SINGLE parallel batch (same turn) — call mcp__orchestrate__decompose and dispatch the worker-plan subagent together; do not wait for decompose first.
2538
2791
 
2539
2792
  - decompose: mcp__orchestrate__decompose({ ask, context: research brief plus blind-spots }). Treat the output as a proposal, not gospel; reject or revise nodes that do not map to a real blind spot.
2540
- - plan: mcp__workers__plan with the ask, acceptance criteria, research pointer, and blind-spot table. Ask for files, tests, rollback concerns, and minimal safe increments; keep it bounded and suited to the change size.
2793
+ - plan: dispatch the worker-plan subagent (Agent tool, subagent_type worker-plan) with the ask, acceptance criteria, research pointer, and blind-spot table. Ask for files, tests, rollback concerns, and minimal safe increments; keep it bounded and suited to the change size.
2541
2794
 
2542
2795
  ## Phase 5: compose a native Workflow
2543
2796
 
@@ -2559,10 +2812,10 @@ Parallelism (the Workflow tool's core optimization rule):
2559
2812
 
2560
2813
  Role to tool mapping:
2561
2814
 
2562
- - research: mcp__workers__explore and mcp__search__code for focused follow-ups.
2563
- - plan: mcp__workers__plan.
2564
- - implement: mcp__workers__implement, with worktree:true for parallel writers.
2565
- - test: mcp__workers__test, authored by a DIFFERENT LAB than the implementer when possible. This is an advisory practice, not enforced provenance.
2815
+ - research: worker-explore (Agent subagent) and mcp__search__code for focused follow-ups.
2816
+ - plan: worker-plan (Agent subagent).
2817
+ - implement: worker-implement (Agent subagent), with worktree:true for parallel writers.
2818
+ - test: worker-test (Agent subagent), authored by a DIFFERENT LAB than the implementer when possible. This is an advisory practice, not enforced provenance.
2566
2819
  - review: mcp__peers__codex_reviewer plus mcp__peers__gemini_reviewer. Advisory unless findings are converted into executable checks or code changes.
2567
2820
  - baseline and selector: OPT-IN only because it doubles cost. Choose max(orchestrated, baseline) by EXECUTABLE gate result, not model judgment. If no executable oracle exists, say the selector is advisory.
2568
2821
  - verify: cross-lab checker plus mcp__orchestrate__attest_step with producer not equal to checker lab.
@@ -2672,7 +2925,7 @@ Default caps unless the user explicitly gives a smaller or larger budget:
2672
2925
  - Then use mcp__search__code lexically for exact symbols, filenames, errors, routes, flags, and config keys.
2673
2926
  - Use git blame or history when authorship, regression timing, or intent matters.
2674
2927
  - Use mcp__search__web for upstream APIs, package behavior, protocol docs, or public issues.
2675
- - Launch parallel mcp__workers__explore workers for heavy gathering, each with a narrow question and expected artifact.
2928
+ - Launch parallel worker-explore background subagents (via the Agent tool, subagent_type worker-explore) for heavy gathering, each with a narrow question and expected artifact. They run non-blocking and report back on completion.
2676
2929
  - Keep worker results summarized; do not paste every detail into the main context.
2677
2930
 
2678
2931
  4. Form a root-cause hypothesis.
@@ -2684,7 +2937,7 @@ Default caps unless the user explicitly gives a smaller or larger budget:
2684
2937
  5. Verify load-bearing claims adversarially.
2685
2938
  - First preference: reproduce the bug, run the failing test, or run the direct check. Tag verified-executable.
2686
2939
  - If executable proof is not available, read the actual source or primary artifact and cite the lines. Tag verified-source.
2687
- - Ask mcp__workers__review to confirm the source-reading for important claims.
2940
+ - Dispatch the worker-review subagent (via the Agent tool) to confirm the source-reading for important claims.
2688
2941
  - Ask a different-lab refuter through mcp__peers__codex_critic or mcp__peers__gemini_critic to try to refute the hypothesis.
2689
2942
  - Give the refuter the symptom, observed facts, and acceptance criteria, but not your proposed root cause. Avoid anchoring them.
2690
2943
  - If the refuter finds a plausible alternative, add it to the worklist and spend at most one bounded round resolving it.
@@ -2723,6 +2976,67 @@ Return a compact brief, not the whole research dump:
2723
2976
  `
2724
2977
  };
2725
2978
 
2979
+ //#endregion
2980
+ //#region src/lib/injected-skills/worker-skill.ts
2981
+ /**
2982
+ * The `/gh-worker` skill: the operating model for the NON-BLOCKING workers
2983
+ * surface. Materialized into the per-launch mirror whenever `workerToolsEnabled()`
2984
+ * (same gate as the other floor-raising skills). Discoverability + playbook; the
2985
+ * load-bearing enforcement is the PreToolUse guard + the `worker-*` dispatcher
2986
+ * subagents (see src/lib/worker-dispatch.ts).
2987
+ */
2988
+ const WORKER_SKILL = {
2989
+ name: "gh-worker",
2990
+ md: `---
2991
+ name: gh-worker
2992
+ description: How to run github-router workers without blocking your turn. Workers (explore/implement/review/plan/test) can run up to 6 hours; dispatch the matching worker-* background subagent so you get a completion notification instead of waiting. Use whenever you would reach for a worker.
2993
+ user-invocable: true
2994
+ ---
2995
+
2996
+ # gh-worker: non-blocking workers
2997
+
2998
+ Worker tasks (explore, implement, review, plan, test) can run for up to 6 hours.
2999
+ In this session they are NON-BLOCKING BY DESIGN: you dispatch a
3000
+ background \`worker-*\` subagent, get control back immediately, and receive the
3001
+ worker's result as a completion notification when it finishes. Your turn is
3002
+ never blocked waiting on a worker, and the worker's tool output never fills your
3003
+ context (only its final result comes back).
3004
+
3005
+ ## How to run a worker
3006
+
3007
+ Dispatch the matching dispatcher subagent with the Agent tool:
3008
+
3009
+ - worker-explore: read-only investigation / codebase gathering, returns a summary.
3010
+ - worker-review: reads the code itself to verify a change or claim; findings with severity + file:line.
3011
+ - worker-plan: returns an ordered implementation plan from a task + acceptance criteria.
3012
+ - worker-implement: read/write/bash coding worker; pass worktree: true for isolated-worktree execution + a returned diff.
3013
+ - worker-test: independent test author that writes tests trying to break the implementation and reports pass/fail.
3014
+
3015
+ Put the full worker brief in the subagent's prompt (and an absolute workspace
3016
+ path, or model/thinking/worktree, only if you need to override the defaults).
3017
+ The dispatcher calls the worker once and relays its result verbatim.
3018
+
3019
+ ## What to expect
3020
+
3021
+ - The dispatch returns immediately; you can keep working or start other workers.
3022
+ - When the worker finishes you get a completion notification carrying its result.
3023
+ - Up to 8 workers run concurrently (the worker-semaphore cap); further dispatches queue.
3024
+ - You do NOT call the raw mcp__...__ worker tools from the main thread: a guard
3025
+ denies that and points you at the matching worker-* subagent. That guard is the
3026
+ guarantee your turn never blocks; dispatching worker-* directly is the normal path.
3027
+
3028
+ ## Notes
3029
+
3030
+ - Large worker output may be summarized by the dispatcher relay; for a big
3031
+ artifact, have the worker write it to a file and return the path.
3032
+ - Dispatching the same worker twice runs it twice (no dedup); avoid double-dispatch
3033
+ for side-effecting work like worker-implement.
3034
+ - Background subagents + completion notifications are the interactive default. In
3035
+ headless (claude -p) runs the task surface behaves differently; prefer interactive
3036
+ for long worker fan-out.
3037
+ `
3038
+ };
3039
+
2726
3040
  //#endregion
2727
3041
  //#region src/lib/injected-skills/artifact-review-skill.ts
2728
3042
  const ARTIFACT_REVIEW_SKILL = {
@@ -2814,6 +3128,8 @@ const PEER_MARKER_OPEN = "<!-- gh-router peer-mcp awareness — auto-injected, r
2814
3128
  const PEER_MARKER_CLOSE = "<!-- /gh-router peer-mcp awareness -->";
2815
3129
  const STYLE_MARKER_OPEN = "<!-- gh-router style directive — auto-injected, regenerated per launch -->";
2816
3130
  const STYLE_MARKER_CLOSE = "<!-- /gh-router style directive -->";
3131
+ const OPERATING_MARKER_OPEN = "<!-- gh-router operating defaults — auto-injected, regenerated per launch -->";
3132
+ const OPERATING_MARKER_CLOSE = "<!-- /gh-router operating defaults -->";
2817
3133
  const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injected, regenerated per launch -->";
2818
3134
  const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
2819
3135
  const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
@@ -2829,6 +3145,28 @@ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by defau
2829
3145
  */
2830
3146
  const STYLE_DIRECTIVE = "Write concisely without losing detail. Use a natural human voice. Avoid em dashes. Do not attribute work to Claude, AI, LLM, or Anthropic anywhere (commits, PRs, issues, code, comments, docs).";
2831
3147
  /**
3148
+ * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md
3149
+ * AND into the main agent's system prompt (`--append-system-prompt`), so it is
3150
+ * the highest-salience behavioral default. Two defaults, both explicitly
3151
+ * overridden by the user's own direction and the domain's standards:
3152
+ *
3153
+ * 1. Orchestrate (strong default): delegate the heavy / parallel /
3154
+ * context-heavy work to the right subagent / worker / model, keeping the
3155
+ * main context free to reason and collaborate with the user, while still
3156
+ * doing trivial / surgical / last-mile work directly (delegating that
3157
+ * would only add relay-fidelity loss + latency).
3158
+ * 2. Excellence lens (HYBRID per the peer review): the reliable control
3159
+ * signal is the PRINCIPLE stated plainly; the well-known names appear only
3160
+ * as a "bar to clear" calibration, NOT as "channel X", with an explicit
3161
+ * no-impersonation / no-theatrics guardrail (cross-lab critics: a named
3162
+ * entity is a dense, high-variance vector that pulls in noise + persona
3163
+ * mannerisms at top salience, so the principle leads and the name calibrates).
3164
+ *
3165
+ * Self-referentially compliant with the style directive: no em dashes, no
3166
+ * Claude / Anthropic attribution.
3167
+ */
3168
+ const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (worker-* agents for background non-blocking runs, Task subagents for parallel work, peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context.\n\nAim high. Default to radical simplicity and relentless focus on the user's real experience (the Jobs and Ive bar for design), whole-systems first-principles thinking that anticipates scale and the long arc (the Gates bar for architecture and tech), and customer obsession that works backwards from the outcome (the Bezos bar for product and business). Question every assumption and prefer what you can derive, reproduce, or test. Adopt the principles, not a persona: no impersonation, name-dropping, or theatrics.";
3169
+ /**
2832
3170
  * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
2833
3171
  * the would-be post-write file) has grown past this size.
2834
3172
  * Read-modify-write becomes pathological at very large sizes; CLAUDE.md
@@ -3166,6 +3504,23 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
3166
3504
  });
3167
3505
  }
3168
3506
  /**
3507
+ * Prepend the operating-defaults directive (orchestrator posture + hybrid
3508
+ * excellence lens; `OPERATING_DEFAULTS_DIRECTIVE` above) to the TOP of the
3509
+ * mirrored CLAUDE.md so descendant agents (Agent subagents, agent-teams
3510
+ * teammates) inherit it. The main agent gets the same text at higher salience
3511
+ * via `--append-system-prompt`. Separate marker fence from the style / peer
3512
+ * blocks so all coexist; best-effort (warn-and-continue) like its siblings.
3513
+ */
3514
+ async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_DEFAULTS_DIRECTIVE) {
3515
+ await injectMarkerBlock({
3516
+ snippet: directive,
3517
+ markerOpen: OPERATING_MARKER_OPEN,
3518
+ markerClose: OPERATING_MARKER_CLOSE,
3519
+ position: "top",
3520
+ label: "operating-defaults"
3521
+ });
3522
+ }
3523
+ /**
3169
3524
  * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
3170
3525
  * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
3171
3526
  * subagents, agent-teams teammates) learn about the provisioned tools.
@@ -3257,6 +3612,7 @@ const INJECTED_SKILLS = [
3257
3612
  RESEARCH_SKILL,
3258
3613
  ORCHESTRATE_SKILL,
3259
3614
  FLOOR_KEEPER_SKILL,
3615
+ WORKER_SKILL,
3260
3616
  FIRST_MATE_SKILL
3261
3617
  ];
3262
3618
 
@@ -3739,7 +4095,7 @@ function initProxyFromEnv() {
3739
4095
  //#endregion
3740
4096
  //#region package.json
3741
4097
  var name = "github-router";
3742
- var version$1 = "0.3.152";
4098
+ var version$1 = "0.3.162";
3743
4099
 
3744
4100
  //#endregion
3745
4101
  //#region src/lib/approval.ts
@@ -4336,90 +4692,1487 @@ function sanitizeAnthropicBody(rawBody) {
4336
4692
  }
4337
4693
 
4338
4694
  //#endregion
4339
- //#region src/routes/messages/handler.ts
4340
- const isWebSearchTool$1 = (tool) => typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search";
4341
- /**
4342
- * Extract whitelisted beta headers from the incoming request to forward
4343
- * to the Copilot API. VS Code sends these to enable extended features
4344
- * like thinking, context management, and advanced tool use.
4345
- */
4346
- function extractBetaHeaders(c) {
4347
- const headers = {};
4348
- const anthropicBeta = c.req.header("anthropic-beta");
4349
- if (anthropicBeta) {
4350
- const filtered = filterBetaHeader(anthropicBeta);
4351
- if (filtered) headers["anthropic-beta"] = filtered;
4352
- }
4353
- return headers;
4695
+ //#region src/lib/anthropic-translate/anthropic-sse.ts
4696
+ function makeMessageId() {
4697
+ return `msg_${randomUUID().replace(/-/g, "")}`;
4698
+ }
4699
+ function makeMessageStart(id, model, usage = {}) {
4700
+ return {
4701
+ type: "message_start",
4702
+ message: {
4703
+ id,
4704
+ type: "message",
4705
+ role: "assistant",
4706
+ model,
4707
+ content: [],
4708
+ stop_reason: null,
4709
+ stop_sequence: null,
4710
+ usage: {
4711
+ input_tokens: usage.input_tokens ?? 0,
4712
+ output_tokens: usage.output_tokens ?? 0,
4713
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
4714
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0
4715
+ }
4716
+ }
4717
+ };
4718
+ }
4719
+ function makeContentBlockStart(index, block) {
4720
+ return {
4721
+ type: "content_block_start",
4722
+ index,
4723
+ content_block: block
4724
+ };
4725
+ }
4726
+ function makeTextDelta(index, text) {
4727
+ return {
4728
+ type: "content_block_delta",
4729
+ index,
4730
+ delta: {
4731
+ type: "text_delta",
4732
+ text
4733
+ }
4734
+ };
4735
+ }
4736
+ function makeInputJsonDelta(index, partialJson) {
4737
+ return {
4738
+ type: "content_block_delta",
4739
+ index,
4740
+ delta: {
4741
+ type: "input_json_delta",
4742
+ partial_json: partialJson
4743
+ }
4744
+ };
4745
+ }
4746
+ function makeThinkingDelta(index, thinking) {
4747
+ return {
4748
+ type: "content_block_delta",
4749
+ index,
4750
+ delta: {
4751
+ type: "thinking_delta",
4752
+ thinking
4753
+ }
4754
+ };
4755
+ }
4756
+ function makeSignatureDelta(index, signature) {
4757
+ return {
4758
+ type: "content_block_delta",
4759
+ index,
4760
+ delta: {
4761
+ type: "signature_delta",
4762
+ signature
4763
+ }
4764
+ };
4765
+ }
4766
+ function makeContentBlockStop(index) {
4767
+ return {
4768
+ type: "content_block_stop",
4769
+ index
4770
+ };
4771
+ }
4772
+ function makeMessageDelta(stopReason, stopSequence, usage) {
4773
+ return {
4774
+ type: "message_delta",
4775
+ delta: {
4776
+ stop_reason: stopReason,
4777
+ stop_sequence: stopSequence
4778
+ },
4779
+ usage: {
4780
+ input_tokens: usage.input_tokens ?? 0,
4781
+ output_tokens: usage.output_tokens ?? 0,
4782
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
4783
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0
4784
+ }
4785
+ };
4786
+ }
4787
+ function makeMessageStop() {
4788
+ return { type: "message_stop" };
4789
+ }
4790
+ /** Serialize one event to the Anthropic SSE wire form (`event:` + `data:`). */
4791
+ function serializeAnthropicEvent(ev) {
4792
+ return `event: ${ev.type}\ndata: ${JSON.stringify(ev)}\n\n`;
4354
4793
  }
4355
4794
  /**
4356
- * Extract the text content from the last user message for web search.
4357
- * Handles both string content and content block arrays (multimodal).
4795
+ * Wrap a generator of Anthropic stream events into a byte `ReadableStream`.
4796
+ * Pull-based; guards the consumer-cancel race on both `enqueue` and `close`;
4797
+ * converts a mid-stream generator throw into a terminal `event: error` frame.
4798
+ * On consumer cancel it invokes `onCancel` (abort the upstream fetch) and
4799
+ * `return()`s the generator so its `finally` tears down the upstream reader.
4358
4800
  */
4359
- function extractUserQuery$1(messages) {
4360
- for (let i = messages.length - 1; i >= 0; i--) {
4361
- const msg = messages[i];
4362
- if (msg.role === "user") {
4363
- if (typeof msg.content === "string") return msg.content;
4364
- if (Array.isArray(msg.content)) {
4365
- const textBlock = msg.content.find((block) => block.type === "text");
4366
- if (textBlock?.text) return textBlock.text;
4801
+ function anthropicSseStreamFromEvents(events, opts) {
4802
+ const enc = new TextEncoder();
4803
+ let consumerCancelled = false;
4804
+ let finished = false;
4805
+ const safeClose = (controller) => {
4806
+ try {
4807
+ controller.close();
4808
+ } catch {}
4809
+ };
4810
+ return new ReadableStream({
4811
+ async pull(controller) {
4812
+ if (consumerCancelled || finished) {
4813
+ safeClose(controller);
4814
+ return;
4815
+ }
4816
+ let res;
4817
+ try {
4818
+ res = await events.next();
4819
+ } catch (err) {
4820
+ finished = true;
4821
+ if (consumerCancelled) {
4822
+ safeClose(controller);
4823
+ return;
4824
+ }
4825
+ const name$1 = err instanceof Error ? err.name : "Error";
4826
+ const message = err instanceof Error ? err.message : String(err);
4827
+ consola.error(`Anthropic-translate stream interrupted at ${opts.routePath}: ${name$1}: ${JSON.stringify(message)}`);
4828
+ try {
4829
+ controller.enqueue(enc.encode(buildAnthropicErrorEvent(name$1, message)));
4830
+ } catch (enqueueError) {
4831
+ if (!isControllerClosedError(enqueueError)) consola.warn(`Could not deliver error event to consumer at ${opts.routePath}: ${enqueueError instanceof Error ? enqueueError.message : String(enqueueError)}`);
4832
+ }
4833
+ safeClose(controller);
4834
+ return;
4835
+ }
4836
+ if (consumerCancelled) {
4837
+ safeClose(controller);
4838
+ return;
4839
+ }
4840
+ if (res.done) {
4841
+ finished = true;
4842
+ safeClose(controller);
4843
+ return;
4844
+ }
4845
+ try {
4846
+ controller.enqueue(enc.encode(serializeAnthropicEvent(res.value)));
4847
+ } catch (err) {
4848
+ if (isControllerClosedError(err)) {
4849
+ consumerCancelled = true;
4850
+ return;
4851
+ }
4852
+ throw err;
4367
4853
  }
4854
+ },
4855
+ cancel() {
4856
+ consumerCancelled = true;
4857
+ finished = true;
4858
+ opts.onCancel?.();
4859
+ events.return?.(void 0);
4368
4860
  }
4369
- }
4861
+ });
4370
4862
  }
4863
+
4864
+ //#endregion
4865
+ //#region src/lib/reasoning-effort.ts
4371
4866
  /**
4372
- * Check if any user message contains tool_result content blocks,
4373
- * indicating a follow-up turn where we should skip web search.
4374
- * In Anthropic format, tool results are content blocks inside user messages,
4375
- * NOT separate role: "tool" messages like in OpenAI format.
4867
+ * Reasoning-effort bucketing + clamping shared by the `/v1/messages` handler
4868
+ * (adaptive-thinking translation) and the Anthropic-translation shim
4869
+ * (thinking-budget Responses `reasoning.effort`).
4870
+ *
4871
+ * Extracted from `src/routes/messages/handler.ts` so a `~/lib/*` module can
4872
+ * depend on it without importing route code (and without forming a
4873
+ * handler → shim → handler import cycle). `handler.ts` re-exports these for
4874
+ * backward compatibility with existing imports/tests.
4376
4875
  */
4377
- function hasToolResultContent(messages) {
4378
- return messages.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block.type === "tool_result"));
4379
- }
4876
+ const EFFORT_ORDER = [
4877
+ "low",
4878
+ "medium",
4879
+ "high",
4880
+ "xhigh"
4881
+ ];
4380
4882
  /**
4381
- * Inject web search results into the Anthropic system field.
4382
- * Handles three cases: absent, string, or array of content blocks.
4383
- * When array, prepends without cache_control to preserve existing directives.
4883
+ * Bucket a thinking budget into a Copilot reasoning-effort string.
4884
+ * `<2000`→low, `<8000`→medium, `<24000`→high, else→xhigh.
4885
+ * Defaults missing/non-numeric budgets to 8000 ("high").
4384
4886
  */
4385
- function injectSearchResults(body, searchContext) {
4386
- if (body.system === void 0 || body.system === null) body.system = searchContext;
4387
- else if (typeof body.system === "string") body.system = `${searchContext}\n\n${body.system}`;
4388
- else if (Array.isArray(body.system)) body.system = [{
4389
- type: "text",
4390
- text: searchContext
4391
- }, ...body.system];
4887
+ function bucketEffort(budget) {
4888
+ const n = typeof budget === "number" && Number.isFinite(budget) ? budget : 8e3;
4889
+ if (n < 2e3) return "low";
4890
+ if (n < 8e3) return "medium";
4891
+ if (n < 24e3) return "high";
4892
+ return "xhigh";
4392
4893
  }
4393
4894
  /**
4394
- * Strip web_search tools from the request and clean up tool_choice.
4395
- * Returns the modified body object.
4895
+ * Clamp a bucketed effort to the closest value in `supported`. Ties resolve to
4896
+ * the lower-tier option (per EFFORT_ORDER).
4897
+ *
4898
+ * Iterates EFFORT_ORDER (canonical low→xhigh) so the first match on a given
4899
+ * distance is always the lower-tier value, regardless of input order in
4900
+ * `supported`.
4396
4901
  */
4397
- function stripWebSearchTool(body) {
4398
- if (!body.tools) return;
4399
- body.tools = body.tools.filter((tool) => !isWebSearchTool$1(tool));
4400
- if (body.tools.length === 0) {
4401
- body.tools = void 0;
4402
- body.tool_choice = void 0;
4403
- } else if (body.tool_choice && typeof body.tool_choice === "object" && body.tool_choice.type === "tool") {
4404
- const choiceName = body.tool_choice.name;
4405
- if (choiceName && !body.tools.some((tool) => tool.name === choiceName)) body.tool_choice = { type: "auto" };
4902
+ function clampEffort(bucketed, supported) {
4903
+ if (supported.includes(bucketed)) return bucketed;
4904
+ const targetIdx = EFFORT_ORDER.indexOf(bucketed);
4905
+ let best;
4906
+ let bestDist = Infinity;
4907
+ for (let i = 0; i < EFFORT_ORDER.length; i++) {
4908
+ const value = EFFORT_ORDER[i];
4909
+ if (!supported.includes(value)) continue;
4910
+ const dist = Math.abs(i - targetIdx);
4911
+ if (dist < bestDist) {
4912
+ bestDist = dist;
4913
+ best = value;
4914
+ }
4915
+ }
4916
+ return best ?? bucketed;
4917
+ }
4918
+
4919
+ //#endregion
4920
+ //#region src/lib/anthropic-translate/anthropic-request.ts
4921
+ /** Flatten Anthropic `system` (string | array of text blocks) into a string. */
4922
+ function flattenSystem(system) {
4923
+ if (typeof system === "string") return system.length > 0 ? system : void 0;
4924
+ if (Array.isArray(system)) {
4925
+ let s = "";
4926
+ for (const block of system) if (block && typeof block === "object" && block.type === "text") {
4927
+ const t = block.text;
4928
+ if (typeof t === "string") s += t;
4929
+ }
4930
+ return s.length > 0 ? s : void 0;
4406
4931
  }
4407
4932
  }
4408
4933
  /**
4409
- * Process web search if the request contains a web_search tool.
4410
- * Performs the search, injects results into system, and strips the tool.
4411
- * Returns the (possibly modified) body string to forward.
4934
+ * Parse an Anthropic `tool_result.content` (string | block array) into the
4935
+ * plain-text `output` for the Responses `function_call_output` (a string-only
4936
+ * item) PLUS any image parts found in the content. A `function_call_output`
4937
+ * cannot carry images, so the caller emits the extracted images as a follow-up
4938
+ * user message (Claude Code browser screenshots/observations arrive this way).
4939
+ * `isError` (the tool_result `is_error` flag) is preserved by prefixing the
4940
+ * text so the model still learns the tool call failed.
4412
4941
  */
4413
- async function processWebSearch(rawBody) {
4414
- if (!rawBody.includes("web_search")) return rawBody;
4415
- let body;
4416
- try {
4417
- body = JSON.parse(rawBody);
4418
- } catch {
4419
- return rawBody;
4942
+ function parseToolResultContent(content, isError) {
4943
+ const images = [];
4944
+ let text = "";
4945
+ if (typeof content === "string") text = content;
4946
+ else if (Array.isArray(content)) for (const block of content) {
4947
+ if (!block || typeof block !== "object") continue;
4948
+ const b = block;
4949
+ if (b.type === "text" && typeof b.text === "string") text += b.text;
4950
+ else if (b.type === "image") {
4951
+ const img = anthropicImageToNeutral(b.source);
4952
+ if (img) images.push(img);
4953
+ }
4420
4954
  }
4421
- if (!body.tools?.some((tool) => isWebSearchTool$1(tool))) return rawBody;
4422
- const query = hasToolResultContent(body.messages ?? []) ? void 0 : extractUserQuery$1(body.messages ?? []);
4955
+ if (images.length > 0 && text.length === 0) text = "[image result below]";
4956
+ if (isError) text = text.length > 0 ? `[tool error] ${text}` : "[tool error]";
4957
+ return {
4958
+ output: text,
4959
+ images
4960
+ };
4961
+ }
4962
+ /** Map an Anthropic `image` block source to a neutral image part. */
4963
+ function anthropicImageToNeutral(source) {
4964
+ if (!source || typeof source !== "object") return null;
4965
+ if (source.type === "url" && typeof source.url === "string") return {
4966
+ type: "image",
4967
+ url: source.url
4968
+ };
4969
+ if (source.type === "base64" && typeof source.data === "string") return {
4970
+ type: "image",
4971
+ mimeType: typeof source.media_type === "string" ? source.media_type : "image/png",
4972
+ data: source.data
4973
+ };
4974
+ return null;
4975
+ }
4976
+ /** Concatenate the text of an Anthropic `document` `content`-source block array. */
4977
+ function joinDocumentContentText(content) {
4978
+ if (!Array.isArray(content)) return "";
4979
+ let text = "";
4980
+ for (const block of content) if (block && typeof block === "object") {
4981
+ const b = block;
4982
+ if (b.type === "text" && typeof b.text === "string") text += b.text;
4983
+ }
4984
+ return text;
4985
+ }
4986
+ /**
4987
+ * Map an Anthropic `document` block to a neutral content part.
4988
+ * - base64 source → neutral `document` (mimeType + data) → Responses
4989
+ * `input_file` with `file_data`; on the chat path → an inline text note
4990
+ * (Copilot's `/chat/completions` rejects file parts).
4991
+ * - url source → neutral `document` (url) → Responses `input_file.file_url`.
4992
+ * - text source (a plain-text document) → the doc's text folded into a `text`
4993
+ * part, so the model sees it on BOTH paths.
4994
+ * - content source (content-block document) → its text blocks folded into a
4995
+ * `text` part.
4996
+ * Missing/invalid fields (unknown source type, `file`-id references Copilot has
4997
+ * no Files API for, empty text) yield null and are dropped.
4998
+ */
4999
+ function anthropicDocumentToNeutral(b) {
5000
+ const source = b.source;
5001
+ if (!source || typeof source !== "object") return null;
5002
+ const s = source;
5003
+ const filename = typeof b.title === "string" && b.title.length > 0 ? b.title : "document.pdf";
5004
+ if (s.type === "base64" && typeof s.data === "string") return {
5005
+ type: "document",
5006
+ mimeType: typeof s.media_type === "string" ? s.media_type : "application/pdf",
5007
+ data: s.data,
5008
+ filename
5009
+ };
5010
+ if (s.type === "url" && typeof s.url === "string") return {
5011
+ type: "document",
5012
+ url: s.url,
5013
+ filename
5014
+ };
5015
+ if (s.type === "text" && typeof s.data === "string") return s.data.length > 0 ? {
5016
+ type: "text",
5017
+ text: s.data
5018
+ } : null;
5019
+ if (s.type === "content") {
5020
+ const text = joinDocumentContentText(s.content);
5021
+ return text.length > 0 ? {
5022
+ type: "text",
5023
+ text
5024
+ } : null;
5025
+ }
5026
+ return null;
5027
+ }
5028
+ /**
5029
+ * Convert one Anthropic message into zero-or-more neutral messages. A user
5030
+ * message with `tool_result` blocks fans out: text/image content becomes a
5031
+ * user message and each tool_result becomes its own `toolResult` message,
5032
+ * emitted in wire order so a function_call_output never precedes its text.
5033
+ */
5034
+ function anthropicMessageToNeutral(msg) {
5035
+ const role = msg.role;
5036
+ const content = msg.content;
5037
+ if (role === "assistant") {
5038
+ const parts = [];
5039
+ if (typeof content === "string") {
5040
+ if (content.length > 0) parts.push({
5041
+ type: "text",
5042
+ text: content
5043
+ });
5044
+ } else if (Array.isArray(content)) for (const block of content) {
5045
+ if (!block || typeof block !== "object") continue;
5046
+ const b = block;
5047
+ if (b.type === "text" && typeof b.text === "string") parts.push({
5048
+ type: "text",
5049
+ text: b.text
5050
+ });
5051
+ else if (b.type === "tool_use") parts.push({
5052
+ type: "toolCall",
5053
+ id: typeof b.id === "string" ? b.id : "",
5054
+ name: typeof b.name === "string" ? b.name : "",
5055
+ arguments: b.input ?? {}
5056
+ });
5057
+ }
5058
+ return [{
5059
+ role: "assistant",
5060
+ content: parts
5061
+ }];
5062
+ }
5063
+ const out = [];
5064
+ let userParts = [];
5065
+ const flushUser = () => {
5066
+ if (userParts.length === 0) return;
5067
+ out.push({
5068
+ role: "user",
5069
+ content: userParts
5070
+ });
5071
+ userParts = [];
5072
+ };
5073
+ if (typeof content === "string") {
5074
+ if (content.length > 0) out.push({
5075
+ role: "user",
5076
+ content
5077
+ });
5078
+ return out;
5079
+ }
5080
+ if (Array.isArray(content)) for (const block of content) {
5081
+ if (!block || typeof block !== "object") continue;
5082
+ const b = block;
5083
+ if (b.type === "text" && typeof b.text === "string") userParts.push({
5084
+ type: "text",
5085
+ text: b.text
5086
+ });
5087
+ else if (b.type === "image") {
5088
+ const img = anthropicImageToNeutral(b.source);
5089
+ if (img) userParts.push(img);
5090
+ } else if (b.type === "document") {
5091
+ const doc = anthropicDocumentToNeutral(b);
5092
+ if (doc) userParts.push(doc);
5093
+ } else if (b.type === "tool_result") {
5094
+ flushUser();
5095
+ const { output, images } = parseToolResultContent(b.content, b.is_error === true);
5096
+ out.push({
5097
+ role: "toolResult",
5098
+ toolCallId: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
5099
+ output
5100
+ });
5101
+ if (images.length > 0) out.push({
5102
+ role: "user",
5103
+ content: images
5104
+ });
5105
+ }
5106
+ }
5107
+ flushUser();
5108
+ return out;
5109
+ }
5110
+ function parseTools(tools) {
5111
+ if (!Array.isArray(tools) || tools.length === 0) return void 0;
5112
+ const out = [];
5113
+ for (const tool of tools) {
5114
+ if (!tool || typeof tool !== "object") continue;
5115
+ const t = tool;
5116
+ if (typeof t.name !== "string" || t.name.length === 0) continue;
5117
+ const schema = t.input_schema ?? t.parameters;
5118
+ out.push({
5119
+ name: t.name,
5120
+ description: typeof t.description === "string" ? t.description : void 0,
5121
+ parameters: schema && typeof schema === "object" ? schema : {
5122
+ type: "object",
5123
+ properties: {}
5124
+ }
5125
+ });
5126
+ }
5127
+ return out.length > 0 ? out : void 0;
5128
+ }
5129
+ function parseToolChoice(toolChoice) {
5130
+ if (!toolChoice || typeof toolChoice !== "object") return void 0;
5131
+ const tc = toolChoice;
5132
+ switch (tc.type) {
5133
+ case "auto": return "auto";
5134
+ case "any": return "required";
5135
+ case "none": return "none";
5136
+ case "tool": return typeof tc.name === "string" && tc.name.length > 0 ? {
5137
+ type: "function",
5138
+ name: tc.name
5139
+ } : void 0;
5140
+ default: return;
5141
+ }
5142
+ }
5143
+ /**
5144
+ * Anthropic carries `disable_parallel_tool_use` on the `tool_choice` object.
5145
+ * Returns `false` (the wire signal to disable parallel tool calls) only when it
5146
+ * is explicitly `true`; `undefined` otherwise, so the payload builders omit the
5147
+ * field rather than ever sending `parallel_tool_calls: true`.
5148
+ */
5149
+ function parseDisableParallelToolUse(toolChoice) {
5150
+ if (!toolChoice || typeof toolChoice !== "object") return void 0;
5151
+ return toolChoice.disable_parallel_tool_use === true ? false : void 0;
5152
+ }
5153
+ /** Default absent Anthropic `thinking` to high effort, clamped by the model.
5154
+ * Returns undefined for a model that advertises NO `reasoning_effort` allowlist
5155
+ * — such a model may not support reasoning at all, so forcing an effort could
5156
+ * 400; leaving it unset preserves the pre-default safe behavior for that case. */
5157
+ function defaultReasoningEffort(model) {
5158
+ const supported = model?.capabilities?.supports?.reasoning_effort;
5159
+ return Array.isArray(supported) && supported.length > 0 ? clampEffort("high", supported) : void 0;
5160
+ }
5161
+ /**
5162
+ * Map Anthropic `thinking` to a Responses reasoning effort, clamped to the
5163
+ * model's `reasoning_effort` allowlist. Returns undefined when thinking is
5164
+ * absent/disabled/non-enabled; the absent default is applied at the call site.
5165
+ */
5166
+ function parseReasoningEffort(thinking, model) {
5167
+ if (!thinking || typeof thinking !== "object") return void 0;
5168
+ const t = thinking;
5169
+ if (t.type !== "enabled") return void 0;
5170
+ const bucketed = bucketEffort(t.budget_tokens);
5171
+ const supported = model?.capabilities?.supports?.reasoning_effort;
5172
+ return Array.isArray(supported) && supported.length > 0 ? clampEffort(bucketed, supported) : bucketed;
5173
+ }
5174
+ /**
5175
+ * Parse an already-JSON-parsed Anthropic Messages body into the neutral shape.
5176
+ * `resolvedModel` is the catalog id the request will run on; `model` its
5177
+ * catalog entry (for the reasoning-effort allowlist).
5178
+ */
5179
+ function parseAnthropicRequest(body, resolvedModel, model) {
5180
+ const messages = [];
5181
+ if (Array.isArray(body.messages)) {
5182
+ for (const msg of body.messages) if (msg && typeof msg === "object") for (const n of anthropicMessageToNeutral(msg)) messages.push(n);
5183
+ }
5184
+ const maxTokens = typeof body.max_tokens === "number" && body.max_tokens > 0 ? body.max_tokens : void 0;
5185
+ const stopSequences = Array.isArray(body.stop_sequences) ? body.stop_sequences.filter((s) => typeof s === "string") : void 0;
5186
+ return {
5187
+ model: resolvedModel,
5188
+ instructions: flattenSystem(body.system),
5189
+ messages,
5190
+ tools: parseTools(body.tools),
5191
+ toolChoice: parseToolChoice(body.tool_choice),
5192
+ parallelToolCalls: parseDisableParallelToolUse(body.tool_choice),
5193
+ reasoningEffort: body.thinking === void 0 ? defaultReasoningEffort(model) : parseReasoningEffort(body.thinking, model),
5194
+ maxOutputTokens: maxTokens,
5195
+ stopSequences: stopSequences && stopSequences.length > 0 ? stopSequences : void 0,
5196
+ stream: body.stream === true
5197
+ };
5198
+ }
5199
+ /** Build the Copilot `/responses` payload from a parsed Anthropic request. */
5200
+ function parsedToResponsesPayload(parsed) {
5201
+ return assembleResponsesPayload({
5202
+ model: parsed.model,
5203
+ instructions: parsed.instructions,
5204
+ messages: parsed.messages,
5205
+ tools: parsed.tools,
5206
+ toolChoice: parsed.toolChoice,
5207
+ reasoningEffort: parsed.reasoningEffort,
5208
+ maxOutputTokens: parsed.maxOutputTokens,
5209
+ stopSequences: parsed.stopSequences,
5210
+ parallelToolCalls: parsed.parallelToolCalls,
5211
+ stream: parsed.stream
5212
+ });
5213
+ }
5214
+
5215
+ //#endregion
5216
+ //#region src/lib/anthropic-translate/chat-request.ts
5217
+ /** Build the `data:` URI (base64) or return the verbatim URL for an image part. */
5218
+ function imageUrlFor(part) {
5219
+ if (typeof part.url === "string" && part.url.length > 0) return part.url;
5220
+ return `data:${part.mimeType ?? "image/png"};base64,${part.data ?? ""}`;
5221
+ }
5222
+ /**
5223
+ * A brief inline note standing in for a document on the chat path. Copilot's
5224
+ * `/chat/completions` rejects file content parts (`type` must be `image_url` or
5225
+ * `text` → HTTP 400), so a document (PDF) can't be forwarded to a Gemini model;
5226
+ * the note keeps the document from being silently dropped and tells the model
5227
+ * one was provided but is unavailable, instead of 400ing the request.
5228
+ *
5229
+ * The note is wrapped in leading + trailing newlines so it is always DELIMITED
5230
+ * from adjacent user text — in the string-collapse branch it can't glue onto a
5231
+ * neighboring text run (`...[model]what is this?`), and in the content-parts
5232
+ * branch it stands as its own line. Regular text-to-text concatenation is left
5233
+ * untouched (only the note carries the delimiter), so wire order is preserved.
5234
+ */
5235
+ function documentNote(part) {
5236
+ return `\n[document "${part.filename ?? "document"}" attached but not supported for this model]\n`;
5237
+ }
5238
+ /**
5239
+ * A user turn: plain string when there are no images; otherwise OpenAI content
5240
+ * parts (`text` / `image_url`). Mirrors `neutralUserToResponses` so the two
5241
+ * shims encode a multimodal user turn the same way.
5242
+ */
5243
+ function neutralUserToChat(m) {
5244
+ if (typeof m.content === "string") return {
5245
+ role: "user",
5246
+ content: m.content
5247
+ };
5248
+ if (!m.content.some((c) => c.type === "image")) {
5249
+ let text = "";
5250
+ for (const c of m.content) if (c.type === "text") text += c.text;
5251
+ else if (c.type === "document") text += documentNote(c);
5252
+ return {
5253
+ role: "user",
5254
+ content: text
5255
+ };
5256
+ }
5257
+ const parts = [];
5258
+ for (const c of m.content) if (c.type === "text") parts.push({
5259
+ type: "text",
5260
+ text: c.text
5261
+ });
5262
+ else if (c.type === "image") parts.push({
5263
+ type: "image_url",
5264
+ image_url: { url: imageUrlFor(c) }
5265
+ });
5266
+ else if (c.type === "document") parts.push({
5267
+ type: "text",
5268
+ text: documentNote(c)
5269
+ });
5270
+ return {
5271
+ role: "user",
5272
+ content: parts
5273
+ };
5274
+ }
5275
+ /**
5276
+ * An assistant turn: text parts collapse into `content`, tool_use parts become
5277
+ * OpenAI `tool_calls`. Unlike the Responses path (which flushes text before each
5278
+ * call to preserve interleaving), the chat wire shape carries all text on
5279
+ * `content` and all calls on `tool_calls`, so ordering within the turn is not
5280
+ * representable — matching how OpenAI itself echoes an assistant turn. When the
5281
+ * turn is tool-calls-only, `content` is `null` (OpenAI convention).
5282
+ */
5283
+ function neutralAssistantToChat(m) {
5284
+ let text = "";
5285
+ const toolCalls = [];
5286
+ for (const c of m.content) if (c.type === "text") text += c.text;
5287
+ else if (c.type === "toolCall") toolCalls.push({
5288
+ id: c.id,
5289
+ type: "function",
5290
+ function: {
5291
+ name: c.name,
5292
+ arguments: JSON.stringify(c.arguments ?? {})
5293
+ }
5294
+ });
5295
+ const msg = {
5296
+ role: "assistant",
5297
+ content: text.length > 0 ? text : toolCalls.length > 0 ? null : ""
5298
+ };
5299
+ if (toolCalls.length > 0) msg.tool_calls = toolCalls;
5300
+ return msg;
5301
+ }
5302
+ /** Translate one neutral message into a single chat/completions message. */
5303
+ function neutralMessageToChat(m) {
5304
+ if (m.role === "user") return neutralUserToChat(m);
5305
+ if (m.role === "assistant") return neutralAssistantToChat(m);
5306
+ return {
5307
+ role: "tool",
5308
+ tool_call_id: m.toolCallId,
5309
+ content: m.output
5310
+ };
5311
+ }
5312
+ function neutralToolsToChat(tools) {
5313
+ if (!tools || tools.length === 0) return void 0;
5314
+ return tools.map((t) => ({
5315
+ type: "function",
5316
+ function: {
5317
+ name: t.name,
5318
+ description: t.description,
5319
+ parameters: t.parameters ?? {
5320
+ type: "object",
5321
+ properties: {}
5322
+ }
5323
+ }
5324
+ }));
5325
+ }
5326
+ /**
5327
+ * Convert the parsed (Responses-flat) `tool_choice` to the chat NESTED form.
5328
+ * A forced tool is `{type:"function", function:{name}}` on chat/completions —
5329
+ * distinct from the Responses flat `{type:"function", name}`. `"auto"` /
5330
+ * `"required"` / `"none"` pass through unchanged.
5331
+ */
5332
+ function toolChoiceToChat(tc) {
5333
+ if (tc === void 0) return void 0;
5334
+ if (typeof tc === "string") return tc === "auto" || tc === "none" || tc === "required" ? tc : void 0;
5335
+ if (tc.type === "function" && typeof tc.name === "string" && tc.name.length > 0) return {
5336
+ type: "function",
5337
+ function: { name: tc.name }
5338
+ };
5339
+ }
5340
+ /** Assemble the `/chat/completions` payload from a parsed Anthropic request. */
5341
+ function parsedToChatPayload(parsed) {
5342
+ const messages = [];
5343
+ if (parsed.instructions) messages.push({
5344
+ role: "system",
5345
+ content: parsed.instructions
5346
+ });
5347
+ for (const m of parsed.messages) messages.push(neutralMessageToChat(m));
5348
+ const payload = {
5349
+ model: parsed.model,
5350
+ messages,
5351
+ stream: parsed.stream
5352
+ };
5353
+ const tools = neutralToolsToChat(parsed.tools);
5354
+ if (tools && tools.length > 0) {
5355
+ payload.tools = tools;
5356
+ payload.tool_choice = toolChoiceToChat(parsed.toolChoice) ?? "auto";
5357
+ }
5358
+ if (parsed.reasoningEffort && parsed.reasoningEffort !== "off") payload.reasoning_effort = parsed.reasoningEffort;
5359
+ if (typeof parsed.maxOutputTokens === "number" && parsed.maxOutputTokens > 0) payload.max_tokens = parsed.maxOutputTokens;
5360
+ if (parsed.stopSequences && parsed.stopSequences.length > 0) payload.stop = parsed.stopSequences;
5361
+ if (parsed.parallelToolCalls === false) payload.parallel_tool_calls = false;
5362
+ return payload;
5363
+ }
5364
+
5365
+ //#endregion
5366
+ //#region src/lib/anthropic-translate/chat-egress.ts
5367
+ /** Synthesize a matchable Anthropic tool_use id when the upstream id is absent. */
5368
+ function makeToolUseId$1() {
5369
+ return `toolu_${randomUUID().replace(/-/g, "")}`;
5370
+ }
5371
+ function parseToolArgs$1(raw) {
5372
+ if (typeof raw !== "string" || raw.trim().length === 0) return {};
5373
+ try {
5374
+ const parsed = JSON.parse(raw);
5375
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
5376
+ } catch {}
5377
+ return {};
5378
+ }
5379
+ function anthropicUsageFromChat(u) {
5380
+ if (!u) return {};
5381
+ return {
5382
+ input_tokens: u.prompt_tokens ?? 0,
5383
+ output_tokens: u.completion_tokens ?? 0,
5384
+ cache_read_input_tokens: u.prompt_tokens_details?.cached_tokens ?? 0,
5385
+ cache_creation_input_tokens: 0
5386
+ };
5387
+ }
5388
+ /**
5389
+ * Map a chat/completions `finish_reason` to an Anthropic stop_reason. A
5390
+ * truncated (`length`) response is `max_tokens` even when a partial tool call
5391
+ * is present — the response was cut — mirroring the Responses egress precedence.
5392
+ * `tool_calls` (or any buffered tool) → `tool_use`; everything else (`stop`,
5393
+ * `content_filter`, null) → `end_turn`.
5394
+ */
5395
+ function chatStopReason(finishReason, sawTool) {
5396
+ if (finishReason === "length") return "max_tokens";
5397
+ if (finishReason === "tool_calls" || sawTool) return "tool_use";
5398
+ return "end_turn";
5399
+ }
5400
+ /**
5401
+ * Map a non-streaming chat/completions object to an Anthropic Messages object.
5402
+ * The first choice's `message.content` becomes a text block (when non-empty)
5403
+ * and each `message.tool_calls[]` becomes a tool_use block.
5404
+ */
5405
+ function chatResponseToAnthropicMessage(resp, modelId) {
5406
+ const choice = resp.choices?.[0];
5407
+ const content = [];
5408
+ let sawTool = false;
5409
+ const message = choice?.message;
5410
+ if (message) {
5411
+ if (typeof message.content === "string" && message.content.length > 0) content.push({
5412
+ type: "text",
5413
+ text: message.content
5414
+ });
5415
+ if (Array.isArray(message.tool_calls)) for (const tc of message.tool_calls) {
5416
+ sawTool = true;
5417
+ const rawId = typeof tc.id === "string" ? tc.id : "";
5418
+ content.push({
5419
+ type: "tool_use",
5420
+ id: rawId.length > 0 ? rawId : makeToolUseId$1(),
5421
+ name: typeof tc.function?.name === "string" ? tc.function.name : "",
5422
+ input: parseToolArgs$1(tc.function?.arguments)
5423
+ });
5424
+ }
5425
+ }
5426
+ const usage = anthropicUsageFromChat(resp.usage);
5427
+ const stopReason = chatStopReason(choice?.finish_reason ?? null, sawTool);
5428
+ return {
5429
+ id: typeof resp.id === "string" && resp.id.length > 0 ? `msg_${resp.id}` : makeMessageId(),
5430
+ type: "message",
5431
+ role: "assistant",
5432
+ model: modelId,
5433
+ content,
5434
+ stop_reason: stopReason,
5435
+ stop_sequence: null,
5436
+ usage: {
5437
+ input_tokens: usage.input_tokens ?? 0,
5438
+ output_tokens: usage.output_tokens ?? 0,
5439
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
5440
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0
5441
+ }
5442
+ };
5443
+ }
5444
+ /**
5445
+ * Streaming synthesizer: consume a chat/completions SSE iterable, yield the
5446
+ * Anthropic event sequence. Emits `message_start` first, streams text live,
5447
+ * buffers tool calls per OpenAI array index and flushes them atomically at
5448
+ * end-of-stream (in numeric index order), then a terminal `message_delta`
5449
+ * (accumulated usage + stop_reason) and `message_stop`. The `[DONE]` sentinel
5450
+ * is the authoritative clean-end marker: a stream that ends WITHOUT it is
5451
+ * treated as truncated and throws so the stream adapter can emit a terminal
5452
+ * `event: error`. A clean `[DONE]` carrying no chunk-level `finish_reason`
5453
+ * still completes cleanly (stop_reason from any buffered tool, else `end_turn`).
5454
+ */
5455
+ async function* synthAnthropicFromChat(upstream, opts) {
5456
+ const messageId = opts.messageId ?? makeMessageId();
5457
+ let nextIndex = 0;
5458
+ let activeTextIndex = null;
5459
+ const toolByIndex = /* @__PURE__ */ new Map();
5460
+ let usageIn = 0;
5461
+ let usageOut = 0;
5462
+ let usageCacheRead = 0;
5463
+ let finishReason = null;
5464
+ let sawDone = false;
5465
+ yield makeMessageStart(messageId, opts.modelId);
5466
+ for await (const evt of upstream) {
5467
+ const data = evt?.data;
5468
+ if (data == null) continue;
5469
+ if (data === "[DONE]") {
5470
+ sawDone = true;
5471
+ break;
5472
+ }
5473
+ let chunk;
5474
+ try {
5475
+ chunk = JSON.parse(data);
5476
+ } catch {
5477
+ continue;
5478
+ }
5479
+ if (chunk.usage) {
5480
+ usageIn = Math.max(usageIn, chunk.usage.prompt_tokens ?? 0);
5481
+ usageOut = Math.max(usageOut, chunk.usage.completion_tokens ?? 0);
5482
+ usageCacheRead = Math.max(usageCacheRead, chunk.usage.prompt_tokens_details?.cached_tokens ?? 0);
5483
+ }
5484
+ const choice = chunk.choices?.[0];
5485
+ if (!choice) continue;
5486
+ const delta = choice.delta;
5487
+ if (delta && typeof delta.content === "string" && delta.content.length > 0) {
5488
+ if (activeTextIndex == null) {
5489
+ activeTextIndex = nextIndex++;
5490
+ yield makeContentBlockStart(activeTextIndex, {
5491
+ type: "text",
5492
+ text: ""
5493
+ });
5494
+ }
5495
+ yield makeTextDelta(activeTextIndex, delta.content);
5496
+ }
5497
+ if (delta && Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
5498
+ if (activeTextIndex != null) {
5499
+ yield makeContentBlockStop(activeTextIndex);
5500
+ activeTextIndex = null;
5501
+ }
5502
+ for (const tcd of delta.tool_calls) {
5503
+ if (tcd == null || typeof tcd.index !== "number") continue;
5504
+ let entry = toolByIndex.get(tcd.index);
5505
+ if (!entry) {
5506
+ entry = {
5507
+ id: "",
5508
+ name: "",
5509
+ args: ""
5510
+ };
5511
+ toolByIndex.set(tcd.index, entry);
5512
+ }
5513
+ if (typeof tcd.id === "string" && tcd.id.length > 0) entry.id = tcd.id;
5514
+ const name$1 = tcd.function?.name;
5515
+ if (typeof name$1 === "string" && name$1.length > 0) entry.name = name$1;
5516
+ const argDelta = tcd.function?.arguments;
5517
+ if (typeof argDelta === "string" && argDelta.length > 0) entry.args += argDelta;
5518
+ }
5519
+ }
5520
+ if (choice.finish_reason != null) finishReason = choice.finish_reason;
5521
+ }
5522
+ if (!sawDone) throw new Error("chat stream ended without a [DONE] sentinel (truncated)");
5523
+ if (activeTextIndex != null) {
5524
+ yield makeContentBlockStop(activeTextIndex);
5525
+ activeTextIndex = null;
5526
+ }
5527
+ let sawTool = false;
5528
+ const orderedTools = [...toolByIndex.entries()].sort((a, b) => a[0] - b[0]);
5529
+ for (const [, entry] of orderedTools) {
5530
+ sawTool = true;
5531
+ const index = nextIndex++;
5532
+ yield makeContentBlockStart(index, {
5533
+ type: "tool_use",
5534
+ id: entry.id.length > 0 ? entry.id : makeToolUseId$1(),
5535
+ name: entry.name,
5536
+ input: {}
5537
+ });
5538
+ yield makeInputJsonDelta(index, JSON.stringify(parseToolArgs$1(entry.args)));
5539
+ yield makeContentBlockStop(index);
5540
+ }
5541
+ yield makeMessageDelta(chatStopReason(finishReason, sawTool), null, {
5542
+ input_tokens: usageIn,
5543
+ output_tokens: usageOut,
5544
+ cache_read_input_tokens: usageCacheRead,
5545
+ cache_creation_input_tokens: 0
5546
+ });
5547
+ yield makeMessageStop();
5548
+ }
5549
+
5550
+ //#endregion
5551
+ //#region src/lib/anthropic-translate/responses-egress.ts
5552
+ /**
5553
+ * Stable map key for a `/responses` output item: prefer `output_index`
5554
+ * (constant per item), fall back to the opaque id only when absent. Namespaced
5555
+ * so a numeric index and a string id can never collide.
5556
+ */
5557
+ function responsesKey(outputIndex, fallbackId) {
5558
+ if (typeof outputIndex === "number") return `oi:${outputIndex}`;
5559
+ if (typeof fallbackId === "string" && fallbackId.length > 0) return `id:${fallbackId}`;
5560
+ }
5561
+ /** Synthesize a matchable Anthropic tool_use id when the upstream id is absent. */
5562
+ function makeToolUseId() {
5563
+ return `toolu_${randomUUID().replace(/-/g, "")}`;
5564
+ }
5565
+ /** First non-empty string among the candidates, or "" when none qualifies. */
5566
+ function firstNonEmpty(...vals) {
5567
+ for (const v of vals) if (typeof v === "string" && v.length > 0) return v;
5568
+ return "";
5569
+ }
5570
+ function anthropicUsageFromResponses(u) {
5571
+ if (!u) return {};
5572
+ return {
5573
+ input_tokens: u.input_tokens ?? 0,
5574
+ output_tokens: u.output_tokens ?? 0,
5575
+ cache_read_input_tokens: u.input_tokens_details?.cached_tokens ?? 0,
5576
+ cache_creation_input_tokens: 0
5577
+ };
5578
+ }
5579
+ function parseToolArgs(raw) {
5580
+ if (typeof raw !== "string" || raw.trim().length === 0) return {};
5581
+ try {
5582
+ const parsed = JSON.parse(raw);
5583
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
5584
+ } catch {}
5585
+ return {};
5586
+ }
5587
+ /**
5588
+ * Map a non-streaming Responses object to an Anthropic Messages object.
5589
+ *
5590
+ * stop_reason precedence for a completed non-streaming response:
5591
+ * an incomplete/max-output response is `max_tokens` even if a partial tool call
5592
+ * is present (the response was truncated), else a function_call → `tool_use`,
5593
+ * else `end_turn`.
5594
+ */
5595
+ function responsesResponseToAnthropicMessage(resp, modelId) {
5596
+ const output = Array.isArray(resp.output) ? resp.output : [];
5597
+ const content = [];
5598
+ let sawToolUse = false;
5599
+ for (const rawItem of output) {
5600
+ if (!rawItem || typeof rawItem !== "object") continue;
5601
+ const item = rawItem;
5602
+ if (item.type === "message") {
5603
+ let text = "";
5604
+ if (Array.isArray(item.content)) {
5605
+ for (const part of item.content) if (part && typeof part === "object" && part.type === "output_text" && typeof part.text === "string") text += part.text;
5606
+ }
5607
+ if (text.length > 0) content.push({
5608
+ type: "text",
5609
+ text
5610
+ });
5611
+ } else if (item.type === "function_call") {
5612
+ sawToolUse = true;
5613
+ const rawId = typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : typeof item.id === "string" && item.id.length > 0 ? item.id : "";
5614
+ content.push({
5615
+ type: "tool_use",
5616
+ id: rawId.length > 0 ? rawId : makeToolUseId(),
5617
+ name: typeof item.name === "string" ? item.name : "",
5618
+ input: parseToolArgs(item.arguments)
5619
+ });
5620
+ } else if (item.type === "reasoning") {
5621
+ let thinking = "";
5622
+ if (Array.isArray(item.summary)) {
5623
+ for (const part of item.summary) if (part && typeof part === "object" && typeof part.text === "string") thinking += part.text;
5624
+ }
5625
+ if (thinking.length > 0) content.push({
5626
+ type: "thinking",
5627
+ thinking,
5628
+ signature: typeof item.encrypted_content === "string" ? item.encrypted_content : ""
5629
+ });
5630
+ }
5631
+ }
5632
+ const usage = anthropicUsageFromResponses(resp.usage);
5633
+ const stopReason = resp.status === "incomplete" && resp.incomplete_details?.reason === "max_output_tokens" ? "max_tokens" : sawToolUse ? "tool_use" : "end_turn";
5634
+ return {
5635
+ id: typeof resp.id === "string" && resp.id.length > 0 ? `msg_${resp.id}` : makeMessageId(),
5636
+ type: "message",
5637
+ role: "assistant",
5638
+ model: modelId,
5639
+ content,
5640
+ stop_reason: stopReason,
5641
+ stop_sequence: null,
5642
+ usage: {
5643
+ input_tokens: usage.input_tokens ?? 0,
5644
+ output_tokens: usage.output_tokens ?? 0,
5645
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
5646
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0
5647
+ }
5648
+ };
5649
+ }
5650
+ /**
5651
+ * Streaming synthesizer: consume a `/responses` SSE iterable, yield the
5652
+ * Anthropic event sequence. Emits `message_start` first, then content blocks in
5653
+ * item order, then a terminal `message_delta` (accumulated usage + stop_reason)
5654
+ * and `message_stop`. A `response.failed` throws so the stream adapter can emit
5655
+ * a terminal `event: error`; a stream that ends WITHOUT a terminal event
5656
+ * (`completed`/`incomplete`/`failed`) is treated as truncated and throws too.
5657
+ */
5658
+ async function* synthAnthropicFromResponses(upstream, opts) {
5659
+ const messageId = opts.messageId ?? makeMessageId();
5660
+ const q = [];
5661
+ let nextIndex = 0;
5662
+ let current = null;
5663
+ const toolByKey = /* @__PURE__ */ new Map();
5664
+ const thinkingByKey = /* @__PURE__ */ new Map();
5665
+ const textByKey = /* @__PURE__ */ new Map();
5666
+ let usageIn = 0;
5667
+ let usageOut = 0;
5668
+ let usageCacheRead = 0;
5669
+ let sawTool = false;
5670
+ let hitMaxTokens = false;
5671
+ let sawTerminal = false;
5672
+ const closeCurrent = () => {
5673
+ if (!current) return;
5674
+ q.push(makeContentBlockStop(current.index));
5675
+ current = null;
5676
+ };
5677
+ const currentKind = () => current ? current.kind : null;
5678
+ const currentIndex = () => current ? current.index : null;
5679
+ const ensureTextState = (key) => {
5680
+ const existing = textByKey.get(key);
5681
+ if (existing != null && current?.kind === "text" && current.index === existing.index) return existing;
5682
+ closeCurrent();
5683
+ const index = nextIndex++;
5684
+ const state$1 = {
5685
+ index,
5686
+ emitted: ""
5687
+ };
5688
+ textByKey.set(key, state$1);
5689
+ current = {
5690
+ index,
5691
+ kind: "text"
5692
+ };
5693
+ q.push(makeContentBlockStart(index, {
5694
+ type: "text",
5695
+ text: ""
5696
+ }));
5697
+ return state$1;
5698
+ };
5699
+ const ensureThinking = (key) => {
5700
+ if (current?.kind === "thinking" && thinkingByKey.get(key) === current.index) return current.index;
5701
+ closeCurrent();
5702
+ const index = nextIndex++;
5703
+ current = {
5704
+ index,
5705
+ kind: "thinking"
5706
+ };
5707
+ thinkingByKey.set(key, index);
5708
+ q.push(makeContentBlockStart(index, {
5709
+ type: "thinking",
5710
+ thinking: ""
5711
+ }));
5712
+ return index;
5713
+ };
5714
+ const emitTool = (t) => {
5715
+ if (t.emitted) return;
5716
+ closeCurrent();
5717
+ const index = nextIndex++;
5718
+ q.push(makeContentBlockStart(index, {
5719
+ type: "tool_use",
5720
+ id: t.id,
5721
+ name: t.name,
5722
+ input: {}
5723
+ }));
5724
+ const args = t.argsBuffer.length > 0 ? t.argsBuffer : "{}";
5725
+ q.push(makeInputJsonDelta(index, args));
5726
+ q.push(makeContentBlockStop(index));
5727
+ t.emitted = true;
5728
+ sawTool = true;
5729
+ };
5730
+ q.push(makeMessageStart(messageId, opts.modelId));
5731
+ for (const e of q) yield e;
5732
+ q.length = 0;
5733
+ for await (const evt of upstream) {
5734
+ const data = evt?.data;
5735
+ if (data == null) continue;
5736
+ if (data === "[DONE]") break;
5737
+ let ev;
5738
+ try {
5739
+ ev = JSON.parse(data);
5740
+ } catch {
5741
+ continue;
5742
+ }
5743
+ switch (ev.type) {
5744
+ case "response.output_text.delta": {
5745
+ const d = ev.delta;
5746
+ if (typeof d !== "string" || d.length === 0) break;
5747
+ const state$1 = ensureTextState(responsesKey(ev.output_index, ev.item_id) ?? "text");
5748
+ state$1.emitted += d;
5749
+ q.push(makeTextDelta(state$1.index, d));
5750
+ break;
5751
+ }
5752
+ case "response.output_text.done": {
5753
+ const key = responsesKey(ev.output_index, ev.item_id) ?? "text";
5754
+ const fullText = typeof ev.text === "string" ? ev.text : "";
5755
+ const existing = textByKey.get(key);
5756
+ if (existing == null) {
5757
+ if (fullText.length > 0) {
5758
+ const state$1 = ensureTextState(key);
5759
+ state$1.emitted = fullText;
5760
+ q.push(makeTextDelta(state$1.index, fullText));
5761
+ closeCurrent();
5762
+ }
5763
+ } else if (currentKind() === "text" && currentIndex() === existing.index) {
5764
+ if (fullText.length > existing.emitted.length && fullText.startsWith(existing.emitted)) {
5765
+ q.push(makeTextDelta(existing.index, fullText.slice(existing.emitted.length)));
5766
+ existing.emitted = fullText;
5767
+ }
5768
+ closeCurrent();
5769
+ }
5770
+ break;
5771
+ }
5772
+ case "response.reasoning_summary_text.delta":
5773
+ case "response.reasoning_text.delta": {
5774
+ const d = ev.delta;
5775
+ if (typeof d !== "string" || d.length === 0) break;
5776
+ const key = responsesKey(ev.output_index, ev.item_id) ?? "reasoning";
5777
+ q.push(makeThinkingDelta(ensureThinking(key), d));
5778
+ break;
5779
+ }
5780
+ case "response.reasoning_summary_text.done":
5781
+ case "response.reasoning_text.done": break;
5782
+ case "response.output_item.added": {
5783
+ const item = ev.item;
5784
+ if (item?.type === "function_call") {
5785
+ const key = responsesKey(ev.output_index, item.id);
5786
+ if (key == null || toolByKey.has(key)) break;
5787
+ const toolId = firstNonEmpty(item.call_id, item.id);
5788
+ toolByKey.set(key, {
5789
+ id: toolId.length > 0 ? toolId : makeToolUseId(),
5790
+ name: item.name ?? "",
5791
+ argsBuffer: "",
5792
+ emitted: false
5793
+ });
5794
+ sawTool = true;
5795
+ }
5796
+ break;
5797
+ }
5798
+ case "response.function_call_arguments.delta": {
5799
+ const key = responsesKey(ev.output_index, ev.item_id);
5800
+ if (key == null) break;
5801
+ const t = toolByKey.get(key);
5802
+ if (!t || t.emitted) break;
5803
+ const d = ev.delta;
5804
+ if (typeof d !== "string" || d.length === 0) break;
5805
+ t.argsBuffer += d;
5806
+ break;
5807
+ }
5808
+ case "response.function_call_arguments.done": {
5809
+ const key = responsesKey(ev.output_index, ev.item_id);
5810
+ if (key == null) break;
5811
+ const t = toolByKey.get(key);
5812
+ if (!t || t.emitted) break;
5813
+ if (typeof ev.arguments === "string") t.argsBuffer = ev.arguments;
5814
+ break;
5815
+ }
5816
+ case "response.output_item.done": {
5817
+ const item = ev.item;
5818
+ if (item?.type === "function_call") {
5819
+ const key = responsesKey(ev.output_index, item.id);
5820
+ if (key == null) break;
5821
+ const t = toolByKey.get(key);
5822
+ if (!t || t.emitted) break;
5823
+ const doneId = firstNonEmpty(item.call_id, item.id);
5824
+ if (doneId.length > 0) t.id = doneId;
5825
+ if (typeof item.name === "string" && item.name.length > 0) t.name = item.name;
5826
+ if (typeof item.arguments === "string") t.argsBuffer = item.arguments;
5827
+ emitTool(t);
5828
+ } else if (item?.type === "reasoning") {
5829
+ const key = responsesKey(ev.output_index, item.id);
5830
+ const idx = key != null ? thinkingByKey.get(key) : void 0;
5831
+ if (idx != null && currentKind() === "thinking" && currentIndex() === idx) {
5832
+ if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) q.push(makeSignatureDelta(idx, item.encrypted_content));
5833
+ closeCurrent();
5834
+ }
5835
+ }
5836
+ break;
5837
+ }
5838
+ case "response.completed":
5839
+ case "response.incomplete": {
5840
+ sawTerminal = true;
5841
+ const u = ev.response?.usage;
5842
+ if (u) {
5843
+ usageIn = Math.max(usageIn, u.input_tokens ?? 0);
5844
+ usageOut = Math.max(usageOut, u.output_tokens ?? 0);
5845
+ usageCacheRead = Math.max(usageCacheRead, u.input_tokens_details?.cached_tokens ?? 0);
5846
+ }
5847
+ if (ev.type === "response.incomplete" && ev.response?.incomplete_details?.reason === "max_output_tokens") hitMaxTokens = true;
5848
+ break;
5849
+ }
5850
+ case "response.failed": throw new Error(ev.response?.error?.message ?? "response.failed");
5851
+ default: break;
5852
+ }
5853
+ for (const e of q) yield e;
5854
+ q.length = 0;
5855
+ }
5856
+ if (!sawTerminal) throw new Error("responses stream ended without a terminal event (truncated)");
5857
+ closeCurrent();
5858
+ for (const t of toolByKey.values()) if (!t.emitted) emitTool(t);
5859
+ const stopReason = hitMaxTokens ? "max_tokens" : sawTool ? "tool_use" : "end_turn";
5860
+ q.push(makeMessageDelta(stopReason, null, {
5861
+ input_tokens: usageIn,
5862
+ output_tokens: usageOut,
5863
+ cache_read_input_tokens: usageCacheRead,
5864
+ cache_creation_input_tokens: 0
5865
+ }));
5866
+ q.push(makeMessageStop());
5867
+ for (const e of q) yield e;
5868
+ q.length = 0;
5869
+ }
5870
+
5871
+ //#endregion
5872
+ //#region src/lib/anthropic-translate/classifier.ts
5873
+ /**
5874
+ * Match "claude"/"anthropic" as a delimiter-bounded segment anywhere in a model
5875
+ * id: at the start, or after a `/ _ . : -` path/version delimiter, and followed
5876
+ * by end-of-string, another such delimiter, or a digit. This catches catalog
5877
+ * aliases like `github/claude-3-7-sonnet` or `anthropic/…` whose vendor is
5878
+ * "github" and whose family is empty — where the token only surfaces mid-id —
5879
+ * while NOT firing on incidental substrings like `notclaude`. Deliberately
5880
+ * over-inclusive at the boundaries: we fail CLOSED toward Claude (→ passthrough)
5881
+ * so a real Claude model can never be diverted to the non-Claude shim.
5882
+ */
5883
+ const CLAUDE_ID_RE = /(^|[/_.:-])(claude|anthropic)(?=$|[/_.:-]|\d)/i;
5884
+ /**
5885
+ * True when the target is a Claude / Anthropic model. Matches on any of:
5886
+ * catalog vendor containing "anthropic", capability family containing "claude",
5887
+ * or a Claude/anthropic path-segment (see `CLAUDE_ID_RE`) in ANY id we hold —
5888
+ * the resolved id (`modelId`), the pre-resolution request id (`originalModelId`),
5889
+ * or the catalog entry's own id (`model.id`). Conservative by design: when in
5890
+ * doubt it returns true so a Claude request can never be diverted to the shim.
5891
+ */
5892
+ function isClaudeModel(modelId, model, originalModelId) {
5893
+ if (model) {
5894
+ if ((model.vendor?.toLowerCase() ?? "").includes("anthropic")) return true;
5895
+ if ((model.capabilities?.family?.toLowerCase() ?? "").includes("claude")) return true;
5896
+ }
5897
+ return [
5898
+ modelId,
5899
+ originalModelId,
5900
+ model?.id
5901
+ ].some((id) => typeof id === "string" && CLAUDE_ID_RE.test(id));
5902
+ }
5903
+ /**
5904
+ * Decide the route for a resolved model id + its catalog entry.
5905
+ *
5906
+ * - No model id, or a Claude model → "claude-passthrough" (existing behaviour).
5907
+ * - A non-Claude model whose catalog endpoint is `/responses` → "responses-shim".
5908
+ * - A non-Claude model whose catalog endpoint is `/chat/completions` (gemini and
5909
+ * any chat-default model) → "chat-shim".
5910
+ * - A non-Claude model absent from the catalog (so we can't confirm an endpoint)
5911
+ * → "claude-passthrough" (unchanged; we don't divert what we can't classify).
5912
+ *
5913
+ * `originalModelId` is the optional pre-resolution request id; when supplied it
5914
+ * is checked for Claude-likeness alongside the resolved id so an alias that
5915
+ * resolves to a non-Claude-looking id can't slip past.
5916
+ */
5917
+ function classifyMessagesRoute(modelId, model, originalModelId) {
5918
+ if (!modelId) return "claude-passthrough";
5919
+ if (isClaudeModel(modelId, model, originalModelId)) return "claude-passthrough";
5920
+ if (!model) return "claude-passthrough";
5921
+ const endpoint = pickEndpoint(model);
5922
+ if (endpoint === "responses") return "responses-shim";
5923
+ if (endpoint === "chat") return "chat-shim";
5924
+ return "claude-passthrough";
5925
+ }
5926
+
5927
+ //#endregion
5928
+ //#region src/lib/anthropic-translate/index.ts
5929
+ const STREAM_HEADERS = {
5930
+ "content-type": "text/event-stream",
5931
+ "cache-control": "no-cache",
5932
+ "transfer-encoding": "chunked",
5933
+ connection: "keep-alive"
5934
+ };
5935
+ function isAsyncIterable(x) {
5936
+ return x != null && typeof x[Symbol.asyncIterator] === "function";
5937
+ }
5938
+ /**
5939
+ * Handle a `/v1/messages` request targeting a non-Claude `/responses` model.
5940
+ * Returns a streaming or non-streaming Anthropic-format Response. Upstream
5941
+ * non-2xx / abort errors are thrown (as HTTPError) and handled by the route's
5942
+ * `forwardError`, exactly like the passthrough path.
5943
+ */
5944
+ async function handleNonClaudeResponses(c, opts) {
5945
+ const routePath = c.req.path;
5946
+ let body;
5947
+ try {
5948
+ body = JSON.parse(opts.rawBody);
5949
+ } catch {
5950
+ return c.json({
5951
+ type: "error",
5952
+ error: {
5953
+ type: "invalid_request_error",
5954
+ message: "Request body is not valid JSON"
5955
+ }
5956
+ }, 400);
5957
+ }
5958
+ const parsed = parseAnthropicRequest(body, opts.modelId, opts.model);
5959
+ const payload = parsedToResponsesPayload(parsed);
5960
+ if (consola.level >= 4) consola.debug(`Anthropic-translate → /responses model=${opts.modelId} stream=${parsed.stream} tools=${parsed.tools?.length ?? 0} effort=${parsed.reasoningEffort ?? "none"}`);
5961
+ if (parsed.stream) {
5962
+ const aborter = new AbortController();
5963
+ const result = await createResponses(payload, opts.model?.requestHeaders, aborter.signal, true);
5964
+ if (!isAsyncIterable(result)) throw new Error("Upstream /responses did not return an SSE stream (stream: true expected)");
5965
+ logRequest({
5966
+ method: "POST",
5967
+ path: routePath,
5968
+ model: opts.originalModel,
5969
+ resolvedModel: opts.modelId,
5970
+ status: 200,
5971
+ streaming: true
5972
+ }, opts.model, opts.startTime);
5973
+ const stream = anthropicSseStreamFromEvents(synthAnthropicFromResponses(result, { modelId: opts.modelId }), {
5974
+ routePath,
5975
+ onCancel: () => aborter.abort()
5976
+ });
5977
+ return new Response(stream, {
5978
+ status: 200,
5979
+ headers: STREAM_HEADERS
5980
+ });
5981
+ }
5982
+ const anthropic = responsesResponseToAnthropicMessage(await createResponses(payload, opts.model?.requestHeaders, void 0, true), opts.modelId);
5983
+ logRequest({
5984
+ method: "POST",
5985
+ path: routePath,
5986
+ model: opts.originalModel,
5987
+ resolvedModel: opts.modelId,
5988
+ inputTokens: anthropic.usage.input_tokens,
5989
+ outputTokens: anthropic.usage.output_tokens,
5990
+ status: 200
5991
+ }, opts.model, opts.startTime);
5992
+ return c.json(anthropic, 200);
5993
+ }
5994
+ /**
5995
+ * Handle a `/v1/messages` request targeting a non-Claude `/chat/completions`
5996
+ * model (gemini). Twin of `handleNonClaudeResponses` — same lifecycle, abort,
5997
+ * and logging contract, but assembles a chat/completions payload and translates
5998
+ * the chat response (object or SSE) back to the Anthropic wire shape.
5999
+ */
6000
+ async function handleNonClaudeChat(c, opts) {
6001
+ const routePath = c.req.path;
6002
+ let body;
6003
+ try {
6004
+ body = JSON.parse(opts.rawBody);
6005
+ } catch {
6006
+ return c.json({
6007
+ type: "error",
6008
+ error: {
6009
+ type: "invalid_request_error",
6010
+ message: "Request body is not valid JSON"
6011
+ }
6012
+ }, 400);
6013
+ }
6014
+ const parsed = parseAnthropicRequest(body, opts.modelId, opts.model);
6015
+ const payload = parsedToChatPayload(parsed);
6016
+ if (consola.level >= 4) consola.debug(`Anthropic-translate → /chat/completions model=${opts.modelId} stream=${parsed.stream} tools=${parsed.tools?.length ?? 0} effort=${parsed.reasoningEffort ?? "none"}`);
6017
+ if (parsed.stream) {
6018
+ const aborter = new AbortController();
6019
+ const result = await createChatCompletions(payload, opts.model?.requestHeaders, aborter.signal, true);
6020
+ if (!isAsyncIterable(result)) throw new Error("Upstream /chat/completions did not return an SSE stream (stream: true expected)");
6021
+ logRequest({
6022
+ method: "POST",
6023
+ path: routePath,
6024
+ model: opts.originalModel,
6025
+ resolvedModel: opts.modelId,
6026
+ status: 200,
6027
+ streaming: true
6028
+ }, opts.model, opts.startTime);
6029
+ const stream = anthropicSseStreamFromEvents(synthAnthropicFromChat(result, { modelId: opts.modelId }), {
6030
+ routePath,
6031
+ onCancel: () => aborter.abort()
6032
+ });
6033
+ return new Response(stream, {
6034
+ status: 200,
6035
+ headers: STREAM_HEADERS
6036
+ });
6037
+ }
6038
+ const anthropic = chatResponseToAnthropicMessage(await createChatCompletions(payload, opts.model?.requestHeaders, void 0, true), opts.modelId);
6039
+ logRequest({
6040
+ method: "POST",
6041
+ path: routePath,
6042
+ model: opts.originalModel,
6043
+ resolvedModel: opts.modelId,
6044
+ inputTokens: anthropic.usage.input_tokens,
6045
+ outputTokens: anthropic.usage.output_tokens,
6046
+ status: 200
6047
+ }, opts.model, opts.startTime);
6048
+ return c.json(anthropic, 200);
6049
+ }
6050
+
6051
+ //#endregion
6052
+ //#region src/routes/messages/handler.ts
6053
+ const isWebSearchTool$1 = (tool) => !!tool && typeof tool === "object" && (typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search");
6054
+ /**
6055
+ * Extract whitelisted beta headers from the incoming request to forward
6056
+ * to the Copilot API. VS Code sends these to enable extended features
6057
+ * like thinking, context management, and advanced tool use.
6058
+ */
6059
+ function extractBetaHeaders(c) {
6060
+ const headers = {};
6061
+ const anthropicBeta = c.req.header("anthropic-beta");
6062
+ if (anthropicBeta) {
6063
+ const filtered = filterBetaHeader(anthropicBeta);
6064
+ if (filtered) headers["anthropic-beta"] = filtered;
6065
+ }
6066
+ return headers;
6067
+ }
6068
+ /**
6069
+ * Extract the text content from the last user message for web search.
6070
+ * Handles both string content and content block arrays (multimodal).
6071
+ */
6072
+ function extractUserQuery$1(messages) {
6073
+ for (let i = messages.length - 1; i >= 0; i--) {
6074
+ const msg = messages[i];
6075
+ if (msg.role === "user") {
6076
+ if (typeof msg.content === "string") return msg.content;
6077
+ if (Array.isArray(msg.content)) {
6078
+ const textBlock = msg.content.find((block) => block.type === "text");
6079
+ if (textBlock?.text) return textBlock.text;
6080
+ }
6081
+ }
6082
+ }
6083
+ }
6084
+ /**
6085
+ * Check if any user message contains tool_result content blocks,
6086
+ * indicating a follow-up turn where we should skip web search.
6087
+ * In Anthropic format, tool results are content blocks inside user messages,
6088
+ * NOT separate role: "tool" messages like in OpenAI format.
6089
+ */
6090
+ function hasToolResultContent(messages) {
6091
+ return messages.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block.type === "tool_result"));
6092
+ }
6093
+ /**
6094
+ * Inject web search results into the Anthropic system field.
6095
+ * Handles three cases: absent, string, or array of content blocks.
6096
+ * When array, prepends without cache_control to preserve existing directives.
6097
+ */
6098
+ function injectSearchResults(body, searchContext) {
6099
+ if (body.system === void 0 || body.system === null) body.system = searchContext;
6100
+ else if (typeof body.system === "string") body.system = `${searchContext}\n\n${body.system}`;
6101
+ else if (Array.isArray(body.system)) body.system = [{
6102
+ type: "text",
6103
+ text: searchContext
6104
+ }, ...body.system];
6105
+ }
6106
+ /**
6107
+ * Strip web_search tools from the request and clean up tool_choice.
6108
+ * Returns the modified body object.
6109
+ */
6110
+ function stripWebSearchTool(body) {
6111
+ if (!body.tools) return;
6112
+ const tools = body.tools.filter((tool) => !isWebSearchTool$1(tool));
6113
+ body.tools = tools;
6114
+ if (tools.length === 0) {
6115
+ body.tools = void 0;
6116
+ body.tool_choice = void 0;
6117
+ } else if (body.tool_choice && typeof body.tool_choice === "object" && body.tool_choice.type === "tool") {
6118
+ const choiceName = body.tool_choice.name;
6119
+ if (choiceName && !tools.some((tool) => tool && typeof tool === "object" && tool.name === choiceName)) body.tool_choice = { type: "auto" };
6120
+ }
6121
+ }
6122
+ /**
6123
+ * Strip the injected `__anthropic_advisor` tool (and any Anthropic-native
6124
+ * `advisor_*` typed tool) from a request body. Used ONLY on the non-Claude
6125
+ * shim path: ADVISOR's server-side translate-loop (buildAdvisorStream) lives
6126
+ * on the native /v1/messages route, so a non-Claude model has no handler for
6127
+ * the tool and it must be removed before forwarding — otherwise the model
6128
+ * could emit a tool_use that nothing fulfils. Mirrors stripWebSearchTool's
6129
+ * tool_choice cleanup. Returns the original string (same reference) when
6130
+ * nothing was removed.
6131
+ */
6132
+ function stripAdvisorTool(rawBody) {
6133
+ let body;
6134
+ try {
6135
+ body = JSON.parse(rawBody);
6136
+ } catch {
6137
+ return rawBody;
6138
+ }
6139
+ if (!Array.isArray(body.tools)) return rawBody;
6140
+ const original = body.tools;
6141
+ const tools = original.filter((tool) => {
6142
+ if (typeof tool !== "object" || tool === null) return true;
6143
+ if (tool.name === ADVISOR_INTERNAL_TOOL_NAME) return false;
6144
+ const type = tool.type;
6145
+ return typeof type !== "string" || !type.startsWith("advisor_");
6146
+ });
6147
+ if (tools.length === original.length) return rawBody;
6148
+ if (tools.length === 0) {
6149
+ body.tools = void 0;
6150
+ body.tool_choice = void 0;
6151
+ } else {
6152
+ body.tools = tools;
6153
+ if (body.tool_choice && typeof body.tool_choice === "object" && body.tool_choice.type === "tool") {
6154
+ const choiceName = body.tool_choice.name;
6155
+ if (choiceName && !tools.some((tool) => tool && typeof tool === "object" && tool.name === choiceName)) body.tool_choice = { type: "auto" };
6156
+ }
6157
+ }
6158
+ return JSON.stringify(body);
6159
+ }
6160
+ /**
6161
+ * Process web search if the request contains a web_search tool.
6162
+ * Performs the search, injects results into system, and strips the tool.
6163
+ * Returns the (possibly modified) body string to forward.
6164
+ */
6165
+ async function processWebSearch(rawBody) {
6166
+ if (!rawBody.includes("web_search")) return rawBody;
6167
+ let body;
6168
+ try {
6169
+ body = JSON.parse(rawBody);
6170
+ } catch {
6171
+ return rawBody;
6172
+ }
6173
+ if (!body.tools?.some((tool) => isWebSearchTool$1(tool))) return rawBody;
6174
+ const messages = body.messages ?? [];
6175
+ const query = hasToolResultContent(messages) ? void 0 : extractUserQuery$1(messages);
4423
6176
  if (query) try {
4424
6177
  const results = await searchWeb(query);
4425
6178
  const searchContext = [
@@ -4474,6 +6227,19 @@ async function handleCompletion(c) {
4474
6227
  } catch {}
4475
6228
  const { body: resolvedBody, originalModel, resolvedModel, selectedModel } = resolveModelInBody$1(finalBody);
4476
6229
  const modelId = resolvedModel ?? originalModel;
6230
+ const messagesRoute = classifyMessagesRoute(modelId, selectedModel, originalModel);
6231
+ if (messagesRoute !== "claude-passthrough") {
6232
+ const shimBody = stripAdvisorTool(resolvedBody);
6233
+ if (advisorEnabled) consola.info("ADVISOR requested with a non-Claude model — stripping the injected __anthropic_advisor tool and proceeding without advisor (Claude-only feature; gracefully degraded).");
6234
+ const shimOpts = {
6235
+ rawBody: shimBody,
6236
+ modelId,
6237
+ model: selectedModel,
6238
+ originalModel,
6239
+ startTime
6240
+ };
6241
+ return messagesRoute === "chat-shim" ? handleNonClaudeChat(c, shimOpts) : handleNonClaudeResponses(c, shimOpts);
6242
+ }
4477
6243
  if (modelId) logEndpointMismatch(modelId, "/v1/messages");
4478
6244
  const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
4479
6245
  const advisorAborter = advisorEnabled ? new AbortController() : void 0;
@@ -4555,13 +6321,14 @@ async function handleCompletion(c) {
4555
6321
  const cappedResult = await readResponseBodyCapped(response, c.req.path, MAX_RESPONSE_BODY_BYTES);
4556
6322
  if (!cappedResult.ok) return c.json(cappedResult.errorResponse, cappedResult.status);
4557
6323
  const responseBody = cappedResult.value;
6324
+ const usage = responseBody.usage;
4558
6325
  logRequest({
4559
6326
  method: "POST",
4560
6327
  path: c.req.path,
4561
6328
  model: originalModel,
4562
6329
  resolvedModel,
4563
- inputTokens: responseBody.usage?.input_tokens,
4564
- outputTokens: responseBody.usage?.output_tokens,
6330
+ inputTokens: usage?.input_tokens,
6331
+ outputTokens: usage?.output_tokens,
4565
6332
  status: response.status
4566
6333
  }, selectedModel, startTime);
4567
6334
  if (debugEnabled) consola.debug("Non-streaming response from Copilot /v1/messages:", JSON.stringify(responseBody).slice(0, 2e3));
@@ -4608,48 +6375,6 @@ function resolveModelInBody$1(rawBody) {
4608
6375
  selectedModel
4609
6376
  };
4610
6377
  }
4611
- const EFFORT_ORDER = [
4612
- "low",
4613
- "medium",
4614
- "high",
4615
- "xhigh"
4616
- ];
4617
- /**
4618
- * Bucket a thinking budget into a Copilot reasoning-effort string.
4619
- * `<2000`→low, `<8000`→medium, `<24000`→high, else→xhigh.
4620
- * Defaults missing/non-numeric budgets to 8000 ("high").
4621
- */
4622
- function bucketEffort(budget) {
4623
- const n = typeof budget === "number" && Number.isFinite(budget) ? budget : 8e3;
4624
- if (n < 2e3) return "low";
4625
- if (n < 8e3) return "medium";
4626
- if (n < 24e3) return "high";
4627
- return "xhigh";
4628
- }
4629
- /**
4630
- * Clamp a bucketed effort to the closest value in `supported`. Ties
4631
- * resolve to the lower-tier option (per EFFORT_ORDER).
4632
- *
4633
- * Iterates EFFORT_ORDER (canonical low→xhigh) so the first match on a
4634
- * given distance is always the lower-tier value, regardless of input
4635
- * order in `supported`.
4636
- */
4637
- function clampEffort(bucketed, supported) {
4638
- if (supported.includes(bucketed)) return bucketed;
4639
- const targetIdx = EFFORT_ORDER.indexOf(bucketed);
4640
- let best;
4641
- let bestDist = Infinity;
4642
- for (let i = 0; i < EFFORT_ORDER.length; i++) {
4643
- const value = EFFORT_ORDER[i];
4644
- if (!supported.includes(value)) continue;
4645
- const dist = Math.abs(i - targetIdx);
4646
- if (dist < bestDist) {
4647
- bestDist = dist;
4648
- best = value;
4649
- }
4650
- }
4651
- return best ?? bucketed;
4652
- }
4653
6378
  /**
4654
6379
  * Clamp `body.output_config.effort` to the model's
4655
6380
  * `capabilities.supports.reasoning_effort` allowlist. Mutates `body`
@@ -4709,8 +6434,9 @@ function translateThinking(body, model) {
4709
6434
  if (!model?.capabilities?.supports?.adaptive_thinking) return false;
4710
6435
  const thinking = body.thinking;
4711
6436
  if (!thinking || typeof thinking !== "object") return false;
4712
- if (thinking.type !== "enabled") return false;
4713
- const bucketed = bucketEffort(thinking.budget_tokens);
6437
+ const t = thinking;
6438
+ if (t.type !== "enabled") return false;
6439
+ const bucketed = bucketEffort(t.budget_tokens);
4714
6440
  const supported = model.capabilities.supports.reasoning_effort;
4715
6441
  const effort = Array.isArray(supported) && supported.length > 0 ? clampEffort(bucketed, supported) : bucketed;
4716
6442
  body.thinking = { type: "adaptive" };
@@ -4732,9 +6458,10 @@ function translateThinking(body, model) {
4732
6458
  function sanitizeCacheControl$1(body) {
4733
6459
  let stripped = false;
4734
6460
  function stripScope(block) {
4735
- if (block.cache_control?.scope !== void 0) {
4736
- delete block.cache_control.scope;
4737
- if (Object.keys(block.cache_control).length === 0) delete block.cache_control;
6461
+ const cc = block.cache_control;
6462
+ if (cc?.scope !== void 0) {
6463
+ delete cc.scope;
6464
+ if (Object.keys(cc).length === 0) delete block.cache_control;
4738
6465
  stripped = true;
4739
6466
  }
4740
6467
  }
@@ -5661,6 +7388,134 @@ function parseSharedArgs(args) {
5661
7388
  };
5662
7389
  }
5663
7390
  /**
7391
+ * Non-Claude models we surface as first-class, selectable rows in Claude
7392
+ * Code's model picker (Phase 3 of native-non-claude-models). The main
7393
+ * agent loop runs on them through the `/v1/messages` translation shim
7394
+ * (`src/lib/anthropic-translate/*`, branched in `routes/messages/handler.ts`)
7395
+ * that forwards non-Claude targets to Copilot `/responses` (gpt) or
7396
+ * `/chat/completions` (gemini). The exact gemini id is
7397
+ * `gemini-3.1-pro-preview` (preview slug — NOT `gemini-3.1-pro`).
7398
+ *
7399
+ * Display labels only: the gateway-model cache schema Claude Code reads is
7400
+ * `{id, display_name?}` per model — there is NO per-model context-window
7401
+ * field, so context accounting for a selected row uses Claude Code's
7402
+ * default window (safe under-accounting: it compacts earlier than the real
7403
+ * 1M/400k window, never overflows). See `seedGatewayModelCache`.
7404
+ */
7405
+ const NATIVE_NON_CLAUDE_MODELS = [
7406
+ {
7407
+ id: "gpt-5.5",
7408
+ displayName: "GPT-5.5"
7409
+ },
7410
+ {
7411
+ id: "gpt-5.3-codex",
7412
+ displayName: "GPT-5.3 Codex"
7413
+ },
7414
+ {
7415
+ id: "gemini-3.5-flash",
7416
+ displayName: "Gemini 3.5 Flash"
7417
+ },
7418
+ {
7419
+ id: "gemini-3.1-pro-preview",
7420
+ displayName: "Gemini 3.1 Pro (preview)"
7421
+ }
7422
+ ];
7423
+ /**
7424
+ * The subset of `NATIVE_NON_CLAUDE_MODELS` actually present in the live
7425
+ * Copilot catalog. License tiers differ (gpt-5.5 needs
7426
+ * pro_plus/business/enterprise/max; gemini-3.5-flash is absent on
7427
+ * edu/individual_trial), so a model missing from the catalog is silently
7428
+ * dropped — the caller then neither enables discovery nor writes a cache
7429
+ * for it, and lesser tiers see the unchanged picker. Pure (reads
7430
+ * `state.models`), so it is unit-testable without side effects.
7431
+ */
7432
+ function nativeSelectableModelsInCatalog() {
7433
+ const catalog = state.models?.data;
7434
+ if (!catalog || catalog.length === 0) return [];
7435
+ const present = new Set(catalog.map((m) => m.id));
7436
+ return NATIVE_NON_CLAUDE_MODELS.filter((m) => present.has(m.id)).map((m) => ({
7437
+ id: m.id,
7438
+ display_name: m.displayName
7439
+ }));
7440
+ }
7441
+ /**
7442
+ * Pre-seed Claude Code's gateway-model discovery cache so the non-Claude
7443
+ * models appear as selectable picker rows WITHOUT the network fetch.
7444
+ *
7445
+ * Verified against the installed Claude Code build (2.1.201): the picker
7446
+ * builder reads `<CLAUDE_CONFIG_DIR>/cache/gateway-models.json`
7447
+ * (schema `{baseUrl: string, fetchedAt: number, models: [{id, display_name?}]}`)
7448
+ * and — when gateway discovery is enabled (first-party auth mode +
7449
+ * `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` set + a non-`api.anthropic.com`
7450
+ * `ANTHROPIC_BASE_URL`, all true for the proxy) — maps each cached model to
7451
+ * a picker row `{value: id, label: display_name}`. Critically, the
7452
+ * cache-READ path applies NO id filter; the `/^(claude|anthropic)/i` filter
7453
+ * lives ONLY in the network-FETCH path. So a pre-seeded cache can carry the
7454
+ * real Copilot ids (`gpt-5.5`, `gemini-3.1-pro-preview`, …) — no `claude-*`
7455
+ * alias needed — and selecting a row sends that real id, which
7456
+ * `resolveModel()` exact-matches and the `/v1/messages` shim routes.
7457
+ *
7458
+ * The network fetch never overwrites this seed: it bails when nonessential
7459
+ * traffic is disabled, and the proxy ALWAYS sets
7460
+ * `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, so the fetch returns before
7461
+ * it can write. The seed is therefore authoritative for the session.
7462
+ *
7463
+ * `baseUrl` MUST equal the `ANTHROPIC_BASE_URL` Claude Code sees
7464
+ * (`serverUrl`) or the cache is discarded. `configDir` defaults to
7465
+ * `PATHS.CLAUDE_CONFIG_DIR` — the same dir the proxy points
7466
+ * `CLAUDE_CONFIG_DIR` at — so the write target and Claude Code's read
7467
+ * target are identical by construction.
7468
+ *
7469
+ * Best-effort: every failure is swallowed — a missing picker row must never
7470
+ * break launch. This is coupled to Claude Code's internal cache path/schema;
7471
+ * if a future build changes them the read simply ignores the seed and the
7472
+ * rows don't appear (graceful degradation). Returns whether a file was
7473
+ * written (for tests/observability).
7474
+ *
7475
+ * The write is atomic (temp file in the same dir + rename) so a Claude Code
7476
+ * read can never observe a torn/partial JSON (which its safeParse would
7477
+ * reject, dropping the rows). Rename-over-existing is atomic on POSIX and
7478
+ * Windows (libuv MoveFileEx REPLACE_EXISTING).
7479
+ */
7480
+ function seedGatewayModelCache(serverUrl, models$1, configDir = PATHS.CLAUDE_CONFIG_DIR) {
7481
+ if (models$1.length === 0) return false;
7482
+ const cacheDir = nodePath$1.join(configDir, "cache");
7483
+ const target = nodePath$1.join(cacheDir, "gateway-models.json");
7484
+ const tmp = nodePath$1.join(cacheDir, `gateway-models.json.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
7485
+ try {
7486
+ fs$2.mkdirSync(cacheDir, { recursive: true });
7487
+ const payload = {
7488
+ baseUrl: serverUrl,
7489
+ fetchedAt: Date.now(),
7490
+ models: models$1.map((m) => ({
7491
+ id: m.id,
7492
+ display_name: m.display_name
7493
+ }))
7494
+ };
7495
+ fs$2.writeFileSync(tmp, JSON.stringify(payload), "utf-8");
7496
+ fs$2.renameSync(tmp, target);
7497
+ return true;
7498
+ } catch {
7499
+ try {
7500
+ fs$2.rmSync(tmp, { force: true });
7501
+ } catch {}
7502
+ return false;
7503
+ }
7504
+ }
7505
+ /**
7506
+ * Remove any seeded gateway-model cache. Called when the current catalog
7507
+ * carries none of the target models, so a user who has pinned
7508
+ * `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` on cannot surface rows for
7509
+ * models that are no longer available. Best-effort (per-launch config dirs
7510
+ * make a stale file rare, but this closes the pinned-port + catalog-change
7511
+ * seam). Never throws.
7512
+ */
7513
+ function clearGatewayModelCache(configDir = PATHS.CLAUDE_CONFIG_DIR) {
7514
+ try {
7515
+ fs$2.rmSync(nodePath$1.join(configDir, "cache", "gateway-models.json"), { force: true });
7516
+ } catch {}
7517
+ }
7518
+ /**
5664
7519
  * Build environment variables for Claude Code.
5665
7520
  *
5666
7521
  * The parent env is sanitized of every key in `STRIPPED_PARENT_ENV_KEYS`
@@ -5704,16 +7559,17 @@ function getClaudeCodeEnvVars(serverUrl, model) {
5704
7559
  const vars = {
5705
7560
  ANTHROPIC_BASE_URL: serverUrl,
5706
7561
  CLAUDE_CONFIG_DIR: PATHS.CLAUDE_CONFIG_DIR,
5707
- MCP_TIMEOUT: "2100000",
5708
- MCP_TOOL_TIMEOUT: "2100000",
5709
7562
  DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
5710
7563
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
5711
7564
  DISABLE_TELEMETRY: "1"
5712
7565
  };
5713
7566
  if (model) vars.ANTHROPIC_MODEL = model;
5714
- if (process.env.ANTHROPIC_SMALL_FAST_MODEL === void 0) vars.ANTHROPIC_SMALL_FAST_MODEL = "claude-sonnet-4-6";
5715
- if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL === void 0) vars.ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-4-6";
5716
- if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === void 0) vars.ANTHROPIC_DEFAULT_HAIKU_MODEL = "claude-haiku-4-5";
7567
+ const mcpToolTimeoutMs = String(resolveMcpToolTimeoutMs());
7568
+ if (process.env.MCP_TIMEOUT === void 0) vars.MCP_TIMEOUT = mcpToolTimeoutMs;
7569
+ if (process.env.MCP_TOOL_TIMEOUT === void 0) vars.MCP_TOOL_TIMEOUT = mcpToolTimeoutMs;
7570
+ if (process.env.ANTHROPIC_SMALL_FAST_MODEL === void 0) vars.ANTHROPIC_SMALL_FAST_MODEL = "claude-sonnet-5";
7571
+ if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL === void 0) vars.ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-5";
7572
+ if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === void 0) vars.ANTHROPIC_DEFAULT_HAIKU_MODEL = "claude-sonnet-5";
5717
7573
  if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL === void 0) vars.ANTHROPIC_DEFAULT_OPUS_MODEL = "claude-opus-4-8";
5718
7574
  if (process.env.CLAUDE_CODE_PLAN_V2_AGENT_COUNT === void 0) vars.CLAUDE_CODE_PLAN_V2_AGENT_COUNT = "7";
5719
7575
  for (const key of [
@@ -5723,6 +7579,10 @@ function getClaudeCodeEnvVars(serverUrl, model) {
5723
7579
  "CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING",
5724
7580
  "CLAUDE_CODE_ENABLE_TASKS"
5725
7581
  ]) if (process.env[key] === void 0) vars[key] = "1";
7582
+ const nativeModels = nativeSelectableModelsInCatalog();
7583
+ if (nativeModels.length > 0) {
7584
+ if (seedGatewayModelCache(serverUrl, nativeModels) && process.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY === void 0 && vars.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY === void 0) vars.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
7585
+ } else clearGatewayModelCache();
5726
7586
  if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
5727
7587
  return vars;
5728
7588
  }
@@ -5750,60 +7610,116 @@ function getCodexEnvVars(serverUrl) {
5750
7610
 
5751
7611
  //#endregion
5752
7612
  //#region src/claude.ts
7613
+ const claudeArgs = {
7614
+ ...sharedServerArgs,
7615
+ model: {
7616
+ alias: "m",
7617
+ type: "string",
7618
+ description: "Override the default model for Claude Code. Accepts a full slug (e.g. claude-opus-4-7) or an Opus family shorthand (e.g. 4.7, 4.8, 4.6) which expands to the best variant for that family — adding the [1m] suffix when a 1M-context backend is in the catalog."
7619
+ },
7620
+ "codex-mcp": {
7621
+ type: "boolean",
7622
+ default: true,
7623
+ description: "Wire peer-model MCP personas (codex-critic, codex-reviewer, gemini-critic) into the spawned Claude Code session"
7624
+ },
7625
+ "codex-cli": {
7626
+ type: "boolean",
7627
+ default: false,
7628
+ description: "Add a `codex mcp-server` stdio backend so codex-implementer can mutate files. Requires codex CLI 0.129+; gracefully falls back to HTTP-only if absent."
7629
+ },
7630
+ "codex-mcp-only": {
7631
+ type: "boolean",
7632
+ default: false,
7633
+ description: "Pass --strict-mcp-config to claude code so only github-router's MCP servers are loaded (hides user's existing MCP servers)"
7634
+ },
7635
+ stealth: {
7636
+ type: "boolean",
7637
+ default: false,
7638
+ description: "Opt back into VS Code-only beta header filtering. Loses leverage features (task budgets, token-efficient tools, prompt caching, etc.) but minimizes the wire-fingerprint difference from VS Code Copilot Chat. By default the `claude` subcommand enables extended/leverage betas because the spawned Claude Code already identifies itself via UA and other headers — partial stealth doesn't buy much."
7639
+ },
7640
+ "trust-gate": {
7641
+ type: "boolean",
7642
+ default: false,
7643
+ description: "Explicitly record consent for the structural Stop-gate in THIS repo (pinned to the repo's root-commit). The gate is ON BY DEFAULT when a harness is detected (consent-by-launching), so this is now mostly redundant; it stays for explicit/scripted use. Disable the gate entirely with GH_ROUTER_DISABLE_STOP_GATE=1."
7644
+ },
7645
+ "no-stop-gate": {
7646
+ type: "boolean",
7647
+ default: false,
7648
+ description: "Disable the structural Stop-gate for THIS session (same effect as GH_ROUTER_DISABLE_STOP_GATE=1). Intended for driven/automated sessions where a blocking Stop hook would hang the turn-end while a fleet driver waits."
7649
+ },
7650
+ "auto-update": {
7651
+ type: "boolean",
7652
+ default: true,
7653
+ description: "Check for and install the latest Claude Code on launch via `claude update` (throttled to once per hour via ~/.local/share/github-router/last-update-check). `claude update` respects the real install method (native installer or npm), so it never creates a conflicting second install; builds too old to support it fall back to `npm install -g @anthropic-ai/claude-code@latest`. Set to false (--no-auto-update) to check and warn only. Falls back gracefully if claude/npm/network unavailable."
7654
+ },
7655
+ "update-check": {
7656
+ type: "boolean",
7657
+ default: true,
7658
+ description: "Check the npm registry for a newer Claude Code version on launch and warn if stale (non-blocking ~500ms cost). Set to false (--no-update-check) to skip the check entirely (useful for offline/CI). Independent from --auto-update: --no-update-check implies no auto-install (nothing to install since we never check)."
7659
+ }
7660
+ };
7661
+ /**
7662
+ * Build the argv to forward to the spawned `claude` child from citty's
7663
+ * rawArgs (every token after the `claude` subcommand). citty is non-strict,
7664
+ * so an unknown flag such as `--print`/`-p`… `--output-format` is absorbed
7665
+ * into the parsed `args` object AND its value swallowed, instead of landing
7666
+ * in `args._`; forwarding only `args._` therefore drops headless flags unless
7667
+ * the user wrapped them in `--`. Here we walk rawArgs and forward every token
7668
+ * that is NOT one of github-router's OWN declared flags (or that flag's
7669
+ * consumed value). Everything after a literal `--` is forwarded verbatim,
7670
+ * preserving the prior explicit-passthrough behavior.
7671
+ *
7672
+ * A child flag whose NAME collides with a github-router flag (`-p`/`--port`,
7673
+ * `-v`/`--verbose`, `-m`/`--model`, `-a`/`--account-type`, `-r`/`--rate-limit`,
7674
+ * `-g`/`--github-token`) is owned by github-router; forward it to the child
7675
+ * explicitly after `--` (e.g. `github-router claude -- -p`). Every other Claude
7676
+ * flag (`--print`, `--output-format`, `--resume`, `--continue`, …) flows
7677
+ * through automatically.
7678
+ */
7679
+ function collectChildPassthroughArgs(rawArgs, argsDef) {
7680
+ const known = /* @__PURE__ */ new Set();
7681
+ const stringTyped = /* @__PURE__ */ new Set();
7682
+ for (const [name$1, def] of Object.entries(argsDef)) {
7683
+ const rawAlias = "alias" in def ? def.alias : void 0;
7684
+ const aliases = rawAlias === void 0 ? [] : Array.isArray(rawAlias) ? rawAlias : [rawAlias];
7685
+ for (const n of [name$1, ...aliases]) {
7686
+ known.add(n);
7687
+ if (def.type === "string") stringTyped.add(n);
7688
+ }
7689
+ }
7690
+ const forwarded = [];
7691
+ for (let i = 0; i < rawArgs.length; i++) {
7692
+ const tok = rawArgs[i];
7693
+ if (tok === "--") {
7694
+ forwarded.push(...rawArgs.slice(i + 1));
7695
+ break;
7696
+ }
7697
+ if (tok === "-" || !tok.startsWith("-")) {
7698
+ forwarded.push(tok);
7699
+ continue;
7700
+ }
7701
+ const doubleDash = tok.startsWith("--");
7702
+ const afterDashes = tok.slice(doubleDash ? 2 : 1);
7703
+ const eq = afterDashes.indexOf("=");
7704
+ const rawName = eq >= 0 ? afterDashes.slice(0, eq) : afterDashes;
7705
+ const hasInlineValue = eq >= 0;
7706
+ const negated = doubleDash && rawName.startsWith("no-");
7707
+ const baseName = negated ? rawName.slice(3) : rawName;
7708
+ if (!(known.has(rawName) || negated && known.has(baseName))) {
7709
+ forwarded.push(tok);
7710
+ continue;
7711
+ }
7712
+ if (!negated && !hasInlineValue && stringTyped.has(rawName) && i + 1 < rawArgs.length && rawArgs[i + 1] !== "--" && !rawArgs[i + 1].startsWith("-")) i++;
7713
+ }
7714
+ return forwarded;
7715
+ }
5753
7716
  const claude = defineCommand({
5754
7717
  meta: {
5755
7718
  name: "claude",
5756
7719
  description: "Start the proxy server and launch Claude Code"
5757
7720
  },
5758
- args: {
5759
- ...sharedServerArgs,
5760
- model: {
5761
- alias: "m",
5762
- type: "string",
5763
- description: "Override the default model for Claude Code. Accepts a full slug (e.g. claude-opus-4-7) or an Opus family shorthand (e.g. 4.7, 4.8, 4.6) which expands to the best variant for that family — adding the [1m] suffix when a 1M-context backend is in the catalog."
5764
- },
5765
- "codex-mcp": {
5766
- type: "boolean",
5767
- default: true,
5768
- description: "Wire peer-model MCP personas (codex-critic, codex-reviewer, gemini-critic) into the spawned Claude Code session"
5769
- },
5770
- "codex-cli": {
5771
- type: "boolean",
5772
- default: false,
5773
- description: "Add a `codex mcp-server` stdio backend so codex-implementer can mutate files. Requires codex CLI 0.129+; gracefully falls back to HTTP-only if absent."
5774
- },
5775
- "codex-mcp-only": {
5776
- type: "boolean",
5777
- default: false,
5778
- description: "Pass --strict-mcp-config to claude code so only github-router's MCP servers are loaded (hides user's existing MCP servers)"
5779
- },
5780
- stealth: {
5781
- type: "boolean",
5782
- default: false,
5783
- description: "Opt back into VS Code-only beta header filtering. Loses leverage features (task budgets, token-efficient tools, prompt caching, etc.) but minimizes the wire-fingerprint difference from VS Code Copilot Chat. By default the `claude` subcommand enables extended/leverage betas because the spawned Claude Code already identifies itself via UA and other headers — partial stealth doesn't buy much."
5784
- },
5785
- "trust-gate": {
5786
- type: "boolean",
5787
- default: false,
5788
- description: "Explicitly record consent for the structural Stop-gate in THIS repo (pinned to the repo's root-commit). The gate is ON BY DEFAULT when a harness is detected (consent-by-launching), so this is now mostly redundant; it stays for explicit/scripted use. Disable the gate entirely with GH_ROUTER_DISABLE_STOP_GATE=1."
5789
- },
5790
- "no-stop-gate": {
5791
- type: "boolean",
5792
- default: false,
5793
- description: "Disable the structural Stop-gate for THIS session (same effect as GH_ROUTER_DISABLE_STOP_GATE=1). Intended for driven/automated sessions where a blocking Stop hook would hang the turn-end while a fleet driver waits."
5794
- },
5795
- "auto-update": {
5796
- type: "boolean",
5797
- default: true,
5798
- description: "Check for and install the latest Claude Code on launch via `claude update` (throttled to once per hour via ~/.local/share/github-router/last-update-check). `claude update` respects the real install method (native installer or npm), so it never creates a conflicting second install; builds too old to support it fall back to `npm install -g @anthropic-ai/claude-code@latest`. Set to false (--no-auto-update) to check and warn only. Falls back gracefully if claude/npm/network unavailable."
5799
- },
5800
- "update-check": {
5801
- type: "boolean",
5802
- default: true,
5803
- description: "Check the npm registry for a newer Claude Code version on launch and warn if stale (non-blocking ~500ms cost). Set to false (--no-update-check) to skip the check entirely (useful for offline/CI). Independent from --auto-update: --no-update-check implies no auto-install (nothing to install since we never check)."
5804
- }
5805
- },
5806
- async run({ args }) {
7721
+ args: claudeArgs,
7722
+ async run({ args, rawArgs }) {
5807
7723
  if (!process$1.stdout.isTTY) {
5808
7724
  consola.error("The claude subcommand requires a TTY (interactive terminal).");
5809
7725
  process$1.exit(1);
@@ -5872,7 +7788,7 @@ const claude = defineCommand({
5872
7788
  const banner = chosenSlug === resolvedSlug ? chosenSlug : `${chosenSlug} → ${resolvedSlug}`;
5873
7789
  process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code (${banner})...\n`);
5874
7790
  const envVars = getClaudeCodeEnvVars(serverUrl, chosenSlug);
5875
- const extraArgs = args._ ?? [];
7791
+ const extraArgs = collectChildPassthroughArgs(rawArgs, claudeArgs);
5876
7792
  if (toolbeltEnabled()) {
5877
7793
  provisionToolbelt().catch((err) => consola.debug("Toolbelt provisioning failed:", err));
5878
7794
  const toolbeltLine = buildToolbeltAwareness(availableToolCommands());
@@ -5890,6 +7806,7 @@ const claude = defineCommand({
5890
7806
  await removeOwnClaudeConfigMirror();
5891
7807
  };
5892
7808
  let onShutdown = baseShutdown;
7809
+ let peerAwarenessSnippet;
5893
7810
  if (args["codex-mcp"] !== false) try {
5894
7811
  const requestedCli = args["codex-cli"] ?? false;
5895
7812
  const backend = resolveCodexCliBackend({
@@ -5912,7 +7829,10 @@ const claude = defineCommand({
5912
7829
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
5913
7830
  codexCli: backend === "cli",
5914
7831
  geminiAvailable,
5915
- groupKeys
7832
+ groupKeys,
7833
+ workerToolsAvailable: workerToolsEnabled(),
7834
+ browseAvailable: browseAgentEnabled(),
7835
+ implementerModel: implementerSubagentModel()
5916
7836
  });
5917
7837
  state.peerMcpNonce = runtime.nonce;
5918
7838
  envVars.GH_ROUTER_HOOK_MCP_URL = serverUrl;
@@ -5945,6 +7865,16 @@ const claude = defineCommand({
5945
7865
  } catch (err) {
5946
7866
  consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`);
5947
7867
  }
7868
+ if (!injected.ok) consola.warn("Workers non-blocking guard NOT registered: subagent MCP injection fell back to parent-only (--mcp-config), so worker-* dispatchers cannot reach the workers server. Raw (blocking) worker tools remain usable on the main thread this session.");
7869
+ else if (process$1.env.GH_ROUTER_DISABLE_WORKER_GUARD === "1") consola.info("Workers non-blocking guard disabled via GH_ROUTER_DISABLE_WORKER_GUARD=1 — raw mcp__workers__* is callable on the main thread (blocking); the worker-* background agents remain the steered, non-blocking default.");
7870
+ else try {
7871
+ const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
7872
+ const workersKey = workersKeyOf(groupKeys);
7873
+ const modes = activeDispatchModes({ browse: browseAgentEnabled() });
7874
+ await injectStopHookIntoSettingsFile(settingsPath, buildWorkerGuardHookCommand(process$1.execPath, process$1.argv[1], workersKey, modes), "PreToolUse", 10, guardToolMatcher(workersKey, modes));
7875
+ } catch (err) {
7876
+ consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`);
7877
+ }
5948
7878
  if (skillsWritten > 0) {
5949
7879
  const skillNames = skillsToWrite.map((s) => `/${s.name}`).join(", ");
5950
7880
  process$1.stderr.write(`Injected skills (${skillsWritten}/${skillsToWrite.length}): ${skillNames}.\n`);
@@ -6084,7 +8014,7 @@ const claude = defineCommand({
6084
8014
  agentToolsAvailable: agentToolsEnabled(),
6085
8015
  groupKeys
6086
8016
  });
6087
- extraArgs.push("--append-system-prompt", peerSnippet);
8017
+ peerAwarenessSnippet = peerSnippet;
6088
8018
  try {
6089
8019
  await appendPeerAwarenessToMirroredClaudeMd(peerSnippet);
6090
8020
  } catch (err) {
@@ -6098,6 +8028,12 @@ const claude = defineCommand({
6098
8028
  } catch (err) {
6099
8029
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
6100
8030
  }
8031
+ extraArgs.push("--append-system-prompt", peerAwarenessSnippet ? `${OPERATING_DEFAULTS_DIRECTIVE}\n\n${peerAwarenessSnippet}` : OPERATING_DEFAULTS_DIRECTIVE);
8032
+ try {
8033
+ await prependOperatingDefaultsToMirroredClaudeMd();
8034
+ } catch (err) {
8035
+ consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
8036
+ }
6101
8037
  launchChild({
6102
8038
  kind: "claude-code",
6103
8039
  envVars,
@@ -6373,7 +8309,7 @@ async function postJson(url, payload, opts) {
6373
8309
  * no such handle. Hooks always receive piped/redirected stdin, so this never
6374
8310
  * blocks (guarded against an interactive TTY, and any error -> "").
6375
8311
  */
6376
- function readStdin$3() {
8312
+ function readStdin$4() {
6377
8313
  try {
6378
8314
  if (process.stdin.isTTY) return "";
6379
8315
  return readFileSync(0, "utf8");
@@ -6404,7 +8340,7 @@ const internalPromptSubmit = defineCommand({
6404
8340
  },
6405
8341
  async run() {
6406
8342
  try {
6407
- const stdin = readStdin$3();
8343
+ const stdin = readStdin$4();
6408
8344
  const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
6409
8345
  const runtime = hookMcpRuntimeFromEnv();
6410
8346
  let decision;
@@ -6464,7 +8400,7 @@ const internalPromptSubmit = defineCommand({
6464
8400
  * teardown and trips a `uv_async_send` assertion; a synchronous read has no such
6465
8401
  * handle. Hooks always receive piped stdin (guarded against a TTY; any error -> "").
6466
8402
  */
6467
- function readStdin$2() {
8403
+ function readStdin$3() {
6468
8404
  try {
6469
8405
  if (process.stdin.isTTY) return "";
6470
8406
  return readFileSync(0, "utf8");
@@ -6554,7 +8490,7 @@ const internalSessionBind = defineCommand({
6554
8490
  try {
6555
8491
  const out = typeof args.out === "string" ? args.out.trim() : "";
6556
8492
  if (!out) return;
6557
- const record = decodeSessionBind(readStdin$2());
8493
+ const record = decodeSessionBind(readStdin$3());
6558
8494
  if (record) writeSidecar(out, record);
6559
8495
  } catch {}
6560
8496
  process.exitCode = 0;
@@ -6723,7 +8659,7 @@ ${body}
6723
8659
 
6724
8660
  //#endregion
6725
8661
  //#region src/internal-artifact-open.ts
6726
- function readStdin$1() {
8662
+ function readStdin$2() {
6727
8663
  try {
6728
8664
  if (process.stdin.isTTY) return "";
6729
8665
  return readFileSync(0, "utf8");
@@ -6817,7 +8753,7 @@ const internalArtifactOpen = defineCommand({
6817
8753
  },
6818
8754
  async run() {
6819
8755
  try {
6820
- const raw = readStdin$1();
8756
+ const raw = readStdin$2();
6821
8757
  let payload;
6822
8758
  try {
6823
8759
  payload = JSON.parse(raw);
@@ -6845,7 +8781,7 @@ const internalArtifactOpen = defineCommand({
6845
8781
  * no such handle. Hooks always receive piped/redirected stdin, so this never
6846
8782
  * blocks (guarded against an interactive TTY, and any error -> "").
6847
8783
  */
6848
- function readStdin() {
8784
+ function readStdin$1() {
6849
8785
  try {
6850
8786
  if (process.stdin.isTTY) return "";
6851
8787
  return readFileSync(0, "utf8");
@@ -6972,7 +8908,7 @@ const internalStopHook = defineCommand({
6972
8908
  description: "Internal: the structural-gate Stop hook. Reads the Claude Code hook payload on stdin, runs the sealed gate, exits 2 (blocks the stop) on a red gate or gate-weakening diff."
6973
8909
  },
6974
8910
  async run() {
6975
- const stdin = readStdin();
8911
+ const stdin = readStdin$1();
6976
8912
  const reviewEnabled = stopReviewEnabled() && hookMcpRuntimeFromEnv() !== void 0;
6977
8913
  let transcriptPath = "";
6978
8914
  let userPrompt = "";
@@ -7047,9 +8983,10 @@ async function readPayload() {
7047
8983
  * live tree itself for anything beyond it, so a giant diff never blows the model
7048
8984
  * window. The Stop hook already caps the captured diff at 2 MiB. */
7049
8985
  const MAX_EMBEDDED_DIFF_BYTES = 200 * 1024;
7050
- /** Wall-clock the reviewer may take. Sized at the worker engine's own 30-min cap
7051
- * plus headroom this process is detached, so nothing waits on it; the bound
7052
- * only stops a hung request from lingering forever. */
8986
+ /** Wall-clock the stop-gate reviewer may take. This is INDEPENDENT of the
8987
+ * autonomous worker's wall-clock cap (`DEFAULT_MAX_WALLCLOCK_MS`, now 6h)
8988
+ * it bounds this one detached review request. Nothing waits on this process,
8989
+ * so the bound only stops a hung request from lingering forever. */
7053
8990
  const REVIEW_TIMEOUT_MS = 2100 * 1e3;
7054
8991
  function buildReviewBrief(payload) {
7055
8992
  const diff = payload.diff.length > MAX_EMBEDDED_DIFF_BYTES ? `${payload.diff.slice(0, MAX_EMBEDDED_DIFF_BYTES)}\n\n[diff truncated at ${MAX_EMBEDDED_DIFF_BYTES} bytes — read the files directly for the rest]` : payload.diff;
@@ -7109,6 +9046,56 @@ const internalStopReview = defineCommand({
7109
9046
  }
7110
9047
  });
7111
9048
 
9049
+ //#endregion
9050
+ //#region src/internal-worker-guard.ts
9051
+ /**
9052
+ * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`) — same
9053
+ * rationale as `internal-prompt-submit`: an async stdin read leaves an in-flight
9054
+ * libuv FS request that races Windows process teardown. Hooks always receive
9055
+ * piped stdin (guarded against an interactive TTY; any error → "").
9056
+ */
9057
+ function readStdin() {
9058
+ try {
9059
+ if (process.stdin.isTTY) return "";
9060
+ return readFileSync(0, "utf8");
9061
+ } catch {
9062
+ return "";
9063
+ }
9064
+ }
9065
+ const internalWorkerGuard = defineCommand({
9066
+ meta: {
9067
+ name: "internal-worker-guard",
9068
+ description: "Internal: the workers PreToolUse guard. Denies a raw mcp__<workers>__<mode> call from the main agent (redirecting to the worker-<mode> background subagent) and allows it from the dispatcher subagent. Fails closed."
9069
+ },
9070
+ args: {
9071
+ "workers-key": {
9072
+ type: "string",
9073
+ description: "Resolved workers MCP config key (bare `workers` or `gh-router-workers`).",
9074
+ required: true
9075
+ },
9076
+ modes: {
9077
+ type: "string",
9078
+ description: "CSV of active worker modes, e.g. `explore,implement,review,plan,test`.",
9079
+ required: false
9080
+ }
9081
+ },
9082
+ run(ctx) {
9083
+ try {
9084
+ const stdin = readStdin();
9085
+ const workersKey = String(ctx.args["workers-key"] ?? "").trim();
9086
+ if (workersKey.length > 0) {
9087
+ const { output } = decideWorkerGuard({
9088
+ stdin,
9089
+ workersKey,
9090
+ modes: parseModesCsv(typeof ctx.args.modes === "string" ? ctx.args.modes : void 0)
9091
+ });
9092
+ if (output) process.stdout.write(output);
9093
+ }
9094
+ } catch {}
9095
+ process.exitCode = 0;
9096
+ }
9097
+ });
9098
+
7112
9099
  //#endregion
7113
9100
  //#region src/models.ts
7114
9101
  const models = defineCommand({
@@ -7394,7 +9381,7 @@ process.on("uncaughtException", (error) => {
7394
9381
  const version = getPackageVersion();
7395
9382
  const argv = process.argv.slice(2);
7396
9383
  const isVersionFlag = argv.includes("--version");
7397
- const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-artifact-open";
9384
+ const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-artifact-open" || argv[0] === "internal-worker-guard";
7398
9385
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
7399
9386
  await runMain(defineCommand({
7400
9387
  meta: {
@@ -7414,7 +9401,8 @@ await runMain(defineCommand({
7414
9401
  "internal-prompt-submit": internalPromptSubmit,
7415
9402
  "internal-stop-review": internalStopReview,
7416
9403
  "internal-session-bind": internalSessionBind,
7417
- "internal-artifact-open": internalArtifactOpen
9404
+ "internal-artifact-open": internalArtifactOpen,
9405
+ "internal-worker-guard": internalWorkerGuard
7418
9406
  }
7419
9407
  }));
7420
9408