import type { Api, Context, Model, SimpleStreamOptions, ThinkingLevel, Usage } from "@earendil-works/pi-ai"; import { type CanonicalMeta, type CanonicalScores, type CostTier, type ModelProfile } from "./canonical-models.ts"; import { type QuotaConfig } from "./quota.ts"; /** * Keep capability targets, effective cost, and reasoning effort independent: task difficulty sets * the quality floor, economics choose among qualifying variants, and benchmark effort configures the * selected provider call. */ export type RouteClass = CapabilityMode | "model"; export type Confidence = "high" | "medium" | "low"; /** Which benchmark drives every model's capability + cost. The two are never merged; selection is wholesale. */ export type CapabilitySource = "aa" | "ramp"; export type CapabilityMode = "low" | "medium" | "high" | "ultra"; /** Choose provider reasoning: auto uses its measured model-effort variant; forced models honor Pi. */ export declare function routingReasoning(benchmarkEffort: ThinkingLevel | undefined, requestedReasoning: ThinkingLevel | undefined, forcedModel: boolean): ThinkingLevel | "off"; export interface CanonicalResolution { canonical: CanonicalMeta | null; costTier: CostTier; capabilityMode?: CapabilityMode; profiles: ModelProfile[]; frontier: boolean; intelligence: number; priceBlended: number; scores?: CanonicalScores; tps?: number; /** Pi-normalized effort used by the benchmark row backing this resolution, if known. */ benchmarkEffort?: ThinkingLevel; /** Whether the active capability source has data for this model. Unsupported models are not auto-routed. */ supported: boolean; confidence: Confidence; reason: string; } export interface ResolvedModel { model: Model; acceptsImage: boolean; canonicalKey: string | null; costTier: CostTier; capabilityMode?: CapabilityMode; profiles: ModelProfile[]; frontier: boolean; /** Synthetic intelligence index; capability axis for `balanced`/fallback profiles. */ intelligence: number; /** List price $/1M tokens (blended 3:1). The Pareto cost axis; NOT marginal/subscription cost. */ priceBlended: number; scores?: CanonicalScores; tps?: number; /** Pi-normalized effort used by the benchmark row backing this routing variant, if known. */ benchmarkEffort?: ThinkingLevel; /** Whether the active capability source covers this model (or a user override does). Drives auto-pool inclusion. */ supported: boolean; confidence: Confidence; matchReason: string; /** Time-of-day shadow-price windows, carried through so the price can be re-evaluated per turn * (see `repriceForTimeOfDay`) without rebuilding the pool. `priceBlended` here is time-neutral. */ costCoefHours?: CostCoefWindow[]; } export interface Pool { cheapPool: ResolvedModel[]; strongPool: ResolvedModel[]; standardPool: ResolvedModel[]; unknownPool: ResolvedModel[]; all: ResolvedModel[]; } export interface ModelOverride { canonical?: string; costTier?: CostTier; capabilityMode?: CapabilityMode; profiles?: ModelProfile[]; frontier?: boolean; intelligence?: number; priceBlended?: number; scores?: CanonicalScores; tps?: number; /** Override the benchmark-backed effort metadata for private or manually classified models. */ benchmarkEffort?: ThinkingLevel; /** * Shadow-price coefficient: multiplies the model's base cost-axis price (Ramp cost-per-task under * `ramp`). It folds *my* economics into the shared capability frontier without touching the quality * axis, and stays dimensionless so it can never put the axis into a foreign unit. <1 = cheaper to me * than Ramp measured (an already-paid subscription, a discounted PAYG deal); >1 = pricier. Default 1 * = pure Ramp. Keep a shared/finite subscription a *positive* shadow price, not ~0: a near-zero coef * makes an already-strong model dominate the whole frontier and starves the cheap PAYG floor. */ costCoef?: number; /** Time-of-day multipliers stacked on `costCoef` (e.g. GLM burns 3× quota 14:00–18:00). */ costCoefHours?: CostCoefWindow[]; } export interface CostCoefWindow { /** [start, end) in local 24h hours; wraps when start > end (e.g. [22, 2] = 22:00–02:00). */ hours: [number, number]; factor: number; } export interface ModelFilter { include: string[]; exclude: string[]; } export interface RouterConfig { /** Which benchmark drives capability + cost. `ramp` (default) = real SWE-bench outcomes; `aa` = synthetic. Never merged. */ capabilitySource: CapabilitySource; threshold: number; weights: { contextTokens: number; lastUserLen: number; toolDensity: number; }; log: boolean; /** Pin an exact provider/model for a user-facing capability mode. */ modeModels: Partial>; /** Restrict the automatically built pool by provider/id/name/canonical substring. Empty include means allow all. */ modelFilter: ModelFilter; /** User-supplied metadata for unknown/private/local models. Keys may be provider/id, model id, or normalized model id. */ modelOverrides: Record; /** * Willingness to pay for capability, by selected mode: the max extra list-price ($/1M) spent for * one more point of quality on the chosen axis. Selection walks the Pareto frontier from the * cheapest point upward, taking each step whose marginal $/quality-point is within budget — so the * mode signal (driven by task content) positions us on the frontier and steep low-value * steps (a near-tie flagship at 2× price) are only taken at `ultra`. The single routing knob, axis- * agnostic. Raise a row to climb further for that mode; `ultra: Infinity` = "top of frontier". */ willingness: Record; /** * Cross-turn cache stickiness. Once a model has a warm prompt cache (a "lease"), switching to a * freshly-picked model pays a cache-write tax; we only switch when the economics win — cheaper warm * reads on a downgrade, or enough capability gain on an upgrade. Layered on top of the Pareto pick. */ cacheAware: { enabled: boolean; /** Extra USD the downgrade's read savings must beat the switch tax by, before switching down. */ downgradeMarginUsd: number; /** Minimum capability gain (axis points: resolve-rate / intelligence) to switch up. */ upgradeQualityMargin: number; /** USD of switch tax that counts as one required extra quality point when upgrading. */ upgradeTaxPenaltyScaleUsd: number; /** Minimum user turns between model switches. */ minTurnsBetweenSwitches: number; }; quota: QuotaConfig; classifier: ClassifierConfig; /** Explicit provider/model or variant ref for the optional LLM classifier. Empty means choose the cheapest eligible pool model. */ classifierModel?: string; } export interface ClassifierConfig { enabled: boolean; failureThreshold: number; cooldownTurns: number; timeoutMs: number; } export interface Decision { cls: RouteClass; score: number; chosen: string; /** Capability mode index into CAPABILITY_MODE_ORDER; sets the quality floor. */ modeBucket: number; requestedProfile?: ModelProfile; reason?: string; } export interface ClassificationResult { mode: CapabilityMode; profile?: ModelProfile; score?: number; reason?: string; } export interface TaskClassifier { classify(context: Context, cfg: RouterConfig): ClassificationResult; } export interface ClassifierState { models: Record; } export interface Selection { selected: ResolvedModel; profile: ModelProfile; /** Pi-normalized effort selected with the model when the benchmark source provides one. */ benchmarkEffort?: ThinkingLevel; reason: string; alternatives: string[]; } /** A warm prompt-cache hold on a model: switching away from it pays a fresh cache-write tax. */ export interface CacheLease { modelKey: string; provider: string; /** Raw registry cost fields for the leased model (per-token or per-1M; normalized at use). */ cost: { input: number; cacheRead: number; cacheWrite: number; }; warmTokens: number; establishedAtTurn: number; lastUsedTurn: number; } /** Per-session routing memory for cache-aware stickiness. */ export interface RoutingState { lease?: CacheLease; lastSwitchTurn: number; observedCacheReadRatio: number; realizedCostByModel: Record; lastUsage?: Usage; } export type CacheReason = "disabled" | "no-lease" | "same-model" | "switch-cooldown" | "downgrade-break-even" | "downgrade-not-worth-it" | "upgrade-quality" | "upgrade-not-worth-it"; export interface CacheAwareResult { selection: Selection; cacheReason: CacheReason; taxUsd?: number; expectedSavingsUsd?: number; } /** * Default willingness per source. The cost axis differs by source — AA is list price ($/1M tokens, * ~0.5–20), Ramp is measured cost per task ($, ~0.09–2.7) — so the $/quality-point budgets are on * different scales and must not be shared. `loadConfig` picks the table matching `capabilitySource` * unless the user sets `willingness` explicitly. */ export declare const AA_WILLINGNESS: Record; export declare const RAMP_WILLINGNESS: Record; export declare const DEFAULT_CONFIG: RouterConfig; export interface RouteModelRequest { models: Model[]; hint: string; context?: Context; nowHour?: number; cfg?: RouterConfig; filterQuota?: boolean; agentDir?: string; } export interface RouteModelSelection { key: string; } export declare function mergeClassifierConfig(raw: unknown, base?: ClassifierConfig): ClassifierConfig; export declare function enableClassifierForPinnedModel(classifier: ClassifierConfig, classifierModel: string | undefined, rawClassifier: unknown): ClassifierConfig; export declare function inferFallbackProfile(context: Context): ModelProfile; /** Loads only the user-level model-router.json; project configuration requires trust context and is intentionally excluded. */ export declare function loadUserRouterConfig(agentDir?: string): RouterConfig; /** Resolves a routing hint without requiring ExtensionContext or invoking a model provider. */ export declare function resolveRouteModel(request: RouteModelRequest): RouteModelSelection | undefined; export declare function normalizeModelKey(key: string): string; /** Map a Ramp solve rate (0–100) to the user-facing capability mode. */ export declare function rampCapabilityMode(resolveRate: number): CapabilityMode; /** Map AA Intelligence Index onto the same user-facing capability modes. */ export declare function aaCapabilityMode(intelligence: number): CapabilityMode; export declare function parseCapabilityMode(value: string): CapabilityMode | undefined; export declare function resolveCanonicalModel(key: string, source?: CapabilitySource): CanonicalResolution; export declare function resolveCanonicalModels(key: string, source?: CapabilitySource): CanonicalResolution[]; export declare function modelKey(model: Model): string; export declare function buildAutoPool(models: Model[], cfg?: RouterConfig): Pool; export declare function selectClassifierModel(pool: Pool, cfg: RouterConfig, state: ClassifierState, turn: number): ResolvedModel | undefined; export declare function resolveModel(model: Model, cfg?: RouterConfig): ResolvedModel; export declare function resolveModelVariants(model: Model, cfg?: RouterConfig): ResolvedModel[]; /** Product of the time-of-day window factors active at `nowHour` (1 when none apply). */ export declare function timeCostMultiplier(windows: CostCoefWindow[] | undefined, nowHour: number): number; /** * Re-apply each model's time-of-day shadow-price windows against `nowHour`, returning a pool with * updated prices. Called once per user turn at the selection boundary (where the clock is read), so a * window like GLM's 14:00–18:00 3× starts and stops biting as time passes — no `/reload` needed. The * caller reads the clock once per turn and reuses the pick within the turn, so prices stay stable * across a turn's tool continuations. */ export declare function repriceForTimeOfDay(pool: Pool, nowHour: number): Pool; export declare function findModelOverride(cfg: RouterConfig, key: string, canonicalKey: string | null): ModelOverride | undefined; export declare function matchesModelFilter(item: ResolvedModel, filter: ModelFilter): boolean; export declare function decide(context: Context, options: SimpleStreamOptions | undefined, forced: { mode: CapabilityMode; } | { model: string; } | undefined, cfg: RouterConfig, classifier?: TaskClassifier): Decision; /** * Continuous difficulty bucket for auto mode. The bucket drives the capability floor, so the whole * frontier (incl. mid-tier models) becomes reachable. Driven purely by task content: the thinking * level is a passthrough that controls how deeply the *chosen* model reasons, never which model is chosen. */ export declare function autoModeBucket(score: number): number; export declare function classify(context: Context, cfg: RouterConfig): number; export declare const HEURISTIC_CLASSIFIER: TaskClassifier; export declare function parseClassificationOutput(text: string): ClassificationResult | undefined; export declare function createClassifierState(): ClassifierState; export declare function isClassifierModelDisabled(state: ClassifierState, key: string, turn: number): boolean; export declare function recordClassifierSuccess(state: ClassifierState, key: string): void; export declare function recordClassifierFailure(state: ClassifierState, key: string, turn: number, cfg: RouterConfig): void; export declare function inferRequestedProfile(context: Context): ModelProfile; /** Approximate one axis from another when a model lacks the native metric (keeps the scale comparable). */ export declare function axisValue(item: ResolvedModel, profile: ModelProfile): number; /** * Capability Pareto frontier on (quality, list price): keep a model only if no other is at least as * capable AND no more expensive (strictly better on one). Dominated models — dumber *and* pricier — * are strict waste and never selected. This replaces the old hard-coded scoreCandidate. */ export declare function paretoFrontier(items: ResolvedModel[], profile: ModelProfile): ResolvedModel[]; /** * The frontier as a monotone chain, cheapest+weakest → priciest+strongest, with equal-(quality,price) * duplicates collapsed deterministically. This is the ordered set of operating points to climb. */ export declare function frontierChain(items: ResolvedModel[], profile: ModelProfile): ResolvedModel[]; export declare function selectFromPool(decision: Decision, pool: Pool, context: Context, options: SimpleStreamOptions | undefined, cfg: RouterConfig): Selection | undefined; export declare function createRoutingState(): RoutingState; /** Monotonic user-turn counter (number of user messages) — no harness turn hooks needed. */ export declare function userTurnIndex(context: Context): number; /** * Given the fresh Pareto selection, decide whether to keep the warm lease instead. Returns the fresh * pick when there is no lease, when the fresh pick already is the lease, or when an economic switch wins. */ export declare function cacheAwareSelect(fresh: Selection, state: RoutingState, pool: Pool, context: Context, cfg: RouterConfig): CacheAwareResult; /** Record the realized usage of a turn: refresh cache-read ratio and re-establish the lease. */ export declare function recordRoutingUsage(state: RoutingState, selected: ResolvedModel, usage: Usage, context: Context): void; export declare function contextHasImage(context: Context): boolean; export declare function lastUserText(context: Context): string; export declare function routingTurnKey(context: Context): string; export declare function shouldReuseTurnSelection(context: Context): boolean; export declare function estimateContextTokens(context: Context): number; export declare function variantKey(item: ResolvedModel): string;