import type { CredentialId } from "./credential-id.js"; export type Mode = "detect" | "repair" | "strict"; export type AuthHeader = "x-api-key" | "authorization"; export type Kind = "anthropic" | "openai"; export type ProviderTierType = "free" | "mixed" | "subscription"; /** * The effort bands, WEAKEST FIRST — one declaration, and the type is derived from it rather than * the other way round. * * ⚠ Order is load-bearing: `dynamic-pools.ts` derives the degrade tail from this array's index, so * a band inserted out of order silently reorders which weaker members an exhausted pool falls back * to. * * ⚠ Why an ordered tuple and not four hand-lists: the member list stood restated in four places * across three modules, and only ONE of them was exhaustiveness-checked. `new Set([…])` * and `EffortLevel[]` both accept a SUBSET, and `cli.ts` validated against a bare `string[]` with * no link to the type at all — so a fifth band would have compiled everywhere and silently failed * in three of the four. `Record` (`EFFORT_FLOORS` in `benchmarks.ts`) is the shape * that already caught it, and is why that one is left as it is. */ export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh"]; /** Requested reasoning/capability band for an automatically discovered pool. */ export type EffortLevel = (typeof EFFORT_LEVELS)[number]; /** Claude tier names, longest-first so "haiku"/"sonnet" match before generic bits. */ export declare const CLAUDE_TIER_NAMES: readonly ["opus", "sonnet", "haiku", "fable"]; export type ClaudeTierName = (typeof CLAUDE_TIER_NAMES)[number]; /** Which requests a client-specific offload rule may reroute. */ export type OffloadScope = "subagents" | "all"; /** One independently controlled offload rule. */ export interface OffloadRule { enabled: boolean; scope: OffloadScope; /** * Refuse — loudly — rather than let this client's rerouted traffic reach a deployment that * is not assessed `free` (see `assessCost`; `unknown` counts as paid on purpose). Applies to * everything `subagentSpec` reroutes for this client, INCLUDING a per-call `@relay:` directive: * the flag is the owner's standing "this lane never spends money", and a subagent prompt must * not be able to out-rank it. The Anthropic passthrough is primary quota and is never free. */ freeOnly?: boolean; } /** * `false`/`true` is the backwards-compatible global switch. The object form is keyed by * originating harness (`claude`, `codex`, or a future client name), with `default` available as * an explicit catch-all for a client that has not got a dedicated rule yet. */ export type OffloadConfig = boolean | Record; export interface ReshaperConfig { base: string; model: string; /** "anthropic": call /v1/messages. "openai": call /chat/completions (NIM/vLLM). */ kind: Kind; /** Provider name for provider-aware credential resolution (auth alias + passthrough rules). */ provider?: string; authEnv?: string; authHeader: AuthHeader; timeoutMs: number; } /** * What happens to the CALLER's own `Authorization`/`x-api-key` at a provider that declares no * `authEnv` of its own. See `buildForwardHeaders`. */ export type CredentialMode = "passthrough" | "contained"; /** * What shape a provider's own validator demands of the tool-call ids the relay puts on the wire. * * - `"preserve"` — forward the caller's ids verbatim. The default everywhere, and what the relay * did for its whole life before 2026-08-23: an id is linkage, so not touching it is the safest * thing a translation can do. * - `"strict9"` — rewrite every outbound `tool_calls[].id` / `tool_call_id` to mistral's stated * `^[a-zA-Z0-9]{9}$` shape. Mistral's `mistral-common` validator enforces it on BOTH halves of * the pair (and, from v13, linkage and uniqueness on top), so an Anthropic `toolu_01…` id is a * hard 400 there — see `src/openai-request.ts`. */ export type ToolCallIdMode = "preserve" | "strict9"; /** * Whether a replayed assistant tool call must carry gemini 3.x's `thought_signature` field. * * - `"none"` — emit nothing. The default everywhere, and byte for byte what this relay put on the * wire before 2026-08-23. * - `"sentinel"` — stamp Google's own documented opt-out token, * `skip_thought_signature_validator`, at `tool_calls[].extra_content.google.thought_signature` * on every replayed tool call. See `src/openai-request.ts` for the 400 that states the rule and * for why echoing a REAL signature is not an option here. */ export type ThoughtSignatureMode = "none" | "sentinel"; /** * How the relay maps the CALLER's reasoning/thinking intent onto an `openai`-kind target. * * - `"none"` — emit nothing (the pre-2026-09-10 behaviour byte for byte): the caller's * request-level `thinking` control and any `output_config.effort` are DROPPED, because a * guessed `reasoning_effort` would be an invention for a provider that never stated one. * - `"deepseek"` — DeepSeek's reasoning vocabulary. The caller's `thinking: {type:"disabled"}` * is forwarded verbatim as OpenAI's `thinking: {type:"disabled"}`; an explicit thinking-on * intent (a `thinking: {type:"enabled", budget_tokens}` block or an `output_config.effort`) * maps to `reasoning_effort` at the caller's stated level or the routed pool's effort band; * and when the caller sent NO thinking control at all the mapper defaults to * `thinking: {type:"disabled"}` — DeepSeek's thinking mode requires the prior turn's * `reasoning_content` to be replayed on a multi-turn conversation (HTTP 400 otherwise), and * this relay deliberately holds no store to round-trip it (see * docs/history/deepseek-responses-truncation-2026-09-09.md), so the default must not think. */ export type ReasoningMode = "none" | "deepseek"; /** * Per-provider WIRE-SHAPE quirks — things a specific host's request validator demands that the * protocol itself does not. Deliberately not routing configuration and deliberately not a * per-provider switch in `src/`: a labelled provider fact may live in code only while config can * override it (see the "Provider knowledge is data" invariant), which is exactly the shape here — * a base-host default that any explicit value beats in both directions. * * Two keys today, one per vendor rule the relay has first-party evidence for. An unknown key or * value is a HARD load error naming it: the `configured-limits` precedent — an ignored typo * silently no-ops while reading like a declaration that took effect. */ export interface ProviderCompatConfig { /** Absent ⇒ resolved from the base host by `resolveToolCallIdMode`. */ toolCallIds?: ToolCallIdMode; /** Absent ⇒ resolved from the base host by `resolveThoughtSignatureMode`. */ thoughtSignature?: ThoughtSignatureMode; /** Absent ⇒ resolved from the base host by `resolveReasoningMode`. */ reasoning?: ReasoningMode; } /** * Which HTTP endpoint an `openai`-kind provider speaks upstream: `"chat"` (`/chat/completions`, * the default) or `"responses"` (`/responses`). * * OpenCode Zen's contributor SKUs — Muse Spark 1.3 included — answer HTTP 500 on * `/chat/completions` and on Zen's Anthropic-shaped `/messages`, and 200 only on `/responses` * (measured 2026-09-04, `docs/history/muse-spark-1.3-opencode-zen-2026-09-04.md` rows 3 and 6-8). A third * `Kind` value for this would touch roughly 55 `kind === "openai"` sites across 19 files (same * doc, §3 route B); this narrower option forks only the request/response builders `src/backend.ts` * selects on, leaving discovery, catalog and key-check paths unchanged. Declaring it on an * `anthropic`-kind provider is a hard config-load error — that kind never speaks either OpenAI * endpoint. */ export declare const PROVIDER_WIRE_MODES: readonly ["chat", "responses"]; /** Derived from `PROVIDER_WIRE_MODES` — one list, never a hand-copied second declaration. */ export type ProviderWireMode = (typeof PROVIDER_WIRE_MODES)[number]; /** The closed set of axes an operator may assert: requests/tokens per minute/day. Nothing else. */ export declare const CONFIGURED_LIMIT_AXES: readonly ["rpm", "rpd", "tpm", "tpd"]; export type ConfiguredLimitAxis = (typeof CONFIGURED_LIMIT_AXES)[number]; /** Flat hard-cap axes, as declared inside a `hard` block. */ export type HardRateLimits = Partial>; /** One rate-limit figure per axis; an omitted axis is simply undeclared, never guessed. */ export interface ProviderRateLimits { rpm?: number; rpd?: number; tpm?: number; tpd?: number; /** * Operator-set REFUSAL ceilings (G2). */ hard?: HardRateLimits; } /** * A `limits` block: the flat axes plus optional per-deployment overrides keyed by BACKEND model * id. */ export interface ProviderLimitsConfig extends ProviderRateLimits { models?: Record; } /** A normalized provider credential declaration. Secrets are never held here. */ export interface ProviderCredentialConfig { label: string; authEnv: string; enabled?: boolean; /** `null` means all models; an empty array deliberately matches no models. */ models?: readonly string[] | null; /** * This slot's own operator-asserted rate limits, overriding the provider-level `limits` for * this key alone. */ limits?: ProviderLimitsConfig; } /** Non-secret identity and policy for one configured credential slot. */ export interface CredentialSlot { readonly credentialId: CredentialId; readonly provider: string; readonly label: string; readonly authEnv: string | undefined; readonly enabled: boolean; readonly models: readonly string[] | null; readonly origin: "implicit" | "legacy-authEnv" | "credentials"; readonly resolutionMode: "legacy-alias" | "declared-only"; readonly configIndex: number; } /** One HTTP backend provider in the registry (NIM, OpenRouter, Gemini API, …). */ export interface ProviderConfig { base: string; /** * "anthropic": provider speaks Anthropic Messages; forward as-is. * "openai": provider is OpenAI-compatible (NIM/vLLM/OpenRouter/Gemini-openai); * the proxy translates request+response via llm-bridge. */ kind: Kind; authEnv?: string; /** Explicit multi-key declarations. Presence of an empty array means an empty fleet. */ credentials?: ProviderCredentialConfig[]; /** * What to do with the caller's own credential when this provider declares no `authEnv`: * - `"passthrough"` — forward it. This is the declaration that makes an Anthropic * passthrough deliberate rather than a side effect of leaving `authEnv` out. * - `"contained"` — strip it and send none. The right answer for a keyless backend that * is not the caller's own vendor: a local daemon, a second relay, someone else's * Anthropic-format endpoint. * * Omitting it still forwards, so existing configs keep working, but config load warns for an * `anthropic`-kind provider. "This backend needs no key of its own" and "send this host the * user's subscription credential" are different intentions, and only the first one should be * inferable from an omission. Illegal together with `authEnv` — that pair states both at once. */ credentialMode?: CredentialMode; /** Maximum concurrent requests for this provider credential domain; null/omitted is unlimited. */ maxConcurrent?: number | null; /** Which header to inject the provider key into. Default: authorization (openai) / x-api-key (anthropic). */ authHeader: AuthHeader; /** Backend request deadline in ms. Default 120000. */ timeoutMs: number; /** * Inter-byte stall watchdog for STREAMED responses, in ms. Once a stream is being served, the * total `timeoutMs` deadline disarms — one flat deadline kills a healthy long generation at * minute two while letting a dead stream hang until the same minute two — and this watchdog * aborts only when NO byte arrives for this long. 0 keeps the old single-deadline behavior. * Default 90000 (fork-validated in freellmapi). Adoption review §1.2. */ stallTimeoutMs?: number; /** * Time-to-first-byte deadline for a NON-STREAMED attempt, in ms — separate from `timeoutMs` so a * backend that has produced no bytes at all fails fast while one that is merely slow to finish a * buffered body is not killed. "First byte" is the moment `fetch()` resolves (response headers * arrived); from there `timeoutMs` governs the body read exactly as before this existed. Default: * `stallTimeoutMs` when that is set (same intent — "no bytes for this long means dead") — a * value that is not explicit takes it, an explicit value wins over it. Otherwise OFF (no * first-byte deadline, the pre-existing behaviour). Never armed on a streamed attempt, which * already has `stallTimeoutMs`'s inter-byte watchdog once its own head is being served. `0` is a * hard config-load error naming the key — it would bound nothing while looking like it did. */ firstByteTimeoutMs?: number; /** "free": wholly free/free-tier catalog. "mixed": catalog contains free and paid models. */ tierType?: ProviderTierType; /** * Operator-asserted rate limits (spec §4 rung 3, basis "configured"). The provider-level block * is the default for every credential of this provider; a slot's own `limits` overrides it per * credential; a `models` entry overrides per deployment — each axis independently. See * `src/configured-limits.ts`. These never refuse a request by themselves; they feed the * availability/headroom surfaces. */ limits?: ProviderLimitsConfig; /** * Wire-shape quirks this host's own request validator enforces. Absent keys fall back to a * labelled base-host default (`resolveToolCallIdMode`, `resolveThoughtSignatureMode`); an * explicit value always wins. */ compat?: ProviderCompatConfig; /** * Which upstream endpoint an `openai`-kind provider speaks. Absent ⇒ `"chat"`. Hard config-load * error on an `anthropic`-kind provider, and on any value outside `ProviderWireMode`. See * `ProviderWireMode`. */ wire?: ProviderWireMode; /** Web URL where users can sign up or obtain API keys. */ signupUrl?: string; } /** * How an inbound request's `model` maps to a provider + backend model. * - `default`: fallback "provider/model" spec (string or array of target specs) when nothing else matches. * - `tiers`: Claude tier name ("opus"/"sonnet"/"haiku"/"fable") → "provider/model" spec or array. * - `pools`: named candidate list addressed as "pool/" — the ranked, discovery-friendly * alternative to pinning one model. A pool resolves to ALL its specs, so `benchmarkSort` ranks * them and the existing failover walks them in order. This is the only routing form that lets a * caller say "the best available coding model" instead of naming one; a bare namespaced spec is * deliberately verbatim and never ranked. * A request may also address a provider directly with a namespaced model id * ("nim/z-ai/glm-5.2") — the prefix picks the provider, the rest is the model. */ export interface Routing { default: string | string[]; tiers: Record; pools?: Record; /** Dynamic pool policies are normalized separately from their materialized target arrays. */ poolPolicies?: Record; /** * Pool → the specs in its DEGRADE TAIL: live members below the pool's effort band, appended so * an exhausted band still has somewhere to go. * * Materialized alongside `pools`, never written to config.json — it is derived state, and the * same refresh that rebuilds a pool rebuilds this. It exists so a served response can say the * answer came from below the requested band: degrading automatically is only acceptable if it is * never silent, since a capability downgrade that reads as an ordinary success is indistinguishable * from having got what you asked for. */ poolDegraded?: Record; /** * Tier → spec for SUBAGENT requests only (`cc_is_subagent=true`). Lets the dispatcher pick a * destination with the one per-call knob it actually has — the Agent tool's `model` enum * (sonnet|opus|haiku|fable) — without writing an agent file. `default` catches anything that * matches no tier. Main-conversation requests never consult this, which is what keeps * `routing.tiers` free to stay on an Anthropic passthrough. */ subagents?: Record; /** * Offload admission. The legacy boolean is a global subagent-only switch. The object form is * independently keyed by originating client (`claude`, `codex`, or a future name), and each * rule chooses whether it applies to marked subagents only or to every conversation from that * client. **Default false** in either form. */ offload?: OffloadConfig; benchmarkSort?: boolean; /** Ephemeral session affinity. Boolean shorthand uses the 30m/1,000-entry defaults. */ sticky?: StickyConfig; /** * Quota-as-demotion enforcement (spec §5.4 / Gap 12). `enforce` (default true) lets a SPENT * quota demote a candidate to the cooling band — provider-stated observations and * operator-declared limits only. `enforceLearned` (default false) additionally admits limits * LEARNED from vendor prose (decision M2): display-only until this is set to true. * * A demotion never drops and never refuses — it only reorders, expiring at the resetsAt the * evidence stated. Unknown quota has no effect whatsoever. */ quota?: QuotaEnforcementConfig; /** * Sustained MEASURED latency as a demotion term (owner decision 2026-08-30). **Default ON** — * absent means enabled with the tunable defaults in `src/latency-demotion.ts`. `false` is the * shorthand for `{ enabled: false }` and restores the pre-2026-08-30 behaviour exactly. * * Like quota, it only ever REORDERS: never drops, never refuses, and unmeasured latency has no * effect whatsoever. */ latency?: LatencyDemotionConfig; /** * Hedged attempts (owner proposal + decisions 2026-08-30, * docs/history/hedged-attempts-design-2026-08-30.md §7). **Default ON**, and confined to deployments * `assessCost()` calls FREE. * * ⚠ This is the FIRST behaviour here that does not merely reorder — it DUPLICATES a request onto * a second candidate. The `CLAUDE.md` invariant reads "Acting on counts is optional, always * announced, and may only reorder"; the owner amended it for this feature on 2026-08-30 and * bounded the duplication three ways: free deployments only (D1), the loser aborted the moment a * winner commits, and the response announcing it (`x-llm-relay-hedged`). * * `false` is the shorthand for `{ enabled: false }` and restores the pre-hedge behaviour exactly. */ hedge?: HedgeConfig; /** * Untested-free-members-first probation band (owner direction 2026-09-09). **Default ON** — * absent means enabled with `minSamples: 5`. * * Like latency and hedging, it only ever REORDERS (probation members lead; nothing is * dropped and nothing is refused), and unmeasured PAID/unknown deployments are unaffected. * * `false` is the documented shorthand for `{ enabled: false }` and restores the * pre-probation behaviour exactly, byte for byte. */ probation?: ProbationConfig; /** * Self-pacing against a STATED rate limit (owner direction 2026-09-10, built 2026-09-15). **Default * ON.** A cell whose provider-stated, operator-configured or LEARNED (`rate-limit-*` fact) ceiling * the relay's own trailing-window attempt count has reached joins a `paced` band behind `live` * and `slow`, so the next request goes elsewhere while the window drains. Counted from the * breaker's per-cell attempt-start log, which every client routing through the relay feeds. * * Like latency and probation it only ever REORDERS — nothing is dropped, nothing is refused — * and a limit nobody stated has no effect at all. `false` is the shorthand for * `{ enabled: false }` and restores the pre-pacing order exactly, byte for byte. */ pacing?: PacingConfig; /** * Post-commit CRAWL abort (backlog item 18, built 2026-09-09 after * `docs/history/post-commit-stall-measurement-2026-09-09.md` measured that both Claude Code and Codex * retry a stream that goes bad after content has already arrived — Claude Code once, downgraded * to non-streaming; Codex up to five times, staying streaming). A silent stall after commit is * already caught by `withStallWatchdog` at `stallTimeoutMs`; this catches the case nothing else * does — bytes keep arriving inside that inter-byte window while the sustained per-token rate, * over a sliding window, is far worse than the same deployment's own history supports. * * **Default ON.** `false` is the shorthand for `{ enabled: false }` and restores the pre-crawl * behaviour exactly — the watchdog is not installed at all. An object with no keys is legal and * means the defaults. The thresholds live in `src/stream-pipeline.ts` beside the measurement * that calibrated `msPerToken`. */ crawl?: CrawlWatchdogConfig; /** * Background lane re-probing (owner decision 2026-08-29, * docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata fresh is the relay's own * job, the way the ping loop already does for HTTP. **Default ON** — catalog probes are * metadata commands that spend no quota, and quota probes fire only for buckets carrying an * ACTIVE recorded death (an alive lane is re-tested by real use for free). Boolean shorthand * toggles `enabled` with the default intervals. Absent on a hand-built `Config` means the * defaults too — the cadence resolves absence itself. */ laneProbe?: LaneProbeSettings; /** * The automatic dispatch lane WALK (owner request 2026-09-06, * docs/history/dispatch-lane-walk-design-2026-09-06.md). **Default ON.** * * Before it, `dispatch` ran ONE lane and reported a failure when that lane was slow; the calling * agent then picked the next lane by hand, which is the friction the owner reported. With it, * the relay walks the ladder past a lane only after it has remained idle for `idleMs` with no * relay traffic, output, owned-process CPU increase or working-tree change; it pins the lane * that answers and demotes the lane it left. * * ⚠ Read ONLY by `llm-relay mcp`, exactly like `mcp` below — the MCP walk owns delegation * policy even when D1 asks the daemon's token-gated broker to own the physical process tree. * No PUBLIC MODEL HTTP turn spawns a lane. The daemon also reads the PIN and DEMOTION those * walks record, because ordering a ladder is not choosing to delegate. * * `false` is the shorthand for `{ enabled: false }` and restores the pre-walk behaviour exactly: * one lane per call, no memory. */ dispatchWalk?: DispatchWalkSettings; /** * Settings for `llm-relay mcp`, the stdio MCP server that exposes the dispatch verb to any MCP * host. Read ONLY by that server — the daemon never consults this block, because the daemon * never serves MCP and never spawns a lane for an HTTP turn. * * Absent means every default: the lane runs in the MCP server's own working directory unless the * caller names another, and any existing directory is accepted. */ mcp?: McpSettings; /** * Ordered dispatch ladder consulted by `/dispatch` — which LANE a host agent should hand a * whole delegated task to, and in what order to fall back. Distinct from `subagents`, which * routes one HTTP turn: a ladder rung may be an agent CLI that never traverses this proxy, * because its quota is client-bound and only the vendor's own binary can spend it. * * Absent means the relay expresses no opinion and dispatch order stays the host's to choose. */ ladder?: LadderRung[]; /** Tier-specific dispatch ladders. `dispatch --tier ` selects one. */ ladders?: Record; /** * How to reach a `relay` rung's spec by SHELLING OUT, for a host whose own traffic does not * reach this relay (see `src/host-routing.ts`). `/dispatch` substitutes the rung's spec into * `{spec}` and the task into `{task}`, and hands back an ordinary `cli` invoke. * * Declared, never invented: the relay must not learn what a `claude` binary is or how to * address one — that is the provider/model-agnostic invariant applied to lane rendering. Absent * means no transposition is possible, and such rungs are reported unreachable rather than * quietly replaced with something the operator never authorised. */ cliLane?: CliLaneTemplate; } export interface StickyRoutingConfig { enabled: boolean; ttlMs?: number; maxSessions?: number; } export type StickyConfig = boolean | StickyRoutingConfig; /** * `routing.quota`. Both keys are optional booleans with deliberate defaults: enforcement of * provider-stated/derived-from-configured figures is ON unless switched off (decision M1), and * learned prose parses stay display-only unless explicitly admitted (decision M2). An object with * neither key is legal and means exactly the defaults — writing it down is documentation, not a * behaviour change. * * G2 adds `hardCaps` (default true): an operator who wrote a `hard` block inside a `limits` * declaration meant it, so the refusal ceilings are live unless this switch turns every one of * them into a soft limit. false demotes nothing per request and logs nothing. */ export interface QuotaEnforcementConfig { /** Default true. false disables quota demotion entirely. */ enforce?: boolean; /** Default false. true additionally lets `derived:learned` figures gate routing. */ enforceLearned?: boolean; /** Default true. false treats every `hard` cap as an ordinary (soft) configured limit. */ hardCaps?: boolean; } /** * `routing.latency` — sustained MEASURED latency as a demotion term (owner decision 2026-08-30). * * **Default ON.** A candidate whose measured p95 exceeds `p95Ms`, over at least `minSamples` * measurable samples, joins the cooling band instead of leading the walk. It is a demotion, so the * worst case is a reorder: nothing is dropped and nothing is refused. * * `false` is the documented shorthand for `{ enabled: false }` and restores the pre-2026-08-30 * behaviour exactly. An object with no keys is legal and means the defaults — writing it down is * documentation, not a behaviour change. * * The thresholds live in `src/latency-demotion.ts` beside the measurement that calibrated them. */ export interface LatencyDemotionConfig { /** Default true. false disables latency demotion entirely. */ enabled?: boolean; /** Fallback ceiling: measured absolute p95 in ms. Above it, the candidate is demoted. */ p95Ms?: number; /** * PRIMARY ceiling: measured p95 latency per OUTPUT TOKEN, in ms. * * Absolute latency cannot compare a probe with a generation - a probe asks for one token, a real * request may produce hundreds and amortise the same fixed overhead. The per-token rate is the * fair figure, so it is tested first, over real request samples only. */ msPerToken?: number; /** Minimum measurable samples before latency may demote anything at all. */ minSamples?: number; } /** * `routing.hedge` — start the NEXT candidate beside a slow in-flight attempt, instead of after it * (owner proposal 2026-08-30; the four decisions are in * `docs/history/hedged-attempts-design-2026-08-30.md` §7). * * **Default ON, free deployments only.** That is owner decision D1, taken against the * recommendation of off-by-default. `assessCost()` treats an UNKNOWN price as paid, so the rule is * fail-safe in the only direction that matters: a duplicate can never land on a deployment whose * price this relay cannot establish. The stated cost is that hedging silently does not fire on many * members whose prices are simply unpublished. * * ⚠ **Hedging DUPLICATES; every other term here only reorders.** `false` is the documented * shorthand for `{ enabled: false }` and restores the pre-hedge behaviour exactly, byte for byte. * An object with no keys is legal and means the defaults. * * The thresholds live in `src/hedge-trigger.ts`. ⚠ `margin` and `minSamples` are still PLACEHOLDERS * awaiting calibration and say so at their definition — do not quote them as measurements. * `msPerInputToken` IS calibrated (`scripts/calibrate-hedge-floor.mjs`, 2026-09-04) — see its own * doc comment in `hedge-trigger.ts` for why the fit came back out of range and what shipped instead. */ export interface HedgeConfig { /** Default true. false disables hedging entirely. */ enabled?: boolean; /** * The floor's flat component, in ms. Owner direction 2026-09-04: the floor is no longer flat on * its own — see `msPerInputToken` — but this still bounds the SMALL-prompt case, where the * size-scaled component is negligible. Without it a deployment with a tiny p90 is hedged on * ordinary noise, and a fast pool duplicates almost every request. */ minFloorMs?: number; /** * LEGACY alias of `minFloorMs`, kept for backward compatibility — an operator config written * before 2026-09-04 (`{"floorMs": 8000}`) keeps loading and keeps meaning exactly what it always * meant: the floor never drops below 8000 ms. Honoured only when `minFloorMs` itself is absent; * `resolveHedgeSettings` in `hedge-trigger.ts` is the ONE place that resolves the alias, so a new * caller of that function can never re-decide the precedence. */ floorMs?: number; /** * The floor's size-scaled component, in ms per estimated INPUT token * (`estimateRequestTokens` in `metadata.ts`). Without it a large prompt is hedged against the * time it simply takes a healthy deployment to read the prompt, not against real slowness. */ msPerInputToken?: number; /** How far past the expected time an attempt must run before a hedge starts. */ margin?: number; /** Minimum samples before a measured statistic may set the bar instead of the floor. */ minSamples?: number; } /** * `routing.probation` — put an untested FREE deployment at the start of the pool so the relay * gathers data on it (owner direction 2026-09-09). * * **Default ON.** A free-class candidate with fewer than `minSamples` SERVED-REQUEST samples in * the probe dataset (the samples `recordRequestSample` writes — probe samples do not count) * joins a `probation` band AHEAD of `live`, in config order. It leaves the band by itself as * its request samples accumulate, so one untested member at a time gathers data. Breaker * cooling, credential faults, hard caps, quota demotion and latency demotion all OUTRANK * probation — a cooling probation member goes to `cooling`, a slow one to `slow`. * * `false` is the documented shorthand for `{ enabled: false }` and restores the pre-probation * behaviour exactly, byte for byte. An object with no keys is legal and means the defaults. * * An unmeasured free primary is already hedged (`hedge-trigger.ts`: unmeasured IS hedged), so * a probation member that hangs costs one hedge, not a timeout — no second mechanism here. */ export interface ProbationConfig { /** Default true. false disables the probation band entirely. */ enabled?: boolean; /** Minimum SERVED-REQUEST samples before a free deployment counts as measured. Default 5. */ minSamples?: number; } /** * `routing.pacing` — hold the relay's OWN request rate under a ceiling a deployment stated * (owner direction 2026-09-10: *"use rate-limited messages to calculate when it might need to * slow something down"*; built 2026-09-15, `src/pacing.ts`). * * **Default ON.** For each credential×model cell, every (axis, period) bucket carrying a stated * ceiling — a provider-stated quota header's `limit`, an operator-declared `limits` figure, or a * LEARNED `rate-limit-rpm|rpd|tpm|tpd` fact parsed from a 429 body — is held against the * attempts this relay itself started in the trailing window of that period (a sliding window * over the breaker's per-cell start log; tokens count the request's own input estimate). At or * past the ceiling the cell joins a `paced` band behind `live` and `slow`, ahead of the failure * bands, and leaves it by itself as the window drains. Breaker cooling, credential faults and * quota demotion outrank it. * * ⚠ A learned ceiling PACES here without the `routing.quota.enforceLearned` opt-in — that is the * owner's 2026-09-10 direction and the backlog property ("a 429 that states a window updates * that pacing without a human verdict"). `routing.quota`'s own M2 gate is untouched: it governs * the ALLOWANCE path (remaining ≤ 0 ⇒ cooling until reset), which is a different question. * * `false` is the documented shorthand for `{ enabled: false }` and restores the pre-pacing * order exactly, byte for byte. An object with no keys is legal and means the defaults. */ export interface PacingConfig { /** Default true. false disables the paced band entirely. */ enabled?: boolean; } /** * `routing.crawl` — abort a COMMITTED stream whose sustained per-token output rate, over a FULL * trailing window, is far worse than a deployment's own history supports (backlog item 18, built * 2026-09-09, arithmetic corrected the same day — see `DEFAULT_CRAWL_WINDOW_MS` in * `src/stream-pipeline.ts` for why the first pairing of defaults could never fire). * * **Default ON.** `false` is the shorthand for `{ enabled: false }` and restores the pre-crawl * behaviour exactly — the watchdog is never installed. An object with no keys is legal and means * the defaults. Every number must be finite and positive — the `routing.latency` precedent: a `0` * bounds nothing while looking like it does. * * The rule, in words (full detail on `resolveCrawlSettings` in `src/stream-pipeline.ts`): * `minTokens` output tokens must be observed since commit, over the WHOLE stream, before any * judgement runs at all; then a window is judged only once it is FULL (elapsed since commit at * least `windowMs`); `tokensInWindow` counts only tokens sampled in the trailing `windowMs`, and a * full window holding zero tokens yields no opinion (silence is `withStallWatchdog`'s job). * Otherwise the rate is `windowMs / tokensInWindow`, and the stream is CRAWLING — aborted — when * that rate exceeds `msPerToken`. * * The thresholds live in `src/stream-pipeline.ts` beside the measurement that calibrated * `msPerToken`. */ export interface CrawlWatchdogConfig { /** Default true. false disables the crawl watchdog entirely. */ enabled?: boolean; /** * The threshold, in ms per output token. A committed stream whose measured rate over a FULL * trailing window (`windowMs / tokensInWindow`) exceeds this is CRAWLING and gets aborted. */ msPerToken?: number; /** Width, in ms, of the trailing window the rate is measured over — judged only once this much * time has elapsed since commit. */ windowMs?: number; /** Minimum output tokens that must be observed SINCE COMMIT — across the whole stream, not just * the trailing window — before any judgement runs at all. */ minTokens?: number; } /** * Template for rendering a `relay` rung as a shelled-out CLI command. * * Same executable shape as a `cli` rung — the host runs `command` with `args`, applying `env` * (string sets, `null` unsets) — with one extra placeholder: `{spec}`, replaced by the rung's * routing spec so ONE template serves every pool and pinned model in the ladder. */ export interface CliLaneTemplate { command: string; /** Must contain `{spec}`; `{task}` too, on the same reasoning as a cli rung's args. */ args: string[]; /** Applied by the host when spawning. Placeholders are never substituted here. */ env?: Record; } /** A fixed configured prefix followed by automatically discovered free models. */ export interface PoolPolicy { preferred: string[]; include: "free"; /** Permanent user tombstones, applied to both the fixed prefix and discovered tail. */ exclude?: string[]; /** Optional evidence-aware effort band for the discovered tail. */ effort?: EffortLevel; } /** * One rung of `routing.ladder`. * * `cli` rungs are executed by the HOST (the relay never spawns a process — it is a proxy, not a * process supervisor); `relay` rungs are addressed through this proxy in the ordinary way. * `quota` names the balance a rung draws on so that rungs sharing one are cooled down together * and rungs that merely share a binary are not: one CLI can meter two model families against * two independent balances, and treating those as one bucket would skip a live lane. */ export interface LadderRung { id: string; kind: "cli" | "relay"; /** Parked rungs stay visible in the ladder but are never auto-selected. Default true. */ enabled: boolean; quota?: string; note?: string; /** cli rungs: the binary to run, and its args — one of which must contain the task placeholder. */ command?: string; args?: string[]; /** * cli rungs: environment the HOST applies when spawning the command. A string value sets the * variable; `null` unsets an inherited one. Both directions are load-bearing for the lane this * exists for — a `claude -p` child routed through this proxy: `ANTHROPIC_BASE_URL` must be SET * (a terminal-spawned `claude` honours it even though Claude Desktop pins its own sessions to * api.anthropic.com), and `CLAUDECODE`/`CLAUDE_CODE_SSE_PORT`/`CLAUDE_CODE_ENTRYPOINT`/ * `ANTHROPIC_API_KEY` must be UNSET or a child spawned from inside a Claude session inherits * the parent's harness wiring and refuses to start cleanly. The task placeholder is never * substituted here — env values are operator-authored routing, not task content. */ env?: Record; /** * cli rungs: the most jobs this MCP server process will run against THIS rung at once. A * dispatch walk whose turn reaches a rung already at this many spawned processes SKIPS it for * that walk rather than starting a competing one — see `mcp/lane-runner.ts` `LaneJobStore.inFlight` * and `mcp/server.ts`'s walk. Absent means unbounded, which is the byte-for-byte pre-existing * behaviour: nothing here changes for an operator who never sets it. * * ⚠ The ADMISSION count remains per MCP SERVER PROCESS by design, even though D1 moves physical * process ownership to the daemon. The originating MCP job store counts its own/recovered jobs; * another simultaneously-live MCP process is not folded into that count. Two host sessions can * therefore still exceed this figure together: this is a per-host cap, not a machine-wide semaphore. */ maxConcurrent?: number; /** * Legacy compatibility key. Parsed values warn and have NO routing effect; dispatch derives lane * capability from synced model evidence (or the dynamic pool's effort band). Retained in the type * so programmatic/older configs remain structurally readable while the key is phased out. */ capability?: EffortLevel; /** relay rungs: the spec to address (`pool/`, `/`, a provider name). */ spec?: string; } /** The request headers needed for protocol-specific subagent markers. */ export type RequestHeaders = Readonly>; /** A request routed to a concrete provider + backend model. */ export interface ResolvedTarget { provider: string; base: string; kind: Kind; /** Real backend model id (required for openai; absent = anthropic passthrough). */ model?: string; authEnv?: string; /** Normalized non-secret credential slots for this provider. */ credentialSlots?: readonly CredentialSlot[]; /** Carried from the provider: whether the caller's own credential may travel to this target. */ credentialMode?: CredentialMode; authHeader: AuthHeader; timeoutMs: number; /** Carried from the provider: inter-byte stall watchdog for streamed responses. */ stallTimeoutMs?: number; /** * RESOLVED time-to-first-byte deadline for a non-streamed attempt (`resolveFirstByteTimeoutMs`) * — an explicit `ProviderConfig.firstByteTimeoutMs`, or `stallTimeoutMs` as the default, or * absent when neither is set. Resolved here so the attempt runner is handed a number and never * re-derives a default from two provider fields. Absent (a hand-built target) means no * first-byte deadline, the pre-2026-09-09 behaviour byte for byte. */ firstByteTimeoutMs?: number; /** * RESOLVED outbound tool-call-id shape (`resolveToolCallIdMode`) — an explicit * `compat.toolCallIds` or the labelled base-host default. Resolved here so the request mapper * is handed a mode and never a provider identity to re-derive one from. Absent (a hand-built * target) reads as `"preserve"`, which is the pre-2026-08-23 behaviour byte for byte. */ toolCallIds?: ToolCallIdMode; /** * RESOLVED thought-signature mode (`resolveThoughtSignatureMode`) — an explicit * `compat.thoughtSignature` or the labelled base-host default. Resolved here for the same reason * as `toolCallIds`: the request mapper is handed a mode, never a provider identity to sniff one * from. Absent (a hand-built target) reads as `"none"` — the pre-2026-08-23 bytes exactly. */ thoughtSignature?: ThoughtSignatureMode; /** * RESOLVED wire mode (`ProviderConfig.wire`, absent ⇒ `"chat"`) — carried onto the target at * resolution time, exactly like `toolCallIds`/`thoughtSignature`, so `src/backend.ts` is handed * a mode and never re-derives one from provider identity. Absent (a hand-built target) reads as * `"chat"`, the pre-2026-09-09 behaviour byte for byte. */ wire?: ProviderWireMode; /** * RESOLVED reasoning-mapping mode (`resolveReasoningMode`) — an explicit `compat.reasoning` or * the labelled base-host default (`api.deepseek.com` ⇒ `"deepseek"`). Resolved here for the same * reason as `toolCallIds`/`thoughtSignature`: the request mapper is handed a mode, never a * provider identity to sniff one from. Absent (a hand-built target) reads as `"none"` — the * pre-2026-09-10 bytes exactly. */ reasoning?: ReasoningMode; /** * The routed POOL's effort band, when the request was resolved through a single dynamic pool * whose policy declares an `effort`. Stamped at resolution time so the request mapper can map * "pool effort → `reasoning_effort`" for a `"deepseek"` target without reaching back into * routing. Absent for a direct spec or a static pool — then the mapper has no effort to map and * a deepseek target defaults its thinking OFF. */ effort?: EffortLevel; } export interface Config { host: string; port: number; providers: Record; routing: Routing; mode: Mode; /** Explicit global reshaper override; otherwise repair reshapes on the resolved target itself. */ reshaper?: ReshaperConfig; /** * Ranked reshaper candidates, from `reshaper: { pool: "" }`. Tried in order, so a single * de-listed model cannot take repair down with it — the whole point of not pinning one model. * `reshaper` is candidates[0] so every existing single-reshaper path keeps working unchanged. */ reshaperCandidates?: ReshaperConfig[]; /** Dynamic reshaper pool resolved lazily after its catalog-backed tail is materialized. */ reshaperPool?: { name: string; timeoutMs?: number; }; repair: { maxAttempts: number; destructiveTools: string[]; }; /** * Wall-clock ceiling (ms) on STARTING further failover attempts within one request's pool * walk. The first two attempts are always allowed and an attempt already in flight is never * aborted — the budget bounds the walk, not the answer. 0 disables. Absent ⇒ the server's * DEFAULT_WALK_BUDGET_MS (45s). */ walkBudgetMs?: number; /** Maximum inbound request-body size in bytes. Absent ⇒ 36 MiB. */ maxBodyBytes?: number; log: { level: "metadata" | "silent"; file: string | null; maxBytes?: number; }; /** * Providers the onboarding nudge must stop asking about (`leave_me_alone` in config.json). * * Scope is deliberately narrow: it silences the "❌ Missing Key / 👉 get one here" prompt in * `llm-relay onboard`, and nothing else. Suppressed providers still appear in `llm-relay keys`, * in `/registry` and in every status surface — silencing a nudge is not hiding state, and a * provider that vanished from the status commands would be undebuggable later. * * Names that match no known provider are legal (see `parseLeaveMeAlone`). */ leaveMeAlone?: string[]; /** Path this config was loaded from. Set by `loadConfig`; absent for hand-built test configs. * Only consumer is the runtime offload toggle, which persists back to the same file. */ sourcePath?: string; /** * The config file's mtime (ms since epoch) at the moment `loadConfig` read it — paired with * `sourcePath` for the config-staleness notice (`configStaleness()` below). Set by `loadConfig` * as a NON-ENUMERABLE property (see there) so it never appears in a `JSON.stringify` of the * whole config, a `toEqual` comparison of a loaded `Config`, or `Object.keys(cfg)`; absent for * a hand-built test config with no backing file. The relay does not hot-reload — this field * exists only to let the relay and the CLI SAY so, never to trigger a reload. */ sourceMtimeMs?: number; /** * Non-fatal load-time problems (a provider disabled for an unset `${ENV}`, a pool member * dropped with it). Present so startup can print them — a degraded config that boots * silently is how you end up running on one provider without noticing. */ warnings?: string[]; } /** Background lane re-probing settings — see the `laneProbe` field doc on `Routing`. */ export interface LaneProbeSettings { enabled: boolean; /** Gate between quota probes of ONE dead bucket. A probe spends that lane's quota. */ quotaIntervalMs: number; /** Gate between catalog re-probes of ONE lane. Metadata commands, no quota spent. */ catalogIntervalMs: number; } /** Automatic dispatch lane-walk settings — see the `dispatchWalk` field doc on `Routing`. */ export interface DispatchWalkSettings { enabled: boolean; /** * How long a lane may show NO activity before the walk stops it and starts the next one. Default * 300000 (5 minutes). Activity is a request the relay daemon serves with the lane's tag, the lane's * own output, owned process-tree CPU, or a change in its git working tree (`mcp/server.ts` `latestActivity`). * * ⚠ Owner decision 2026-09-17: a lane is stopped only when it is IDLE, never because it ran longer * than its past runs. The five minutes covers a lane that runs a long command (a test suite) and * sends no model traffic meanwhile. The last lane in a walk is never stopped. */ idleMs: number; /** * Legacy pre-v0.84 attempt-budget floor. Retained and validated so existing configs keep loading, * but it no longer affects lane stopping; an explicit setting produces a load warning. */ attemptMs: number; /** Legacy agent-mode budget floor; retained for compatibility, no longer used to stop a lane. */ agentAttemptMs: number; /** Legacy attempt-budget quantile; retained for compatibility, no longer used to stop a lane. */ attemptQuantile: number; /** * Sample floor before a more-specific lane-history window is trusted over a legacy fallback, * and the minimum samples each side of recent-vs-history outlier demotion needs. Default 5. * Too little history means no opinion, never "slow". */ attemptMinSamples: number; /** * How many lanes one dispatch may try. Bounded so a ladder of a dozen dead rungs cannot consume * a dozen attempts before reporting; the walk states how many it tried and how many it skipped, * because a silent cap reads as "everything was tried" when it was not. */ maxLanes: number; /** How long a lane that answered is preferred. See `lane-affinity.ts` for the promote-only rule. */ pinMs: number; /** How long a lane the walk abandoned is ordered behind undemoted lanes. */ demoteMs: number; /** * Recent-versus-earlier outlier demotion (backlog item 9): demote a lane whose recent runs * are an outlier against its OWN earlier history, on a threshold calibrated from that * history (`scripts/calibrate-lane-outlier.mjs`, defaults in `lane-affinity.ts`). * Default ON; `false` makes the rule inert while rows still carry their timestamps. */ outlier: false | DispatchWalkOutlierSettings; } /** * Automatic dispatch outlier-demotion settings — see the `outlier` field doc on * `DispatchWalkSettings`. */ export interface DispatchWalkOutlierSettings { /** * How many of the window's most recent samples form the "recent" half of the comparison. * Default 5. Both halves need at least `attemptMinSamples` samples or the rule is silent. */ recentCount: number; /** * Which point of the EARLIER window the recent median is judged against. Default 0.8; * strictly inside (0, 1). */ historyQuantile: number; /** * How far above the earlier quantile the recent median must sit to demote. Default 2.5 * (calibrated — see `DEFAULT_OUTLIER_FACTOR` in `lane-affinity.ts`). Greater than 1. */ outlierFactor: number; } /** `llm-relay mcp` settings — see the `mcp` field doc on `Routing`. */ export interface McpSettings { /** * Directories a caller-supplied `cwd` must sit under. Absent or empty ⇒ no bound beyond the * directory existing. * * ⚠ Offered, never imposed. The MCP caller is already a trusted agent on the operator's own * machine, and defaulting to a bound would make the tool useless for its stated purpose. Whether * to narrow it is the operator's decision to record here, not this file's to assume. */ allowedRoots?: string[]; /** * Ceiling (ms) on how long one `dispatch` tool call blocks before handing back a pollable job * id. A `waitMs` above it is clamped to it (and the clamp announced); a non-positive, * non-finite or non-numeric `waitMs` is refused. Absent ⇒ `DEFAULT_MCP_MAX_WAIT_MS`. */ maxWaitMs?: number; /** * Ceiling (ms) on the blocking wait for a host that tolerates a long tool call and asked for * progress (`BLOCKING_WAIT_CLIENTS` in `mcp/server.ts`). Such a host gets the answer in ONE call, * the way its own subagents answer. `0` turns the blocking wait off, so every host gets * `maxWaitMs`. Absent ⇒ `DEFAULT_MCP_BLOCKING_WAIT_MS`. */ blockingWaitMs?: number; } /** * Default `routing.mcp.blockingWaitMs` — 25 minutes. * * Measured and documented 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code's * wall-clock tool limit (`MCP_TOOL_TIMEOUT`) defaults to about 28 hours, and a 240 s call succeeded * headless with and without progress. Its stdio idle timeout is 30 minutes, and the documentation * says a progress notification resets it. The default stays under 30 minutes so the call survives * even if that reset does not happen. A lane still running at the cap degrades to polling. */ export declare const DEFAULT_MCP_BLOCKING_WAIT_MS = 1500000; /** * Default `routing.mcp.maxWaitMs` — the longest one `dispatch` tool call blocks before handing * back a job id to poll. * * It must end before the SHORTEST host limit, because above a host's limit the host fails the call * AND loses the job handle. Two limits are measured on this machine: Claude Code fails an MCP call * somewhere between 45 s and 100 s, and Codex's code-mode `exec` tool yields its script at 31.0 s * with empty output ("Script running with cell ID N / Wall time 31.0 seconds"). The 2026-09-10 * transcript sweep counted 29 of 266 first Codex dispatch calls that lost their job id that way * while this default was 40 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §8). 25 s sits under * both. The tool description names the config key rather than this figure, so an operator override * never leaves the text stale. */ export declare const DEFAULT_MCP_MAX_WAIT_MS = 25000;