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/browser-ext/manifest.json +1 -1
- package/dist/engine-DHFa2_hK.js +6 -0
- package/dist/{lifecycle-BSGk7sdE.js → lifecycle-BAug_A5A.js} +2 -2
- package/dist/{lifecycle-DGvk4z63.js → lifecycle-BUXxiltc.js} +2 -2
- package/dist/{lifecycle-DGvk4z63.js.map → lifecycle-BUXxiltc.js.map} +1 -1
- package/dist/{lifecycle-DyEXZu2z.js → lifecycle-ChPBRt6K.js} +2 -2
- package/dist/{lifecycle-DyEXZu2z.js.map → lifecycle-ChPBRt6K.js.map} +1 -1
- package/dist/{lifecycle-8EhIXG4L.js → lifecycle-L7Y7RJl4.js} +2 -2
- package/dist/main.js +2200 -212
- package/dist/main.js.map +1 -1
- package/dist/{paths-D0tJ_tms.js → paths-CTlT1nTo.js} +14 -9
- package/dist/paths-CTlT1nTo.js.map +1 -0
- package/dist/{paths-DhLJ9bLG.js → paths-DN3O54iE.js} +1 -1
- package/dist/{peer-mcp-personas-BQVOxB1i.js → peer-mcp-personas-Bfq3FPKc.js} +396 -117
- package/dist/peer-mcp-personas-Bfq3FPKc.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-CvY9mR3b.js +0 -6
- package/dist/paths-D0tJ_tms.js.map +0 -1
- package/dist/peer-mcp-personas-BQVOxB1i.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
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-
|
|
4
|
-
import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-
|
|
5
|
-
import { a as sweepRegistry } from "./lifecycle-
|
|
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
|
-
|
|
1271
|
+
const lines = [
|
|
1024
1272
|
"---",
|
|
1025
1273
|
`name: ${spec.name}`,
|
|
1026
|
-
`description: ${escapeYamlString(spec.description)}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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-
|
|
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
|
|
2254
|
-
- Use
|
|
2255
|
-
- Use
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
2563
|
-
- plan:
|
|
2564
|
-
- implement:
|
|
2565
|
-
- test:
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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/
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
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
|
-
*
|
|
4357
|
-
*
|
|
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
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
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
|
-
*
|
|
4373
|
-
*
|
|
4374
|
-
*
|
|
4375
|
-
*
|
|
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
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4876
|
+
const EFFORT_ORDER = [
|
|
4877
|
+
"low",
|
|
4878
|
+
"medium",
|
|
4879
|
+
"high",
|
|
4880
|
+
"xhigh"
|
|
4881
|
+
];
|
|
4380
4882
|
/**
|
|
4381
|
-
*
|
|
4382
|
-
*
|
|
4383
|
-
*
|
|
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
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
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
|
-
*
|
|
4395
|
-
*
|
|
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
|
|
4398
|
-
if (
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
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
|
-
*
|
|
4410
|
-
*
|
|
4411
|
-
*
|
|
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
|
-
|
|
4414
|
-
|
|
4415
|
-
let
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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 (
|
|
4422
|
-
|
|
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:
|
|
4564
|
-
outputTokens:
|
|
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
|
-
|
|
4713
|
-
|
|
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
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
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
|
-
|
|
5715
|
-
if (process.env.
|
|
5716
|
-
if (process.env.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
7051
|
-
*
|
|
7052
|
-
*
|
|
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
|
|