/** * Shift — the stage router over pi-ai (E002 Inc 02, Switchyard pattern). * * Routes each stage to its cast model, provides a PROVIDER fallback chain on * 429/5xx with capped retries, and guards a token+cost budget. Routing * decisions are emitted as {@link RoutingEvent}s through a redacting activity * sink — the same seam session events use. * * The router reuses the Inc 01 cast config ({@link Cast}): no second * model-config surface. The cast's `architectModel` → plan stage, * `builderModel` → build stage, `judgeModel` (or `architectModel`) → review * stage. The fallback chain for each stage is derived from the cast's model * list PLUS other configured casts (cross-provider fallback) — no new * configuration surface, just the existing casts. */ import type { Cast } from "../fusion/casts.js"; import type { Stage } from "./stages.js"; import { type ShiftInput, type StageConfig } from "./stages.js"; import { type Tier, type TierDecisionSource, type TierInput } from "./tier.js"; import { type ActivitySink } from "./activity.js"; /** A fallback target: a model + its provider (cross-provider fallback). */ export interface FallbackTarget { /** Provider id (from the cast). */ provider: string; /** Model id (as it appears in the cast). */ model: string; } /** The model selected for a stage, with its source provider. */ export interface RouteResult { stage: Stage; /** Model id (as it appears in the cast). */ model: string; /** Provider id (from the cast — may differ on fallback). */ provider: string; /** Zero-based position in the fallback chain (0 = primary). */ attempt: number; } /** Budget configuration for the budget guard. */ export interface BudgetConfig { /** Maximum total tokens across all routed calls. Default: Infinity. */ maxTokens?: number; /** Maximum total cost. Default: Infinity. */ maxCost?: number; } /** Router configuration. */ export interface ShiftRouterOptions { /** The cast to route among (from Inc 01 config — no second model surface). */ cast: Cast; /** * Additional casts for cross-provider fallback. When the primary cast's * provider is rate-limited (429) or down (5xx), the router falls back to * models from these casts. Derived from the same `~/.openkai/config.json` * casts list — no second config surface. */ fallbackCasts?: Cast[]; /** Stage classification config (keyword sets, priority, defaults). */ stageConfig?: StageConfig; /** Maximum fallback attempts per stage (default: 3). */ maxRetries?: number; /** Budget guard configuration. */ budget?: BudgetConfig; /** * Activity sink for routing events. Events are redacted before reaching * this sink (see {@link createRedactingSink}). When omitted, no events are * emitted (useful for headless tests that inspect return values only). */ onActivity?: ActivitySink; } /** * Build the ordered fallback chain for a stage from the cast's models PLUS * cross-cast fallbacks. * * The chain starts with the stage's primary model (from the primary cast), * then lists every OTHER distinct model in the primary cast (same provider, * different model — handles model-specific 429s), then adds models from * fallback casts — prioritising DIFFERENT providers first (handles * provider-wide 429s/outages). Duplicates (same provider+model) are removed. * * This fixes the rework finding "fallback is model-only, not provider": a * self-paired cast (one model) now gets cross-provider fallback targets from * other configured casts, so a provider-wide 429 doesn't exhaust the chain * on the first retry. */ export declare function fallbackChain(stage: Stage, cast: Cast, fallbackCasts?: Cast[]): FallbackTarget[]; /** Error thrown when all fallbacks for a stage are exhausted. */ export declare class FallbackExhaustedError extends Error { readonly name = "FallbackExhaustedError"; readonly stage: Stage; readonly chain: FallbackTarget[]; constructor(stage: Stage, chain: FallbackTarget[]); } /** Error thrown when the budget guard refuses a call. */ export declare class BudgetExceededError extends Error { readonly name = "BudgetExceededError"; constructor(used: number, max: number, unit: string); } /** * The Shift router — stateful (tracks the current invocation's attempt * counter, the per-task total fallback count, and cumulative budget). * One instance per task; discard after the task completes. */ export declare class ShiftRouter { private readonly cast; private readonly fallbackCasts; private readonly stageConfig; private readonly maxRetries; private readonly budget; private readonly sink; /** * Fallback attempts within the CURRENT invocation (one route() → next()* * sequence). Reset by {@link route} so a fresh call never inherits an * earlier call's attempts — the previous per-stage accumulation meant the * second invocation of a stage started partway down its own chain. */ private invocationAttempt; /** * Total fallback attempts across the whole task — the per-task bound that * `maxRetries` caps. Separate from the per-invocation counter on purpose: * one invocation's retries must not exhaust another's, but a task must not * retry forever across many invocations either. */ private totalRetries; private tokensUsed; private costUsed; constructor(options: ShiftRouterOptions); /** Classify a prompt into a stage (delegates to the pure classifier). */ classify(input: ShiftInput): Stage; /** * Check the budget guard. Throws {@link BudgetExceededError} when either * the token or cost budget is exceeded. Called on BOTH the primary route * and every fallback — the rework found the guard was inert on the primary * route because it was only checked in {@link next}. */ private checkBudget; /** * Route a stage to its primary model. Emits a `routing` event. * Does NOT consume budget — call {@link trackUsage} after a successful call. * * The budget guard IS checked here (on the primary route) — the rework * found it was only checked on the fallback path, leaving every job's first * call unguarded. */ route(stage: Stage): RouteResult; /** * Report an error for the current stage and get the next fallback target. * * Fallback-eligible errors: 429 (rate limit), 5xx (server error), and * STATUSLESS transient network failures — ECONNRESET, ETIMEDOUT, * ENOTFOUND, ECONNREFUSED, "fetch failed". A genuine 4xx (auth, bad * request) is terminal: retrying it on another model changes nothing. * * Returns the next {@link RouteResult} in the fallback * chain, or throws {@link FallbackExhaustedError} when the chain or retry * cap is exhausted. Throws {@link BudgetExceededError} when the budget * guard refuses. */ next(stage: Stage, error: { status?: number; message: string; }): RouteResult; /** * Complete a call with automatic provider fallback. Wraps a caller-supplied * completion function: routes to the primary target, calls `fn`, and on a * retryable error (429/5xx) falls back to the next target in the chain. * * The `errorExtractor` callback lets the caller map a thrown error to a * `{status, message}` shape — provider SDKs surface HTTP status in * different ways, so the router does not assume a specific error class. * * On success, usage is tracked via {@link trackUsage} when the result * carries a `usage` field. */ completeWithFallback(stage: Stage, fn: (target: FallbackTarget) => Promise, errorExtractor: (error: unknown) => { status?: number; message: string; } | null): Promise<{ result: T; attempts: FallbackTarget[]; }>; /** * Track token + cost usage from a successful call (updates the budget * guard). Call this after each model call completes. */ trackUsage(usage: { totalTokens?: number; cost?: { total?: number; }; }): void; /** Current budget state (for inspection / testing). */ get budgetState(): { tokensUsed: number; costUsed: number; maxTokens: number; maxCost: number; }; /** Emit a routing event through the redacting sink (no-op if no sink). */ private emit; } /** * Route a prompt end-to-end: classify the stage, then return the primary * model for that stage. Convenience wrapper for one-shot routing. */ export declare function shiftRoute(input: ShiftInput, options: ShiftRouterOptions): { stage: Stage; result: RouteResult; }; /** * OK-9.1 — tier-aware routing: classify the stage, then let the Switchyard * tier scorer move the turn between an (efficient, capable) model pair. * The decision carries its `source` (override | tests_passed | dimensions | * fall_open) onto the activity feed — Switchyard's observability discipline. * Pure over the provided signals; no model call on the hot path. * * The fall-open default tier is per stage (K3): plan/review fall open to the * CAPABLE member (their cast roles are the strong models — an ambiguous * signal must not silently cheapen an architecture turn); build falls open * to efficient. Callers override via `defaultTier` (the OK-9.7 posture dial * plugs in there). NOTE: this is the pure decision seam — it does not run * ShiftRouter's budget guard or fallback chain; production wiring goes * through the orchestration facade (OK-9 W2/W3), not this preview helper. */ export interface TierRouteResult { stage: Stage; tier: Tier; model: string; provider: string; source: TierDecisionSource; score: number; reason: string; } export interface TierRouteOptions { stageConfig?: StageConfig; onActivity?: ActivitySink; /** Override the fall-open tier (default: per stage — plan/review capable, build efficient). */ defaultTier?: Tier; } export declare function routeWithTier(input: ShiftInput, signals: TierInput, options: TierRouteOptions, tiers: { efficient: FallbackTarget; capable: FallbackTarget; }): TierRouteResult; //# sourceMappingURL=router.d.ts.map