import type { ProviderId } from "./canonical-model.js"; import type { CredentialSource } from "./plan.js"; /** * Circuit-breaker state is keyed by (provider, invocation model, credential * source) โ€” LLM Provider Routing PRD ยง7.2. State is process-local in v1; * deterministic routing and provider telemetry make replica differences * observable. */ export interface BreakerKey { readonly providerId: ProviderId; readonly invocationModel: string; readonly credentialSource: CredentialSource; /** * Optional non-secret credential identity (e.g. an opaque per-tenant tag) * that further scopes the endpoint state. Without it, one tenant's revoked * BYOK key would open the circuit for every tenant whose calls share * `credentialSource: "tenant"` on this process. Never put key material here * โ€” the value lands in breaker state keys. */ readonly credentialScope?: string; } export interface CircuitBreakerOptions { /** Consecutive fallbackable failures before the circuit opens. Default 3. */ readonly failureThreshold?: number; /** Default cooldown once open, in ms. Default 60_000. */ readonly cooldownMs?: number; /** Upper bound on any cooldown, including Retry-After extensions. Default 60_000. */ readonly maxCooldownMs?: number; /** Injectable clock for tests. Defaults to Date.now. */ readonly now?: () => number; } export type EndpointAdmission = Readonly<{ admitted: true; halfOpenProbe: boolean; }> | Readonly<{ admitted: false; retryAtMs: number; }>; export interface RecordFailureOptions { /** * Open immediately regardless of the consecutive count โ€” used for * credential/credit failures, which repeated requests cannot repair. */ readonly openImmediately?: boolean; /** Bounded Retry-After hint; extends the cooldown up to `maxCooldownMs`. */ readonly retryAfterMs?: number | null; } export interface RouteCircuitBreaker { /** * Ask to attempt an endpoint. Closed circuits admit; open circuits refuse * until cooldown elapses; after cooldown exactly one caller is admitted as * a half-open probe while others keep being refused until the probe * resolves via `recordSuccess`/`recordFailure`. */ admit(key: BreakerKey): EndpointAdmission; recordSuccess(key: BreakerKey): void; recordFailure(key: BreakerKey, options?: RecordFailureOptions): void; /** * Resolve a half-open probe that ended without a success or a * breaker-recordable failure (an abort, a propagated client error). The * probe slot is freed and the cooldown re-armed, so the next admit after * cooldown runs a fresh probe instead of refusing forever. No-op when no * probe is in flight. */ releaseProbe(key: BreakerKey): void; } export declare function createCircuitBreaker(options?: CircuitBreakerOptions): RouteCircuitBreaker;