import { type KeystoreOptions } from "./keystore.js"; import { type OffloadRule, type ReshaperConfig, type ToolCallIdMode, type ThoughtSignatureMode, type ReasoningMode, type ProviderCompatConfig, type RequestHeaders, type ResolvedTarget, type Config } from "./config-types.js"; export { AUTO_MODEL, POOL_PREFIX, splitSpec } from "./spec.js"; export { DEFAULT_LANE_PROBE, parseOffload, parseRouting } from "./config/routing-parser.js"; export { EFFORT_LEVELS, CLAUDE_TIER_NAMES, type Mode, type AuthHeader, type Kind, type ProviderTierType, type EffortLevel, type ClaudeTierName, type OffloadScope, type OffloadRule, type OffloadConfig, type ReshaperConfig, type CredentialMode, type ToolCallIdMode, type ThoughtSignatureMode, type ReasoningMode, type ProviderCompatConfig, type ProviderWireMode, type ProviderConfig, type Routing, type StickyRoutingConfig, type StickyConfig, type QuotaEnforcementConfig, type LatencyDemotionConfig, type HedgeConfig, type CliLaneTemplate, type PoolPolicy, type LadderRung, type RequestHeaders, type ResolvedTarget, type Config, type DispatchWalkSettings, type LaneProbeSettings, type McpSettings, } from "./config-types.js"; /** * The RESOLVED time-to-first-byte deadline for one provider: an explicit `firstByteTimeoutMs`, or * `stallTimeoutMs` as the default (same intent — "no bytes for this long means dead"), or absent * when neither is configured. An explicit value always wins, even `undefined` cannot occur here * since `parseProviderFirstByteTimeout` never returns `0` or another falsy-but-present value. */ export declare function resolveFirstByteTimeoutMs(p: { stallTimeoutMs?: number; firstByteTimeoutMs?: number; }): number | undefined; /** * The resolved outbound tool-call-id shape for one provider. * * A LABELLED PROVIDER FACT, allowed by the "Provider knowledge is data, not routing configuration" * invariant precisely because config overrides it: mistral's own validator states the rule * (`^[a-zA-Z0-9]{9}$`, first-party evidence in `src/openai-request.ts`), so a mistral base host * defaults to `"strict9"` and every other host to `"preserve"`. An explicit `compat.toolCallIds` * wins in BOTH directions — `"preserve"` on a mistral host, `"strict9"` on anything else. */ export declare function resolveToolCallIdMode(p: { base: string; compat?: ProviderCompatConfig; }): ToolCallIdMode; /** * The resolved thought-signature mode for one provider. * * The SAME labelled-fact mechanism as `resolveToolCallIdMode`, and allowed by the SAME invariant — * "Provider knowledge is data, not routing configuration" permits a labelled provider fact in * `src/` only while config can override it. Google's Generative Language API states the rule (its * gemini 3.x models 400 a replayed tool call carrying no signature; first-party evidence in * `src/openai-request.ts`), so that base host defaults to `"sentinel"` and every other host to * `"none"`. An explicit `compat.thoughtSignature` wins in BOTH directions. */ export declare function resolveThoughtSignatureMode(p: { base: string; compat?: ProviderCompatConfig; }): ThoughtSignatureMode; /** * The resolved reasoning-mapping mode for one provider. * * The SAME labelled-fact mechanism as `resolveToolCallIdMode` / `resolveThoughtSignatureMode`, and * allowed by the SAME invariant — "Provider knowledge is data, not routing configuration" permits a * labelled provider fact in `src/` only while config can override it. DeepSeek's own API states its * thinking/reasoning vocabulary (thinking ON by default; `thinking: {type:"disabled"}` / * `reasoning_effort: low|high|max`; first-party evidence in * docs/history/deepseek-responses-truncation-2026-09-09.md), so `api.deepseek.com` defaults to `"deepseek"` * and every other host to `"none"`. An explicit `compat.reasoning` wins in BOTH directions — * `"none"` on deepseek, `"deepseek"` on anything else. */ export declare function resolveReasoningMode(p: { base: string; compat?: ProviderCompatConfig; }): ReasoningMode; /** True when this request is a marked Claude Code or local Codex child turn. */ export declare function isSubagentRequest(reqJson: unknown, headers?: RequestHeaders): boolean; /** * Read (and optionally strip) an `@relay: ` directive from the dispatcher's prompt. * * ⚠ Only the LAST text block of `messages[0]` is inspected — that is the dispatcher-authored * prompt. Block 0 is Claude Code's injected `` (CLAUDE.md, date, …) and later * messages carry tool results, i.e. file contents. Reading those would let any file the subagent * happens to read redirect its own routing. */ export declare function readRelayDirective(reqJson: unknown, strip?: boolean): string | null; /** * The client names used by the built-in front doors. Other clients may use their own key in the * object form of `routing.offload`, or fall back to the explicit `default` rule. */ export declare const CLAUDE_CLIENT = "claude"; export declare const CODEX_CLIENT = "codex"; export declare const OPENAI_CLIENT = "openai"; export declare const DEFAULT_CLIENT = "default"; /** Map a relay front-door path to the originating harness name used by offload settings. */ export declare function clientForPath(pathname: string): string; /** Every client name `clientForPath` can produce — the complete set of rule keys a request consults. */ export declare const FRONT_DOOR_CLIENTS: readonly string[]; /** * Why a targeted offload rule for `client` could never affect a request, or null when it can. * * The request path looks up ONLY the name `clientForPath()` derived from the front-door path. A * rule keyed anything else ("claude-desktop" was the real case) is dead config: the toggle * succeeds, status shows it ON, and every request falls through to the `default` rule — so the * operator's `--scope all` silently did nothing. Same principle as an unknown pool: refuse loudly * and name what IS valid. A key that already exists in the config stays legal (`fatal: false`) so * state remains visible and an operator can still turn a dead rule off — callers surface the * message as a prominent warning instead. */ export declare function unroutableOffloadClient(client: string, cfg: Pick): { fatal: boolean; message: string; } | null; /** Return the effective rule for one originating client. Legacy booleans apply everywhere. */ export declare function offloadRule(cfg: Pick, client?: string): OffloadRule; /** Whether any named client rule is active; used by aggregate status surfaces. */ export declare function anyOffloadEnabled(cfg: Pick): boolean; /** * The spec a request should route to, or null to leave routing unchanged. * * Precedence: an explicit `@relay:` directive (per-call opt-in, works even with the client rule off) > * `routing.subagents[]` > `routing.subagents.default` — the last two only when * the effective client rule is enabled. A `scope: "subagents"` rule only applies to marked child * requests; `scope: "all"` also applies to the client's main conversation. * * ⚠ An unresolvable directive is a loud `RoutingError`, never a quiet fall-through. The map forms * are validated at config load (`assertSpecResolvable`), but a directive arrives per request and * had no check at all: a typo'd provider or pool name matched nothing in `pickSpecs`, so it landed * on `routing.default` — the Anthropic passthrough. That spends PRIMARY quota while the dispatcher * believes it offloaded, and nothing in the response says otherwise. Same rule as an unknown pool: * fail and name what IS configured. */ export declare function subagentSpec(reqJson: unknown, model: string | null, cfg: Config, headers?: RequestHeaders, client?: string): string | null; /** * Tools a repaired call is never allowed to name. THE single definition — the * shipped config template, config.example.json and the docs all derive from or * are asserted equal to this list, because they previously disagreed: this array * had 8 entries including "remove" while the other three had 7, so a config that * omitted repair.destructiveTools got different coverage than a generated one. * * Matching is exact (see destructiveMatcher), so these are real tool names, not * fragments. The clients' own destructive tools are listed first — they are the * ones that can actually destroy something, and the previous fragment list * ("rm", "delete", …) matched none of them. */ export declare const DEFAULT_DESTRUCTIVE: string[]; declare const DEFAULT_ANTHROPIC_VERSION = "2023-06-01"; export { DEFAULT_ANTHROPIC_VERSION }; /** A routing failure — surfaced to the client as a clean 400, never a crash. */ export declare class RoutingError extends Error { } /** * Expand any `pool/` spec into that pool's candidate list. Applied to whatever * pickSpecs chose, so pools work uniformly whether addressed directly by the request, * from routing.tiers/default, or via routing.subagents. Pool members themselves are * provider specs only (config load rejects pool-in-pool), so no recursion. * * An unknown pool must NOT silently fall through to routing.default — that is exactly the * "succeeded against a much weaker model than you asked for" failure. Fail loudly instead. */ export declare function expandPoolSpecs(specs: string[], cfg: Config): string[]; /** * Resolve an inbound `model` to an array of concrete targets (primary + fallbacks). */ export declare function resolveTargets(model: string | null, cfg: Config, keystoreOptions?: KeystoreOptions): ResolvedTarget[]; /** * Resolve an inbound `model` to the primary concrete target. */ export declare function resolveTarget(model: string | null, cfg: Config, keystoreOptions?: KeystoreOptions): ResolvedTarget; /** The reshaper for a resolved target when no explicit global reshaper is set. */ export declare function reshaperForTarget(target: ResolvedTarget): ReshaperConfig | undefined; /** CLI overrides, applied over the file so routing can be repointed without editing it. */ export interface ConfigOverrides { listen?: string | undefined; /** Override routing.default with a "provider/model" spec. */ routeDefault?: string | undefined; mode?: string | undefined; } /** Load + validate a config file, failing loudly on anything unusable. */ export declare function loadConfig(path: string, overrides?: ConfigOverrides, keystoreOptions?: KeystoreOptions): Config; export interface ConfigStalenessReport { /** The config file this process loaded from, or null for a hand-built Config with no file. */ path: string | null; /** This config's recorded mtime (ms since epoch) at load time, or null if never recorded. */ loadedAt: number | null; /** True when the file on disk no longer matches what this process loaded, including when the * file has been deleted. False for a Config with no recorded source (never file-backed) — * there is nothing on disk to have diverged from. */ changedOnDisk: boolean; /** The file's CURRENT mtime (ms since epoch), read live at call time; null when it cannot be * stat'd (missing, permission denied, or no source recorded at all). */ diskMtime: number | null; } /** * The ONE text of the config-staleness notice — printed by the daemon (once, to its log/stderr, * the first time `GET /telemetry` observes the change) and by every config-reading CLI command * that talks to a running relay (`routing show`, `offload status`, `pools`, `routing`, `config`), * so every surface says exactly the same thing rather than each hand-copying its own wording. */ export declare const CONFIG_STALENESS_NOTICE = "config changed on disk since the relay loaded it \u2014 run \"llm-relay reload\"; a restart is required if the changed fields are not reloadable"; /** * Does the config file on disk still match what this process loaded? Pure over its inputs aside * from the injectable `stat` — the default reads the real filesystem, a caller may inject one for * tests. NEVER throws: a missing or otherwise unreadable file reads as `changedOnDisk: true` with * `diskMtime: null`, because "the relay's copy no longer matches whatever is on disk" is the true * state whether the file was edited or removed out from under it. * * The relay does not hot-reload (see `Config.sourceMtimeMs`) — this is the one helper that turns * "the file changed" into a fact `GET /telemetry` and the CLI can both report, so an operator who * edits `config.json` is told to restart rather than being left to wonder why nothing took effect. */ export declare function configStaleness(cfg: Pick, stat?: (path: string) => { mtimeMs: number; }): ConfigStalenessReport;