Files
OmniRoute/src/domain/fallbackPolicy.ts

161 lines
3.9 KiB
TypeScript

/**
* 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
}
}