mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(routing): LGKP remembers last good account per provider (#2338)
Integrated into release/v3.8.0 — added unit tests for connectionId-aware getLKGP/setLKGP
This commit is contained in:
@@ -130,10 +130,11 @@ class LKGPStrategyImpl implements RouterStrategy {
|
||||
}
|
||||
|
||||
if (context.lastKnownGoodProvider) {
|
||||
const best = pool.find(
|
||||
const candidates = pool.filter(
|
||||
(c) => c.provider === context.lastKnownGoodProvider && c.circuitBreakerState !== "OPEN"
|
||||
);
|
||||
if (best) {
|
||||
if (candidates.length > 0) {
|
||||
const best = candidates[0];
|
||||
return {
|
||||
provider: best.provider,
|
||||
model: best.model,
|
||||
|
||||
@@ -1742,7 +1742,7 @@ export async function handleComboChat({
|
||||
try {
|
||||
const { getLKGP } = await import("../../src/lib/localDb");
|
||||
const lkgp = await getLKGP(combo.name, combo.id || combo.name);
|
||||
if (lkgp) lastKnownGoodProvider = lkgp;
|
||||
if (lkgp) lastKnownGoodProvider = lkgp.provider;
|
||||
} catch (err) {
|
||||
log.warn("COMBO", "Failed to retrieve Last Known Good Provider. This is non-fatal.", { err });
|
||||
}
|
||||
@@ -1819,22 +1819,34 @@ export async function handleComboChat({
|
||||
const lkgpProvider = await getLKGP(combo.name, combo.id || combo.name);
|
||||
|
||||
if (lkgpProvider) {
|
||||
const lkgpIndex = orderedTargets.findIndex(
|
||||
(target) =>
|
||||
target.provider === lkgpProvider || target.modelStr.startsWith(`${lkgpProvider}/`)
|
||||
);
|
||||
const lkgpRecord = lkgpProvider;
|
||||
const providerName = lkgpRecord.provider;
|
||||
const connId = lkgpRecord.connectionId;
|
||||
|
||||
let lkgpIndex = -1;
|
||||
if (connId) {
|
||||
lkgpIndex = orderedTargets.findIndex(
|
||||
(target) => target.provider === providerName && target.connectionId === connId
|
||||
);
|
||||
}
|
||||
if (lkgpIndex < 0) {
|
||||
lkgpIndex = orderedTargets.findIndex(
|
||||
(target) =>
|
||||
target.provider === providerName || target.modelStr.startsWith(`${providerName}/`)
|
||||
);
|
||||
}
|
||||
|
||||
if (lkgpIndex > 0) {
|
||||
const [lkgpTarget] = orderedTargets.splice(lkgpIndex, 1);
|
||||
orderedTargets.unshift(lkgpTarget);
|
||||
log.info(
|
||||
"COMBO",
|
||||
`[LKGP] Prioritizing last known good provider ${lkgpProvider} for combo "${combo.name}"`
|
||||
`[LKGP] Prioritizing last known good provider ${providerName}${connId ? ` (account ${connId})` : ""} for combo "${combo.name}"`
|
||||
);
|
||||
} else if (lkgpIndex === 0) {
|
||||
log.debug(
|
||||
"COMBO",
|
||||
`[LKGP] Last known good provider ${lkgpProvider} already first for combo "${combo.name}"`
|
||||
`[LKGP] Last known good provider ${providerName}${connId ? ` (account ${connId})` : ""} already first for combo "${combo.name}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2070,12 +2082,13 @@ export async function handleComboChat({
|
||||
|
||||
// Record last known good provider (LKGP) for this combo/model (#919)
|
||||
if (provider) {
|
||||
const connId = target.connectionId || undefined;
|
||||
void (async () => {
|
||||
try {
|
||||
const { setLKGP } = await import("../../src/lib/localDb");
|
||||
await Promise.all([
|
||||
setLKGP(combo.name, target.executionKey, provider),
|
||||
setLKGP(combo.name, combo.id || combo.name, provider),
|
||||
setLKGP(combo.name, target.executionKey, provider, connId),
|
||||
setLKGP(combo.name, combo.id || combo.name, provider, connId),
|
||||
]);
|
||||
} catch (err) {
|
||||
log.warn("COMBO", "Failed to record Last Known Good Provider. This is non-fatal.", {
|
||||
@@ -2400,12 +2413,13 @@ async function handleRoundRobinCombo({
|
||||
});
|
||||
recordedAttempts++;
|
||||
if (provider) {
|
||||
const connId = target.connectionId || undefined;
|
||||
void (async () => {
|
||||
try {
|
||||
const { setLKGP } = await import("../../src/lib/localDb");
|
||||
await Promise.all([
|
||||
setLKGP(combo.name, target.executionKey, provider),
|
||||
setLKGP(combo.name, combo.id || combo.name, provider),
|
||||
setLKGP(combo.name, target.executionKey, provider, connId),
|
||||
setLKGP(combo.name, combo.id || combo.name, provider, connId),
|
||||
]);
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
|
||||
@@ -110,9 +110,17 @@ export async function getCachedProviderConnections(
|
||||
|
||||
// ──────────────── LKGP Cache Wrappers ────────────────
|
||||
|
||||
const lkgpCache = new TTLCache<string | null>(SETTINGS_TTL_MS);
|
||||
interface LKGPRecordCache {
|
||||
provider: string;
|
||||
connectionId?: string;
|
||||
}
|
||||
|
||||
export async function getCachedLKGP(comboName: string, modelId: string): Promise<string | null> {
|
||||
const lkgpCache = new TTLCache<LKGPRecordCache | null>(SETTINGS_TTL_MS);
|
||||
|
||||
export async function getCachedLKGP(
|
||||
comboName: string,
|
||||
modelId: string
|
||||
): Promise<LKGPRecordCache | null> {
|
||||
const cacheKey = `lkgp:${comboName}:${modelId}`;
|
||||
const cached = lkgpCache.get(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
@@ -126,10 +134,11 @@ export async function getCachedLKGP(comboName: string, modelId: string): Promise
|
||||
export async function setCachedLKGP(
|
||||
comboName: string,
|
||||
modelId: string,
|
||||
providerId: string
|
||||
providerId: string,
|
||||
connectionId?: string
|
||||
): Promise<void> {
|
||||
const { setLKGP } = await import("@/lib/db/settings");
|
||||
await setLKGP(comboName, modelId, providerId);
|
||||
await setLKGP(comboName, modelId, providerId, connectionId);
|
||||
lkgpCache.invalidate(`lkgp:${comboName}:${modelId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -405,7 +405,12 @@ export async function resetAllPricing() {
|
||||
|
||||
// ──────────────── LKGP (Last Known Good Provider) ────────────────
|
||||
|
||||
export async function getLKGP(comboName: string, modelId: string): Promise<string | null> {
|
||||
export interface LKGPRecord {
|
||||
provider: string;
|
||||
connectionId?: string;
|
||||
}
|
||||
|
||||
export async function getLKGP(comboName: string, modelId: string): Promise<LKGPRecord | null> {
|
||||
const db = getDbInstance();
|
||||
const key = `${comboName}:${modelId}`;
|
||||
const row = db
|
||||
@@ -413,18 +418,29 @@ export async function getLKGP(comboName: string, modelId: string): Promise<strin
|
||||
.get(key) as { value?: string } | undefined;
|
||||
if (!row?.value) return null;
|
||||
try {
|
||||
return JSON.parse(row.value);
|
||||
const parsed = JSON.parse(row.value);
|
||||
if (typeof parsed === "object" && parsed !== null && "provider" in parsed) {
|
||||
return parsed as LKGPRecord;
|
||||
}
|
||||
return { provider: String(parsed) };
|
||||
} catch {
|
||||
return row.value;
|
||||
return { provider: row.value };
|
||||
}
|
||||
}
|
||||
|
||||
export async function setLKGP(comboName: string, modelId: string, providerId: string) {
|
||||
export async function setLKGP(
|
||||
comboName: string,
|
||||
modelId: string,
|
||||
providerId: string,
|
||||
connectionId?: string
|
||||
) {
|
||||
const db = getDbInstance();
|
||||
const key = `${comboName}:${modelId}`;
|
||||
const value: LKGPRecord = { provider: providerId };
|
||||
if (connectionId) value.connectionId = connectionId;
|
||||
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('lkgp', ?, ?)").run(
|
||||
key,
|
||||
JSON.stringify(providerId)
|
||||
JSON.stringify(value)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,16 +150,16 @@ test("cached LKGP values refresh only after the specific key is invalidated", as
|
||||
const lkgpKey = `${comboName}:${modelId}`;
|
||||
|
||||
await settingsDb.setLKGP(comboName, modelId, "openai");
|
||||
assert.equal(await readCache.getCachedLKGP(comboName, modelId), "openai");
|
||||
assert.deepEqual(await readCache.getCachedLKGP(comboName, modelId), { provider: "openai" });
|
||||
|
||||
db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'lkgp' AND key = ?").run(
|
||||
JSON.stringify("anthropic"),
|
||||
lkgpKey
|
||||
);
|
||||
|
||||
assert.equal(await readCache.getCachedLKGP(comboName, modelId), "openai");
|
||||
assert.deepEqual(await readCache.getCachedLKGP(comboName, modelId), { provider: "openai" });
|
||||
|
||||
await readCache.setCachedLKGP(comboName, modelId, "gemini");
|
||||
|
||||
assert.equal(await readCache.getCachedLKGP(comboName, modelId), "gemini");
|
||||
assert.deepEqual(await readCache.getCachedLKGP(comboName, modelId), { provider: "gemini" });
|
||||
});
|
||||
|
||||
@@ -201,14 +201,37 @@ test("LKGP values can be set, read and cleared", async () => {
|
||||
await settingsDb.setLKGP("combo-a", "model-a", "openai");
|
||||
await settingsDb.setLKGP("combo-a", "model-b", "anthropic");
|
||||
|
||||
assert.equal(await settingsDb.getLKGP("combo-a", "model-a"), "openai");
|
||||
assert.equal(await settingsDb.getLKGP("combo-a", "model-b"), "anthropic");
|
||||
assert.deepEqual(await settingsDb.getLKGP("combo-a", "model-a"), { provider: "openai" });
|
||||
assert.deepEqual(await settingsDb.getLKGP("combo-a", "model-b"), { provider: "anthropic" });
|
||||
|
||||
settingsDb.clearAllLKGP();
|
||||
|
||||
assert.equal(await settingsDb.getLKGP("combo-a", "model-a"), null);
|
||||
});
|
||||
|
||||
test("LKGP stores and retrieves connectionId", async () => {
|
||||
await settingsDb.setLKGP("combo-c", "model-c", "openai", "conn-abc123");
|
||||
|
||||
const record = await settingsDb.getLKGP("combo-c", "model-c");
|
||||
assert.deepEqual(record, { provider: "openai", connectionId: "conn-abc123" });
|
||||
});
|
||||
|
||||
test("LKGP without connectionId omits the field", async () => {
|
||||
await settingsDb.setLKGP("combo-d", "model-d", "anthropic");
|
||||
|
||||
const record = await settingsDb.getLKGP("combo-d", "model-d");
|
||||
assert.deepEqual(record, { provider: "anthropic" });
|
||||
assert.equal("connectionId" in (record as object), false);
|
||||
});
|
||||
|
||||
test("LKGP overwrites connectionId when updated without one", async () => {
|
||||
await settingsDb.setLKGP("combo-e", "model-e", "openai", "conn-old");
|
||||
await settingsDb.setLKGP("combo-e", "model-e", "openai");
|
||||
|
||||
const record = await settingsDb.getLKGP("combo-e", "model-e");
|
||||
assert.deepEqual(record, { provider: "openai" });
|
||||
});
|
||||
|
||||
test("pricing helpers ignore malformed synced data and LKGP falls back to raw values", async () => {
|
||||
const db = core.getDbInstance();
|
||||
|
||||
@@ -234,7 +257,9 @@ test("pricing helpers ignore malformed synced data and LKGP falls back to raw va
|
||||
|
||||
assert.equal(pricing["broken-provider"], undefined);
|
||||
assert.equal(await settingsDb.getPricingForModel("alias-provider", "missing-model"), null);
|
||||
assert.equal(await settingsDb.getLKGP("combo-raw", "model-raw"), "raw-provider-id");
|
||||
assert.deepEqual(await settingsDb.getLKGP("combo-raw", "model-raw"), {
|
||||
provider: "raw-provider-id",
|
||||
});
|
||||
});
|
||||
|
||||
test("pricing helpers resolve aliased providers and tolerate no-op resets", async () => {
|
||||
|
||||
Reference in New Issue
Block a user