import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { Api, Model } from "@earendil-works/pi-ai"; import { getCircuitState, type ReliabilityConfig, type ReliabilityState } from "./reliability.ts"; import type { QuotaRoutingConfig, QuotaSnapshot, ProviderQuota } from "./quota.ts"; export type RoutingStrategy = | "first" | "cheapest" | "cheapest_input" | "cheapest_output" | "largest_context" | "random" | "fastest" | "subscription_balance" | "subscription_preferred"; export interface RouteRule { pattern: string; model: string; } export function modelKey(model: Model | undefined): string { if (!model) return "none"; return `${model.provider}/${model.id}`; } export function supportsImageInput(model: Model): boolean { return model.input.includes("image"); } /** Sum of input + output token costs per 1M tokens. Does not include cache read/write costs. */ export function modelCost(model: Model): number { return model.cost.input + model.cost.output; } /** Input token cost only. */ export function modelInputCost(model: Model): number { return model.cost.input; } /** Output token cost only. */ export function modelOutputCost(model: Model): number { return model.cost.output; } /** Context window size. */ export function modelContextSize(model: Model): number { return model.contextWindow; } export function findOneModel( ctx: ExtensionContext, pattern: string, ): Model | undefined { if (!pattern) return undefined; if (pattern.includes("/")) { const [provider, ...idParts] = pattern.split("/"); const id = idParts.join("/"); return ctx.modelRegistry.find(provider, id); } const lower = pattern.toLowerCase(); const available = ctx.modelRegistry.getAvailable(); return available.find( (m) => m.id.toLowerCase().includes(lower) || m.provider.toLowerCase().includes(lower), ); } export function findCandidates( ctx: ExtensionContext, pattern: string | string[] | undefined, ): Model[] { if (!pattern) return []; const candidates: Model[] = []; const seen = new Set(); const patterns = Array.isArray(pattern) ? pattern : [pattern]; // Resolve once — avoid N getAvailable() calls for N substring patterns. const available = ctx.modelRegistry.getAvailable(); for (const p of patterns) { if (p.includes("/")) { const model = findOneModel(ctx, p); if (model) { const key = modelKey(model); if (!seen.has(key)) { seen.add(key); candidates.push(model); } } } else { const lower = p.toLowerCase(); for (const m of available) { if ( !seen.has(modelKey(m)) && (m.id.toLowerCase().includes(lower) || m.provider.toLowerCase().includes(lower)) ) { seen.add(modelKey(m)); candidates.push(m); } } } } return candidates; } export function diagnoseCandidates( ctx: ExtensionContext, pattern: string | string[] | undefined, ): { candidates: Model[]; unresolved: string[] } { if (!pattern) return { candidates: [], unresolved: [] }; const candidates: Model[] = []; const unresolved: string[] = []; const seen = new Set(); const patterns = Array.isArray(pattern) ? pattern : [pattern]; const available = ctx.modelRegistry.getAvailable(); for (const p of patterns) { let found = false; if (p.includes("/")) { const model = findOneModel(ctx, p); if (model) { found = true; const key = modelKey(model); if (!seen.has(key)) { seen.add(key); candidates.push(model); } } } else { const lower = p.toLowerCase(); for (const m of available) { if ( (m.id.toLowerCase().includes(lower) || m.provider.toLowerCase().includes(lower)) ) { found = true; if (!seen.has(modelKey(m))) { seen.add(modelKey(m)); candidates.push(m); } } } } if (!found) unresolved.push(p); } return { candidates, unresolved }; } /** * Resolves candidate models matching pattern, respecting Pi's scoped-model selection * (`ctx.scopedModels`) when configured. When `scopedModels` is empty or undefined, * all available models in the registry are in scope (as per Pi extension spec). */ export function scopedCandidates( ctx: ExtensionContext, pattern: string | string[] | undefined, ): Model[] { const candidates = findCandidates(ctx, pattern); if (!ctx.scopedModels || ctx.scopedModels.length === 0) { return candidates; } const scoped = new Set(ctx.scopedModels.map(({ model }) => modelKey(model))); return candidates.filter((model) => scoped.has(modelKey(model))); } /** * Shared logic for subscription_balance and subscription_preferred. * * Partition candidates by billing class. Subscription models with * sufficient session AND weekly quota remaining are tried first using * weighted balancing. Paid-credit (OpenRouter) models are only * reached when every subscription candidate is genuinely drained or * absent. Free and unknown models are always eligible as final * fallback. */ function selectSubscriptionFirst( candidates: Model[], quota: QuotaSnapshot | undefined, quotaConfig: QuotaRoutingConfig | undefined, now: number, ): Model | undefined { const subscriptionModels = candidates.filter((m) => billingClass(m) === "subscription"); const freeModels = candidates.filter((m) => billingClass(m) === "free"); const paidCreditModels = candidates.filter((m) => billingClass(m) === "paid-credit"); const otherModels = candidates.filter((m) => billingClass(m) === "unknown"); // Try subscription models first when any have usable quota. if (subscriptionModels.length > 0) { const viable = subscriptionModels.filter((m) => hasUsableQuota(m, quota, quotaConfig, now), ); if (viable.length > 0) { const preference = weeklyQuotaPreference(viable, quota, quotaConfig, now); // ignoreWeeklyQuota means providers are within tolerance — pick via // weighted random among *subscription-only* candidates, not the // full list (the old bug: candidates[0] could be paid-credit). return selectWeighted( preference.candidates, subscriptionWeights(preference.candidates, quota, quotaConfig, now), ); } } // All subscriptions drained or absent — fall through. if (freeModels.length > 0) return freeModels[0]; if (otherModels.length > 0) return otherModels[0]; // Last resort: paid-credit. if (paidCreditModels.length > 0) return paidCreditModels[0]; return undefined; } /** * Returns true when a subscription model has enough weekly quota to * confidently handle a request. When quota data is stale or missing * the model is assumed usable (conservative: prefer attempting a * subscription over prematurely falling to credits). * * Session quota is intentionally NOT checked here — it is already * handled upstream by filterSessionExhausted + guardSubscriptionStrategy. * Re-checking it here would defeat the guard's re-injection of * near-drained subscription models that should still be preferred * over paid-credit. */ function hasUsableQuota( model: Model, quota: QuotaSnapshot | undefined, cfg: QuotaRoutingConfig | undefined, now: number, ): boolean { if (!quota) return true; const fresh = now - quota.fetchedAt < (cfg?.staleMinutes ?? 15) * 60_000; if (!fresh) return true; // stale → assume usable, don't punish const pq = getModelQuota(model, quota); if (!pq) return true; // no data → assume usable const weeklyReserve = cfg?.reservePercent ?? 0.03; // Weekly: if measured and below reserve, not usable if (typeof pq.weeklyRemainingFraction === "number" && pq.weeklyRemainingFraction <= weeklyReserve) { return false; } return true; } export function selectModel( candidates: Model[], strategy: RoutingStrategy, quota?: QuotaSnapshot, quotaConfig?: QuotaRoutingConfig, now = Date.now(), ): Model | undefined { if (candidates.length === 0) return undefined; switch (strategy) { case "cheapest": return [...candidates].sort((a, b) => modelCost(a) - modelCost(b))[0]; case "cheapest_input": return [...candidates].sort((a, b) => modelInputCost(a) - modelInputCost(b))[0]; case "cheapest_output": return [...candidates].sort((a, b) => modelOutputCost(a) - modelOutputCost(b))[0]; case "largest_context": return [...candidates].sort((a, b) => modelContextSize(b) - modelContextSize(a))[0]; case "random": return candidates[Math.floor(Math.random() * candidates.length)]; case "subscription_balance": case "subscription_preferred": return selectSubscriptionFirst(candidates, quota, quotaConfig, now); default: // "first", "fastest" — list order is assumed meaningful. return candidates[0]; } } // ── Subscription-aware weighting ──────────────────────────────────── export type BillingClass = "subscription" | "free" | "paid-credit" | "unknown"; /** * Classify a model's billing. Cost-free models are always usable; * subscription providers (Codex/Antigravity) carry the weekly quota; * OpenRouter-compatible hubs are credit-based. Unknown providers are * treated neutrally (never blocked, never biased). */ export function billingClass(model: Model): BillingClass { if (modelCost(model) <= 0) return "free"; const p = model.provider.toLowerCase(); if (p === "openai-codex" || p === "codex" || p === "antigravity" || p === "anthropic") { return "subscription"; } if (/openrouter|opencode/.test(p)) return "paid-credit"; return "unknown"; } /** * Look up quota for a specific model, resolving sub-provider buckets * (e.g. `antigravity:claude` vs `antigravity:gemini`) when present. */ export function getModelQuota( model: Model, quota: QuotaSnapshot | undefined, ): ProviderQuota | undefined { if (!quota) return undefined; if (model.provider === "antigravity") { if (/claude|gpt/i.test(model.id)) { return quota.byProvider["antigravity:claude"] ?? quota.byProvider["antigravity"]; } if (/gemini/i.test(model.id)) { return quota.byProvider["antigravity:gemini"] ?? quota.byProvider["antigravity"]; } } return quota.byProvider[model.provider]; } const WEEKLY_BALANCE_TOLERANCE = 0.10; function weeklyQuotaPreference( candidates: Model[], quota: QuotaSnapshot | undefined, cfg: QuotaRoutingConfig | undefined, now: number, ): { candidates: Model[]; ignoreWeeklyQuota: boolean } { const fresh = quota && now - quota.fetchedAt < (cfg?.staleMinutes ?? 15) * 60_000; if (!fresh) return { candidates, ignoreWeeklyQuota: false }; const byProvider = new Map(); for (const model of candidates) { if (billingClass(model) !== "subscription") continue; const remaining = getModelQuota(model, quota)?.weeklyRemainingFraction; if (typeof remaining === "number") byProvider.set(model.provider, remaining); } if (byProvider.size < 2) return { candidates, ignoreWeeklyQuota: false }; const ranked = [...byProvider].sort((a, b) => b[1] - a[1]); const [bestProvider, bestRemaining] = ranked[0]; const secondRemaining = ranked[1][1]; if (bestRemaining - secondRemaining <= WEEKLY_BALANCE_TOLERANCE + Number.EPSILON) { return { candidates, ignoreWeeklyQuota: true }; } if (bestRemaining <= (cfg?.reservePercent ?? 0.03)) { return { candidates, ignoreWeeklyQuota: false }; } return { candidates: candidates.filter((model) => model.provider === bestProvider), ignoreWeeklyQuota: false, }; } /** * Weights for `subscription_balance`. * * - free: weight 1 always (zero cost, nothing to conserve) * - subscription: weight = weeklyRemainingFraction^gamma — the provider * with more remaining allowance is favored harder as gamma grows; * near equilibrium the weights converge toward even. A subscription * with no fresh telemetry gets a conservative 0.5 so it can't * outrank a measured healthy subscription. * - paid-credit: weight 0 while any measured subscription still has * allowance above `reservePercent`; unblocked once subscriptions are * drained or when no subscription telemetry is available at all. * - unknown: weight 1 (no telemetry → no bias, but never blocked) * * A globally stale or empty snapshot degrades everything to weight 1 * (uniform) — no data → no bias, and paid credits are never starved. */ export function subscriptionWeights( candidates: Model[], quota: QuotaSnapshot | undefined, cfg: QuotaRoutingConfig | undefined, now = Date.now(), ): number[] { const reserve = cfg?.reservePercent ?? 0.03; const gamma = cfg?.gamma ?? 3; const fresh = quota !== undefined && now - quota.fetchedAt < (cfg?.staleMinutes ?? 15) * 60_000; // Only measured subscriptions can block credits; an unmeasured one can't // prove remaining allowance, so it must not keep the user off credits. const measuredSubs = fresh ? candidates.filter( (m) => billingClass(m) === "subscription" && typeof getModelQuota(m, quota)?.weeklyRemainingFraction === "number", ) : []; const anySubAboveReserve = measuredSubs.some((m) => { const w = getModelQuota(m, quota)?.weeklyRemainingFraction; return typeof w === "number" && w > reserve; }); // 0.5-for-unmeasured only applies when we DO have signal on another // subscription; a totally empty snapshot stays uniform (no data at all). const anyMeasuredSub = measuredSubs.length > 0; return candidates.map((m) => { switch (billingClass(m)) { case "free": return 1; case "paid-credit": return anySubAboveReserve ? 0 : 1; case "subscription": { if (!fresh || !anyMeasuredSub) return 1; const w = getModelQuota(m, quota)?.weeklyRemainingFraction; if (typeof w !== "number") return 0.5; // unmeasured: conservative, not dominant if (w <= reserve) return 0.05; // measured & drained — lose to credits return Math.pow(w, gamma); } default: return 1; } }); } /** Weighted random pick. Zero-weight items never win; all-zero degenerates to uniform. */ export function selectWeighted(items: readonly T[], weights: readonly number[]): T | undefined { if (items.length === 0) return undefined; const total = items.reduce((sum, _, i) => sum + Math.max(0, weights[i] ?? 0), 0); if (total <= 0) return items[Math.floor(Math.random() * items.length)]; let r = Math.random() * total; for (let i = 0; i < items.length; i++) { r -= Math.max(0, weights[i] ?? 0); if (r <= 0) return items[i]; } return items[items.length - 1]; } export function resolveModel( ctx: ExtensionContext, pattern: string | string[] | undefined, strategy: RoutingStrategy, ): Model | undefined { return selectModel(findCandidates(ctx, pattern), strategy); } export interface TierCandidateGroup { tier: string; candidates: Model[]; strategy: RoutingStrategy; } export function selectImageCapableModelFromGroups( groups: readonly TierCandidateGroup[], ): { tier: string; model: Model } | undefined { for (const group of groups) { const model = selectModel( group.candidates.filter(supportsImageInput), group.strategy, ); if (model) return { tier: group.tier, model }; } return undefined; } export interface SkippedCandidate { key: string; reason: "open_circuit" | "quota_exhausted" | "session_exhausted"; openUntil?: number; } export function isProviderQuotaExhausted( model: Model, quota: QuotaSnapshot | undefined, quotaConfig: QuotaRoutingConfig | undefined, now = Date.now(), ): boolean { if (!quota || now - quota.fetchedAt >= (quotaConfig?.staleMinutes ?? 15) * 60_000) return false; const remaining = getModelQuota(model, quota)?.weeklyRemainingFraction; return typeof remaining === "number" && remaining <= (quotaConfig?.reservePercent ?? 0.03); } /** Pick another scoped model in the same capability tier with measured quota available. */ export function selectComparableAvailableModel( current: Model, candidates: Model[], strategy: RoutingStrategy, quota: QuotaSnapshot | undefined, quotaConfig: QuotaRoutingConfig | undefined, now = Date.now(), heuristics?: TierHeuristicsConfig, ): Model | undefined { if (!isProviderQuotaExhausted(current, quota, quotaConfig, now) || !quota) return undefined; const reserve = quotaConfig?.reservePercent ?? 0.03; const tier = guessTier(current, heuristics); const available = candidates.filter((model) => model.provider !== current.provider && guessTier(model, heuristics) === tier && (quota.byProvider[model.provider]?.weeklyRemainingFraction ?? -1) > reserve ); return selectModel(available, strategy, quota, quotaConfig, now); } /** Remove models from providers whose weekly quota is exhausted. * Only applies when quota telemetry is fresh (within staleMinutes). * Never removes ALL candidates — that would deadlock the user. */ export function filterQuotaExhausted( candidates: Model[], quota: QuotaSnapshot | undefined, quotaConfig: QuotaRoutingConfig | undefined, now: number, ): { candidates: Model[]; skipped: SkippedCandidate[] } { if (!quota) return { candidates, skipped: [] }; const fresh = now - quota.fetchedAt < (quotaConfig?.staleMinutes ?? 15) * 60_000; if (!fresh) return { candidates, skipped: [] }; const reserve = quotaConfig?.reservePercent ?? 0.03; const skipped: SkippedCandidate[] = []; const filtered = candidates.filter((model) => { if (billingClass(model) !== "subscription") return true; const remaining = getModelQuota(model, quota)?.weeklyRemainingFraction; // No data: keep the model (conservative — unmeasured means unblocked) if (typeof remaining !== "number") return true; if (remaining <= reserve) { skipped.push({ key: modelKey(model), reason: "quota_exhausted" }); return false; } return true; }); // Don't starve the user — keep at least one candidate if (filtered.length === 0) return { candidates, skipped: [] }; return { candidates: filtered, skipped }; } /** Remove models from providers whose rolling session allowance is near-exhausted. * Only applies when session telemetry is fresh (within staleMinutes). The `quick` * tier is exempt from the near-exhausted reserve — short requests can still * squeeze through a nearly-drained session — but a fully-drained (0%) session * is a guaranteed 429 for every tier, so it is always blocked. * Never removes ALL candidates — that would deadlock the user. */ export function filterSessionExhausted( candidates: Model[], quota: QuotaSnapshot | undefined, quotaConfig: QuotaRoutingConfig | undefined, now: number, tier?: string, ): { candidates: Model[]; skipped: SkippedCandidate[] } { if (!quota) return { candidates, skipped: [] }; const fresh = now - quota.fetchedAt < (quotaConfig?.staleMinutes ?? 15) * 60_000; if (!fresh) return { candidates, skipped: [] }; const reserve = quotaConfig?.sessionReservePercent ?? 0.10; const skipped: SkippedCandidate[] = []; const filtered = candidates.filter((model) => { if (billingClass(model) !== "subscription") return true; const remaining = getModelQuota(model, quota)?.sessionRemainingFraction; // No session data: keep the model (unmeasured means unblocked) if (typeof remaining !== "number") return true; // 0% remaining is a hard block for every tier; no request squeezes through. if (remaining <= 0) { skipped.push({ key: modelKey(model), reason: "session_exhausted" }); return false; } // Near-exhausted sessions are tolerated only for `quick`. if (tier !== "quick" && remaining < reserve) { skipped.push({ key: modelKey(model), reason: "session_exhausted" }); return false; } return true; }); // Don't starve the user — keep at least one candidate if (filtered.length === 0) return { candidates, skipped: [] }; return { candidates: filtered, skipped }; } export interface HealthyModelResolution { selected: Model | undefined; candidates: Model[]; healthyCandidates: Model[]; skipped: SkippedCandidate[]; } export interface RoutedModelResolution { requestedTier: string; selectedTier?: string; selected: Model | undefined; strategy: RoutingStrategy; skipped: SkippedCandidate[]; fallbackReason?: "requested_tier_unhealthy" | "requested_tier_unavailable" | "all_tiers_exhausted"; primary: HealthyModelResolution; fallback?: HealthyModelResolution; } /** * When a subscription_* strategy is active, ensure at least one subscription * model survives filtering. The quota/session filters only remove subscription * models (paid-credit passes unconditionally), so they can strip every * subscription candidate while leaving OpenRouter — which defeats the strategy. * * If filtering removed all subscription models but the pre-filter set had some, * re-inject the subscription models so the strategy can rank them properly. * The strategy's own weighting (subscriptionWeights) already deprioritises * drained providers, so keeping them in the candidate list is safe — it just * prevents a hard cutover to paid-credit when the user explicitly asked for * subscription-first routing. */ function guardSubscriptionStrategy( filtered: Model[], preFilter: Model[], strategy: RoutingStrategy, ): Model[] { if (strategy !== "subscription_balance" && strategy !== "subscription_preferred") { return filtered; } const hasSub = filtered.some((m) => billingClass(m) === "subscription"); if (hasSub) return filtered; // All subscription models were removed by filters. Re-inject them so the // strategy can apply its own weighting instead of falling through to credits. const subs = preFilter.filter((m) => billingClass(m) === "subscription"); if (subs.length === 0) return filtered; return [...subs, ...filtered]; } export function resolveHealthyModel( ctx: ExtensionContext, pattern: string | string[] | undefined, strategy: RoutingStrategy, reliabilityState: ReliabilityState | undefined, reliabilityConfig: ReliabilityConfig | undefined, now = Date.now(), quota?: QuotaSnapshot, quotaConfig?: QuotaRoutingConfig, resolvedCandidates?: readonly Model[], tier?: string, ): HealthyModelResolution { const candidates = resolvedCandidates ? [...resolvedCandidates] : findCandidates(ctx, pattern); if (!reliabilityState || reliabilityConfig?.enabled === false) { const quotaFiltered = filterQuotaExhausted(candidates, quota, quotaConfig, now); const sessionFiltered = filterSessionExhausted(quotaFiltered.candidates, quota, quotaConfig, now, tier); const final = guardSubscriptionStrategy(sessionFiltered.candidates, candidates, strategy); return { selected: selectModel(final, strategy, quota, quotaConfig, now), candidates, healthyCandidates: candidates, skipped: [...quotaFiltered.skipped, ...sessionFiltered.skipped], }; } const healthyCandidates: Model[] = []; const skipped: SkippedCandidate[] = []; for (const candidate of candidates) { const circuit = getCircuitState(reliabilityState, modelKey(candidate), now, reliabilityConfig); if (circuit.open) { skipped.push({ key: modelKey(candidate), reason: "open_circuit", openUntil: circuit.openUntil }); continue; } healthyCandidates.push(candidate); } // Filter out models from providers with exhausted weekly quota. // Guard: if all candidates would be removed, keep the original set. const quotaFiltered = filterQuotaExhausted(healthyCandidates, quota, quotaConfig, now); const sessionFiltered = filterSessionExhausted(quotaFiltered.candidates, quota, quotaConfig, now, tier); const allSkipped = [...skipped, ...quotaFiltered.skipped, ...sessionFiltered.skipped]; const final = guardSubscriptionStrategy(sessionFiltered.candidates, healthyCandidates, strategy); return { selected: selectModel(final, strategy, quota, quotaConfig, now), candidates, healthyCandidates, skipped: allSkipped, }; } export function resolveModelWithFallback( ctx: ExtensionContext, options: { requestedTier: string; requestedPattern: string | string[] | undefined; requestedStrategy: RoutingStrategy; defaultTier?: string; defaultPattern?: string | string[] | undefined; defaultStrategy?: RoutingStrategy; reliabilityState?: ReliabilityState; reliabilityConfig?: ReliabilityConfig; quota?: QuotaSnapshot; quotaConfig?: QuotaRoutingConfig; requestedCandidates?: readonly Model[]; defaultCandidates?: readonly Model[]; now?: number; /** When true, never fall back to `defaultTier` if the requested tier has * no healthy candidate. Used for strict categories (e.g. `coding`) * where falling back would silently select an unapproved cross-category * model instead of surfacing the unavailable/unhealthy result. */ strict?: boolean; }, ): RoutedModelResolution { const now = options.now ?? Date.now(); const primary = resolveHealthyModel( ctx, options.requestedPattern, options.requestedStrategy, options.reliabilityState, options.reliabilityConfig, now, options.quota, options.quotaConfig, options.requestedCandidates, options.requestedTier, ); if (primary.selected) { return { requestedTier: options.requestedTier, selectedTier: options.requestedTier, selected: primary.selected, strategy: options.requestedStrategy, skipped: primary.skipped, primary, }; } const requestedUnavailable = primary.candidates.length === 0; let fallbackReason: RoutedModelResolution["fallbackReason"] = requestedUnavailable ? "requested_tier_unavailable" : (primary.skipped.length > 0 ? "requested_tier_unhealthy" : undefined); if (options.strict) { return { requestedTier: options.requestedTier, selected: undefined, strategy: options.requestedStrategy, skipped: primary.skipped, fallbackReason, primary, }; } // Compute final reason after evaluating fallback const resolveFinalReason = (fb: HealthyModelResolution): RoutedModelResolution["fallbackReason"] => { if (fb.selected) return fallbackReason; if (requestedUnavailable && fb.candidates.length === 0) return "requested_tier_unavailable"; if (fb.skipped.length > 0 || primary.skipped.length > 0) return "all_tiers_exhausted"; return fallbackReason; }; if (!options.defaultTier || options.defaultTier === options.requestedTier) { return { requestedTier: options.requestedTier, selected: undefined, strategy: options.requestedStrategy, skipped: primary.skipped, fallbackReason, primary, }; } const fallback = resolveHealthyModel( ctx, options.defaultPattern, options.defaultStrategy ?? options.requestedStrategy, options.reliabilityState, options.reliabilityConfig, now, options.quota, options.quotaConfig, options.defaultCandidates, options.defaultTier, ); return { requestedTier: options.requestedTier, selectedTier: fallback.selected ? options.defaultTier : undefined, selected: fallback.selected, strategy: fallback.selected ? (options.defaultStrategy ?? options.requestedStrategy) : options.requestedStrategy, skipped: [...primary.skipped, ...fallback.skipped], fallbackReason: resolveFinalReason(fallback), primary, fallback, }; } /** Refresh a stale host registry once when a configured tier resolved no candidates. */ export async function retryUnavailableResolution( resolution: RoutedModelResolution, refresh: () => Promise, resolve: () => RoutedModelResolution, ): Promise { if (resolution.fallbackReason !== "requested_tier_unavailable" || !(await refresh())) { return resolution; } return resolve(); } export function getStrategy( categoryStrategies: Record | undefined, fallbackStrategy: RoutingStrategy | undefined, category: string, ): RoutingStrategy { return categoryStrategies?.[category] ?? fallbackStrategy ?? "first"; } /** * Check whether an OpenRouter-resolved model should be redirected to a * subscription provider. Returns the subscription `provider/modelId` * string if a guard matches, or `undefined` if no redirect applies. */ export function applySubscriptionGuard( model: Model, guard: Record | undefined, ): string | undefined { if (!guard || model.provider !== "openrouter") return undefined; for (const [prefix, subscriptionProvider] of Object.entries(guard)) { if (model.id.startsWith(prefix)) { // Strip the matched prefix (e.g. "openai/") to get the bare model id, // then qualify it with the subscription provider. const bareId = model.id.slice(prefix.length); return `${subscriptionProvider}/${bareId}`; } } return undefined; } export function classify(text: string, rules: readonly RouteRule[]): string | undefined { for (const rule of rules) { try { const re = new RegExp(rule.pattern, "i"); if (re.test(text)) return rule.model; } catch (err) { console.error(`[bifrost] invalid regex "${rule.pattern}": ${err}`); } } return undefined; } // ── Tier heuristics ─────────────────────────────────────────── export interface TierHeuristicsConfig { quickParamCeiling?: number; frontierParamThreshold?: number; quickPattern?: string; frontierPattern?: string; frontierCostThreshold?: number; quickCostThreshold?: number; subscriptionFrontierContext?: number; subscriptionGeneralContext?: number; freeGeneralContext?: number; modelTiers?: Record; } const DEFAULT_FRONTIER_COST_THRESHOLD = 5; // $/1M tokens (input + output) const DEFAULT_QUICK_COST_THRESHOLD = 1; const DEFAULT_SUBSCRIPTION_FRONTIER_CONTEXT = 200_000; const DEFAULT_SUBSCRIPTION_GENERAL_CONTEXT = 64_000; const DEFAULT_FREE_GENERAL_CONTEXT = 200_000; const DEFAULT_QUICK_PARAM_CEILING = 14; const DEFAULT_FRONTIER_PARAM_THRESHOLD = 70; const DEFAULT_QUICK_PATTERN = "\\b(haiku|flash|mini|nano|lite|small|edge)\\b"; const DEFAULT_FRONTIER_PATTERN = "\\b(o[134]|opus|sonnet|gpt-[456]|gemini-.*-pro|deepseek-(v[34]|r1)|qwen-(max|plus))\\b"; export type BifrostTier = "frontier" | "general" | "writing" | "quick"; /** Extracts numerical parameter count (in billions) from a model ID, e.g. 70b -> 70, 2.4t -> 2400. */ export function extractParamCount(id: string): number | undefined { const match = id.toLowerCase().match(/(?:^|[^a-z0-9])(\d+(?:\.\d+)?)\s*([bt])(?:[^a-z0-9]|$)/); if (!match) return undefined; const val = parseFloat(match[1]); if (isNaN(val)) return undefined; const unit = match[2]; return unit === "t" ? val * 1000 : val; } /** Assign a model to a tier based on explicit overrides, parameter counts, architecture patterns, and cost/context heuristics. */ export function guessTier( model: Model, heuristics?: TierHeuristicsConfig, ): BifrostTier { const fullKey = modelKey(model); const id = model.id.toLowerCase(); // 1. Explicit model override if (heuristics?.modelTiers) { if (heuristics.modelTiers[fullKey]) return heuristics.modelTiers[fullKey]; if (heuristics.modelTiers[model.id]) return heuristics.modelTiers[model.id]; } // 2. Numerical parameter extraction (>= and <= logic) const paramCount = extractParamCount(model.id); const quickCeiling = heuristics?.quickParamCeiling ?? DEFAULT_QUICK_PARAM_CEILING; const frontierThreshold = heuristics?.frontierParamThreshold ?? DEFAULT_FRONTIER_PARAM_THRESHOLD; if (paramCount !== undefined) { if (paramCount <= quickCeiling) return "quick"; if (paramCount >= frontierThreshold) return "frontier"; } // 3. Name-based architecture / family patterns const quickRegexStr = heuristics?.quickPattern ?? DEFAULT_QUICK_PATTERN; if (quickRegexStr) { try { if (new RegExp(quickRegexStr, "i").test(id)) return "quick"; } catch (err) { console.error(`[bifrost] invalid quickPattern regex "${quickRegexStr}": ${err}`); } } const frontierRegexStr = heuristics?.frontierPattern ?? DEFAULT_FRONTIER_PATTERN; if (frontierRegexStr) { try { if (new RegExp(frontierRegexStr, "i").test(id)) return "frontier"; } catch (err) { console.error(`[bifrost] invalid frontierPattern regex "${frontierRegexStr}": ${err}`); } } // 4. Cost and Context heuristics const contextWindow = model.contextWindow ?? 0; const cost = model.cost ? modelCost(model) : 0; const cls: BillingClass = model.cost ? billingClass(model) : "free"; const subFrontierContext = heuristics?.subscriptionFrontierContext ?? DEFAULT_SUBSCRIPTION_FRONTIER_CONTEXT; const subGeneralContext = heuristics?.subscriptionGeneralContext ?? DEFAULT_SUBSCRIPTION_GENERAL_CONTEXT; const freeGenContext = heuristics?.freeGeneralContext ?? DEFAULT_FREE_GENERAL_CONTEXT; const frontierCost = heuristics?.frontierCostThreshold ?? DEFAULT_FRONTIER_COST_THRESHOLD; const quickCost = heuristics?.quickCostThreshold ?? DEFAULT_QUICK_COST_THRESHOLD; if (cls === "subscription") { if (contextWindow >= subFrontierContext) return "frontier"; if (contextWindow >= subGeneralContext) return "general"; return "quick"; } if (cls === "free") { if (contextWindow >= freeGenContext) return "general"; return "quick"; } if (cost > frontierCost) return "frontier"; if (cost < quickCost) return "quick"; return "general"; }