diff --git a/open-sse/services/autoCombo/routerStrategy.ts b/open-sse/services/autoCombo/routerStrategy.ts index 1278d80b1b..ec60b72939 100644 --- a/open-sse/services/autoCombo/routerStrategy.ts +++ b/open-sse/services/autoCombo/routerStrategy.ts @@ -16,6 +16,7 @@ export interface RoutingContext { requestHasTools?: boolean; requestHasVision?: boolean; estimatedInputTokens?: number; + lastKnownGoodProvider?: string; } export interface RoutingDecision { @@ -116,6 +117,34 @@ class LatencyStrategyImpl implements RouterStrategy { } } +// ── LKGPStrategy: tries last known good provider first ─────────────────────── + +class LKGPStrategyImpl implements RouterStrategy { + readonly name = "lkgp"; + readonly description = "Tries last known good provider first, then falls back to rules"; + + select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision { + if (context.lastKnownGoodProvider) { + const best = pool.find( + (c) => c.provider === context.lastKnownGoodProvider && c.circuitBreakerState !== "OPEN" + ); + if (best) { + return { + provider: best.provider, + model: best.model, + strategy: this.name, + reason: `LKGP: using last known good provider ${best.provider}`, + candidatesConsidered: 1, + finalScore: 1.0, + }; + } + } + + // Fallback to rules strategy + return getStrategy("rules").select(pool, context); + } +} + // ── Registry ────────────────────────────────────────────────────────────────── const strategyRegistry = new Map(); @@ -123,12 +152,14 @@ const strategyRegistry = new Map(); const rulesStrategy = new RulesStrategyImpl(); const costStrategy = new CostStrategyImpl(); const latencyStrategy = new LatencyStrategyImpl(); +const lkgpStrategy = new LKGPStrategyImpl(); strategyRegistry.set("rules", rulesStrategy); strategyRegistry.set("cost", costStrategy); strategyRegistry.set("eco", costStrategy); // alias strategyRegistry.set("latency", latencyStrategy); strategyRegistry.set("fast", latencyStrategy); // alias +strategyRegistry.set("lkgp", lkgpStrategy); export function getStrategy(name: string): RouterStrategy { const strategy = strategyRegistry.get(name); diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 582418ddea..13e9ffc0b1 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -99,7 +99,10 @@ async function validateResponseQuality( if (json?.output || json?.result || json?.data || json?.response) return { valid: true }; if (json?.error) { const err = json.error as Record; - return { valid: false, reason: `upstream error in 200 body: ${err?.message || JSON.stringify(json.error).substring(0, 200)}` }; + return { + valid: false, + reason: `upstream error in 200 body: ${err?.message || JSON.stringify(json.error).substring(0, 200)}`, + }; } return { valid: true }; } @@ -809,6 +812,16 @@ export async function handleComboChat({ const modePack = typeof autoConfigSource.modePack === "string" ? autoConfigSource.modePack : undefined; + // Retrieve last known good provider (LKGP) for this combo/model (#919) + let lastKnownGoodProvider: string | undefined; + try { + const { getLKGP } = await import("../../src/lib/localDb"); + const lkgp = await getLKGP(combo.name, combo.id || combo.name); + if (lkgp) lastKnownGoodProvider = lkgp; + } catch { + /* ignore db errors */ + } + const candidates = await buildAutoCandidates(eligibleModels, combo.name); if (candidates.length > 0) { let selectedProvider = null; @@ -819,7 +832,7 @@ export async function handleComboChat({ try { const decision = selectWithStrategy( candidates, - { taskType, requestHasTools }, + { taskType, requestHasTools, lastKnownGoodProvider }, routingStrategy ); selectedProvider = decision.provider; @@ -980,6 +993,14 @@ export async function handleComboChat({ fallbackCount, strategy, }); + + // Record last known good provider (LKGP) for this combo/model (#919) + if (provider) { + import("../../src/lib/localDb") + .then(({ setLKGP }) => setLKGP(combo.name, combo.id || combo.name, provider)) + .catch(() => {}); + } + return result; } diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index 77b65ef4f0..f723748fbd 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -248,6 +248,31 @@ export async function resetAllPricing() { return {}; } +// ──────────────── LKGP (Last Known Good Provider) ──────────────── + +export async function getLKGP(comboName: string, modelId: string): Promise { + const db = getDbInstance(); + const key = `${comboName}:${modelId}`; + const row = db + .prepare("SELECT value FROM key_value WHERE namespace = 'lkgp' AND key = ?") + .get(key) as { value?: string } | undefined; + if (!row?.value) return null; + try { + return JSON.parse(row.value); + } catch { + return row.value; + } +} + +export async function setLKGP(comboName: string, modelId: string, providerId: string) { + const db = getDbInstance(); + const key = `${comboName}:${modelId}`; + db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('lkgp', ?, ?)").run( + key, + JSON.stringify(providerId) + ); +} + // ──────────────── Proxy Config ──────────────── const DEFAULT_PROXY_CONFIG: ProxyConfig = { global: null, providers: {}, combos: {}, keys: {} }; diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index b9aa15401c..521416daa5 100644 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -98,6 +98,10 @@ export { updateSettings, isCloudEnabled, + // LKGP (Last Known Good Provider) (#919) + getLKGP, + setLKGP, + // Pricing getPricing, getPricingForModel,