import type { Config } from "./config.js"; export declare const DEFAULT_LOG_MAX_BYTES: number; export declare const MAX_LOG_ATTEMPTS = 64; export type RequestAttemptStatus = number | "failed" | "cancelled" | "committed" | "dead-turn" /** Skipped BEFORE egress by an operator-set hard cap (G2) — no provider saw this attempt. */ | "capped"; /** Status-only metadata for one deployment visited during a bounded candidate walk. */ export interface RequestAttemptLog { provider: string; model: string | null; status: RequestAttemptStatus; ms: number; } /** * Metadata-only request log. NEVER records headers or bodies — only the shape of * what happened. This is the dataset that reveals which backend models trip the * validator (format-broken, reshapeable) vs pass cleanly. */ export interface RequestLog { ts: string; path: string; /** * The provider that actually served the request (`ResolvedTarget.provider`), or * `null` when the request never reached a backend (guardrail rejection, routing * error, an admin endpoint) and so nothing served it. `null` means none — never * a guess. * * REQUIRED. It was optional for the length of the OBS-b5ade458 transition, where * an absent field meant "this call site has not been migrated yet"; every call * site has been migrated, so `tsc` now names any new one that forgets to say * which of the two it is. * * ⚠ There is deliberately no field for the model the CLIENT asked for. There was * one (`backendModel`), and it was the only model id in the log: routing resolves * a tier/pool spec to a `ResolvedTarget`, so the requested id and the serving * deployment routinely differ, and every "which model trips the validator" * conclusion drawn from this dataset was attributed to whatever the client * happened to name. Don't reintroduce it beside these two — a reader who has both * will read the wrong one. */ servedProvider: string | null; /** * The backend model id that actually served the request * (`ResolvedTarget.model`), or `null` when nothing served it. An anthropic * passthrough target carries no model id of its own, and that is `null` too. */ servedModel: string | null; /** Opaque configured credential slot that served, when known. Never a secret value. */ servedCredential: string | null; /** Raw upstream model id, present only when it differs from the resolved target. */ upstreamReportedModel?: string; /** Bounded, statuses-only candidate walk. Never carries error text or bodies. */ attempts: RequestAttemptLog[]; hadTools: boolean; streamed: boolean; backendStatus: number; validated: "pass" | "fail" | "uncheckable" | "skipped"; toolUseCount: number; uncheckableCount: number; errorKinds: string[]; /** * Why a pre-commit stream died, in the backend's own terms, when the backend stated it * (`src/stream-commit.ts` `stopCauseToken`): `backend_stopped_at_max_tokens`, * `backend_stopped_at_tool_use`, `backend_stopped_at_end_turn`, or `stop_reason_unknown`. * * ⚠ Absent unless a stream actually died before commit. A request that answered normally leaves * no trace here, and neither does a failure that is not a dead stream. * * ⚠ An ENUM-LIKE CLASSIFICATION and never the backend's words. It is derived in `stream-commit.ts` * from a closed stop-reason vocabulary, so nothing a provider wrote can reach this field — which * is what makes it admissible under the metadata-only rule. It exists because the measured * failure (2026-09-10: a `deepseek` stream that spent its whole `max_tokens` on reasoning and sent * no text) surfaced as a bare 502 with `errorKinds: []` and was diagnosable only by re-sending the * request outside the relay. */ streamStopCause?: string; /** Repair outcome (repair mode only); "none" when repair did not run. "cancelled" * means the caller went away mid-repair — distinct from "failed" (nothing was * reachable), because the two call for opposite responses. */ repair: "none" | "fixed" | "failed" | "refused" | "refused_destructive" | "cancelled"; /** * How many `tool_use` ids the relay had to mint because the serving host reused ones the * conversation already carried (`src/tool-use-ids.ts`). Absent when none were — a host that * mints unique ids leaves no trace here. * * ⚠ A COUNT, never an id. It is here because a streamed response cannot carry the * `x-llm-relay-tool-use-ids` header (headers are written before the first tool call exists), * so for stream traffic — which is all agentic traffic — this is the only place the pass shows. */ toolUseIdRewrites?: number; /** * How many OUTBOUND tool-call ids the request mapper rewrote to the serving provider's stated * shape (`src/openai-request.ts`, `compat.toolCallIds: "strict9"` — mistral's * `^[a-zA-Z0-9]{9}$`). Absent when none were, which is every provider that states no such rule. * * ⚠ A COUNT, never an id — the same rule as `toolUseIdRewrites`. */ toolCallIdRewrites?: number; /** * How many replayed tool calls the request mapper stamped with gemini's documented * thought-signature sentinel (`src/openai-request.ts`, `compat.thoughtSignature: "sentinel"`). * Absent when none were, which is every provider but Google's Generative Language API. * * ⚠ A COUNT, never a signature. This one has NO response header at all — the sentinel is * vendor-protocol padding on the relay's own outbound shape, not a change to the caller's data, * so the log is the one place the pass shows. */ thoughtSignatureSentinels?: number; latencyMs: number; } export declare class MetadataLogger { private readonly cfg; constructor(cfg: Config["log"]); write(record: RequestLog): void; }