| /** | |
| * Fallback Policy — Domain Layer (T-19) | |
| * | |
| * Declarative fallback chain for model routing. | |
| * When a primary provider is unavailable, the policy engine | |
| * resolves to alternative providers in priority order. | |
| * | |
| * State is persisted in SQLite via domainState.js. | |
| * | |
| * @module domain/fallbackPolicy | |
| */ | |
| import { | |
| saveFallbackChain, | |
| loadFallbackChain, | |
| loadAllFallbackChains, | |
| deleteFallbackChain, | |
| deleteAllFallbackChains, | |
| } from "../lib/db/domainState"; | |
| /** | |
| * @typedef {Object} FallbackEntry | |
| * @property {string} provider - Provider ID | |
| * @property {number} [priority=0] - Lower = higher priority | |
| * @property {boolean} [enabled=true] - Whether this fallback is active | |
| */ | |
| /** @type {Map<string, FallbackEntry[]>} In-memory cache backed by SQLite */ | |
| const fallbackChains = new Map(); | |
| /** @type {boolean} Whether we've loaded from DB yet */ | |
| let _loaded = false; | |
| /** | |
| * Ensure in-memory cache is hydrated from SQLite. | |
| */ | |
| function ensureLoaded() { | |
| if (_loaded) return; | |
| try { | |
| const all = loadAllFallbackChains(); | |
| for (const [model, chain] of Object.entries(all)) { | |
| fallbackChains.set(model, chain); | |
| } | |
| } catch { | |
| // DB may not be ready yet (build phase), that's ok | |
| } | |
| _loaded = true; | |
| } | |
| /** | |
| * Register a fallback chain for a model. | |
| * | |
| * @param {string} model - Model identifier (e.g. "gpt-4o") | |
| * @param {FallbackEntry[]} chain - Ordered list of fallback providers | |
| */ | |
| export function registerFallback(model, chain) { | |
| ensureLoaded(); | |
| const sorted = [...chain] | |
| .map((e) => ({ | |
| provider: e.provider, | |
| priority: e.priority ?? 0, | |
| enabled: e.enabled ?? true, | |
| })) | |
| .sort((a, b) => a.priority - b.priority); | |
| fallbackChains.set(model, sorted); | |
| try { | |
| saveFallbackChain(model, sorted); | |
| } catch { | |
| // Non-critical: in-memory still works | |
| } | |
| } | |
| /** | |
| * Resolve the fallback chain for a model. | |
| * Returns only enabled providers, sorted by priority. | |
| * | |
| * @param {string} model | |
| * @param {string[]} [excludeProviders=[]] - Providers to skip (e.g. already tried) | |
| * @returns {FallbackEntry[]} Ordered list of fallback providers | |
| */ | |
| export function resolveFallbackChain(model, excludeProviders = []) { | |
| ensureLoaded(); | |
| const chain = fallbackChains.get(model); | |
| if (!chain) return []; | |
| const excludeSet = new Set(excludeProviders); | |
| return chain.filter((e) => e.enabled && !excludeSet.has(e.provider)); | |
| } | |
| /** | |
| * Get the next provider in the fallback chain. | |
| * | |
| * @param {string} model | |
| * @param {string[]} [excludeProviders=[]] | |
| * @returns {string | null} Next provider ID or null if chain exhausted | |
| */ | |
| export function getNextFallback(model, excludeProviders = []) { | |
| const chain = resolveFallbackChain(model, excludeProviders); | |
| return chain.length > 0 ? chain[0].provider : null; | |
| } | |
| /** | |
| * Check if a model has any fallback providers configured. | |
| * | |
| * @param {string} model | |
| * @returns {boolean} | |
| */ | |
| export function hasFallback(model) { | |
| ensureLoaded(); | |
| const chain = fallbackChains.get(model); | |
| return !!chain && chain.some((e) => e.enabled); | |
| } | |
| /** | |
| * Remove a fallback chain for a model. | |
| * | |
| * @param {string} model | |
| * @returns {boolean} true if removed | |
| */ | |
| export function removeFallback(model) { | |
| ensureLoaded(); | |
| const removed = fallbackChains.delete(model); | |
| if (removed) { | |
| try { | |
| deleteFallbackChain(model); | |
| } catch { | |
| // Non-critical | |
| } | |
| } | |
| return removed; | |
| } | |
| /** | |
| * Get all registered fallback chains (for dashboard). | |
| * | |
| * @returns {Record<string, FallbackEntry[]>} | |
| */ | |
| export function getAllFallbackChains() { | |
| ensureLoaded(); | |
| /** @type {Record<string, FallbackEntry[]>} */ | |
| const result = {}; | |
| for (const [model, chain] of fallbackChains.entries()) { | |
| result[model] = chain; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Reset all fallback chains (for testing). | |
| */ | |
| export function resetAllFallbacks() { | |
| fallbackChains.clear(); | |
| _loaded = false; | |
| try { | |
| deleteAllFallbackChains(); | |
| } catch { | |
| // Non-critical | |
| } | |
| } | |