From fa29e19863d07af859db4bf19b97584ec53c43a4 Mon Sep 17 00:00:00 2001 From: smartenok-ops Date: Sat, 9 May 2026 23:58:07 -0400 Subject: [PATCH] feat(auth): per-session sticky routing for codex (#1887) Integrated into release/v3.8.0 --- src/instrumentation-node.ts | 15 +- .../041_session_account_affinity.sql | 11 + src/lib/db/sessionAccountAffinity.ts | 121 ++++++++-- src/lib/localDb.ts | 10 + src/sse/handlers/chat.ts | 15 +- tests/unit/sse-auth.test.ts | 223 +++++++++--------- 6 files changed, 264 insertions(+), 131 deletions(-) create mode 100644 src/lib/db/migrations/041_session_account_affinity.sql diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index 50cbd0e208..b55d1e352e 100755 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -136,11 +136,15 @@ export async function registerNodejs(): Promise { } try { - const [{ migrateCodexConnectionDefaultsFromLegacySettings }, { seedDefaultModelAliases }] = - await Promise.all([ - import("@/lib/providers/codexConnectionDefaults"), - import("@/lib/modelAliasSeed"), - ]); + const [ + { migrateCodexConnectionDefaultsFromLegacySettings }, + { startSessionAccountAffinityCleanup }, + { seedDefaultModelAliases }, + ] = await Promise.all([ + import("@/lib/providers/codexConnectionDefaults"), + import("@/lib/db/sessionAccountAffinity"), + import("@/lib/modelAliasSeed"), + ]); let settings = await getSettings(); const passwordState = await ensurePersistentManagementPasswordHash({ logger: console, @@ -161,6 +165,7 @@ export async function registerNodejs(): Promise { console.log( `[STARTUP] Model alias seed: applied=${seededModelAliases.applied.length}, skipped=${seededModelAliases.skipped.length}, failed=${seededModelAliases.failed.length}` ); + startSessionAccountAffinityCleanup(); const migration = await migrateCodexConnectionDefaultsFromLegacySettings(); if (migration.migrated) { diff --git a/src/lib/db/migrations/041_session_account_affinity.sql b/src/lib/db/migrations/041_session_account_affinity.sql new file mode 100644 index 0000000000..5a93707fb3 --- /dev/null +++ b/src/lib/db/migrations/041_session_account_affinity.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS session_account_affinity ( + session_key TEXT NOT NULL, + provider TEXT NOT NULL, + connection_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + last_seen_at INTEGER NOT NULL, + PRIMARY KEY (session_key, provider) +); + +CREATE INDEX IF NOT EXISTS idx_saa_provider ON session_account_affinity(provider); +CREATE INDEX IF NOT EXISTS idx_saa_last_seen ON session_account_affinity(last_seen_at); diff --git a/src/lib/db/sessionAccountAffinity.ts b/src/lib/db/sessionAccountAffinity.ts index 80ba0a729d..dc63011a11 100644 --- a/src/lib/db/sessionAccountAffinity.ts +++ b/src/lib/db/sessionAccountAffinity.ts @@ -1,7 +1,49 @@ -// Stubbed functions for session account affinity (PR 1887 pending) +import { getDbInstance } from "./core"; -export function getSessionAccountAffinity(sessionKey: string, provider: string): any { - return null; +export interface SessionAccountAffinity { + sessionKey: string; + provider: string; + connectionId: string; + createdAt: number; + lastSeenAt: number; +} + +interface SessionAccountAffinityRow { + session_key: string; + provider: string; + connection_id: string; + created_at: number; + last_seen_at: number; +} + +const DEFAULT_TTL_MS = 30 * 60 * 1000; +const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; + +let cleanupTimer: NodeJS.Timeout | null = null; + +function rowToAffinity(row: SessionAccountAffinityRow): SessionAccountAffinity { + return { + sessionKey: row.session_key, + provider: row.provider, + connectionId: row.connection_id, + createdAt: row.created_at, + lastSeenAt: row.last_seen_at, + }; +} + +export function getSessionAccountAffinity( + sessionKey: string, + provider: string +): SessionAccountAffinity | null { + const db = getDbInstance(); + const row = db + .prepare( + `SELECT session_key, provider, connection_id, created_at, last_seen_at + FROM session_account_affinity + WHERE session_key = ? AND provider = ?` + ) + .get(sessionKey, provider) as SessionAccountAffinityRow | undefined; + return row ? rowToAffinity(row) : null; } export function upsertSessionAccountAffinity( @@ -9,23 +51,72 @@ export function upsertSessionAccountAffinity( provider: string, connectionId: string, now: number = Date.now() -): void {} +): void { + const db = getDbInstance(); + db.prepare( + `INSERT INTO session_account_affinity + (session_key, provider, connection_id, created_at, last_seen_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(session_key, provider) DO UPDATE SET + connection_id = excluded.connection_id, + last_seen_at = excluded.last_seen_at` + ).run(sessionKey, provider, connectionId, now, now); +} export function touchSessionAccountAffinity( sessionKey: string, provider: string, now: number = Date.now() -): void {} - -export function deleteSessionAccountAffinity(sessionKey: string, provider: string): void {} - -export function cleanupStaleSessionAccountAffinities( - ttlMs: number = 30 * 60 * 1000, - now: number = Date.now() -): number { - return 0; +): void { + const db = getDbInstance(); + db.prepare( + `UPDATE session_account_affinity + SET last_seen_at = ? + WHERE session_key = ? AND provider = ?` + ).run(now, sessionKey, provider); } -export function startSessionAccountAffinityCleanup(): void {} +export function deleteSessionAccountAffinity(sessionKey: string, provider: string): void { + const db = getDbInstance(); + db.prepare("DELETE FROM session_account_affinity WHERE session_key = ? AND provider = ?").run( + sessionKey, + provider + ); +} -export function stopSessionAccountAffinityCleanupForTests(): void {} +export function cleanupStaleSessionAccountAffinities( + ttlMs: number = DEFAULT_TTL_MS, + now: number = Date.now() +): number { + const db = getDbInstance(); + const cutoff = now - ttlMs; + const result = db + .prepare("DELETE FROM session_account_affinity WHERE last_seen_at < ?") + .run(cutoff); + return Number(result.changes || 0); +} + +export function startSessionAccountAffinityCleanup(): void { + if (cleanupTimer) return; + + try { + cleanupStaleSessionAccountAffinities(); + } catch (error) { + console.warn("[SESSION_AFFINITY] Startup cleanup failed:", error); + } + + cleanupTimer = setInterval(() => { + try { + cleanupStaleSessionAccountAffinities(); + } catch (error) { + console.warn("[SESSION_AFFINITY] Periodic cleanup failed:", error); + } + }, CLEANUP_INTERVAL_MS); + cleanupTimer.unref?.(); +} + +export function stopSessionAccountAffinityCleanupForTests(): void { + if (!cleanupTimer) return; + clearInterval(cleanupTimer); + cleanupTimer = null; +} diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index efe96f3199..f3af94bc83 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -372,3 +372,13 @@ export { } from "./db/oneproxy"; export type { OneproxyProxyRecord, OneproxyStats } from "./db/oneproxy"; + +export { + getSessionAccountAffinity, + upsertSessionAccountAffinity, + touchSessionAccountAffinity, + deleteSessionAccountAffinity, + cleanupStaleSessionAccountAffinities, + startSessionAccountAffinityCleanup, + stopSessionAccountAffinityCleanupForTests, +} from "./db/sessionAccountAffinity"; diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 56cbee63d9..8d57cfe438 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -3,6 +3,8 @@ import { getProviderCredentialsWithQuotaPreflight, markAccountUnavailable, extractApiKey, + isValidApiKey, + extractSessionAffinityKey, } from "../services/auth"; import { getRuntimeProviderProfile, @@ -208,6 +210,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { // T04: client-provided external session header has priority over generated fingerprint. const externalSessionId = extractExternalSessionId(request.headers); const sessionId = externalSessionId || generateStableSessionId(body); + const sessionAffinityKey = extractSessionAffinityKey(body, request.headers) || sessionId; const requestedConnectionId = request.headers.get("x-omniroute-connection")?.trim() || null; if (sessionId) { touchSession(sessionId); @@ -358,6 +361,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { allowedConnections, resolvedModel, { + sessionKey: sessionAffinityKey, ...(target?.connectionId ? { forcedConnectionId: target.connectionId } : {}), } ); @@ -400,6 +404,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { telemetry, { sessionId, + sessionAffinityKey, forceLiveComboTest: isComboLiveTest, forcedConnectionId: target?.connectionId ?? null, allowedConnectionIds: target?.allowedConnectionIds ?? null, @@ -450,7 +455,12 @@ export async function handleChat(request: any, clientRawRequest: any = null) { combo.name, apiKeyInfo, telemetry, - { sessionId, emergencyFallbackTried: true, forceLiveComboTest: isComboLiveTest }, + { + sessionId, + sessionAffinityKey, + emergencyFallbackTried: true, + forceLiveComboTest: isComboLiveTest, + }, combo.strategy, true ); @@ -486,6 +496,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { telemetry, { sessionId, + sessionAffinityKey, forceLiveComboTest: isComboLiveTest, forcedConnectionId: requestedConnectionId, }, @@ -524,6 +535,7 @@ async function handleSingleModelChat( emergencyFallbackTried?: boolean; forceLiveComboTest?: boolean; sessionId?: string | null; + sessionAffinityKey?: string | null; forcedConnectionId?: string | null; allowedConnectionIds?: string[] | null; comboStepId?: string | null; @@ -670,6 +682,7 @@ async function handleSingleModelChat( effectiveAllowedConnections, model, { + sessionKey: runtimeOptions.sessionAffinityKey ?? runtimeOptions.sessionId ?? null, excludeConnectionIds: Array.from(excludedConnectionIds), ...(forceLiveComboTest ? { diff --git a/tests/unit/sse-auth.test.ts b/tests/unit/sse-auth.test.ts index 7ac60df468..23f3f5419d 100644 --- a/tests/unit/sse-auth.test.ts +++ b/tests/unit/sse-auth.test.ts @@ -14,7 +14,6 @@ const settingsDb = await import("../../src/lib/db/settings.ts"); const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const auth = await import("../../src/sse/services/auth.ts"); const quotaCache = await import("../../src/domain/quotaCache.ts"); -const { COOLDOWN_MS } = await import("../../open-sse/config/constants.ts"); async function resetStorage() { core.resetDbInstance(); @@ -45,7 +44,6 @@ async function seedConnection(provider: string, overrides: any = {}) { lastErrorSource: overrides.lastErrorSource, errorCode: overrides.errorCode, backoffLevel: overrides.backoffLevel, - maxConcurrent: overrides.maxConcurrent, providerSpecificData: overrides.providerSpecificData || {}, lastUsedAt: overrides.lastUsedAt, consecutiveUseCount: overrides.consecutiveUseCount, @@ -135,7 +133,7 @@ test("getProviderCredentials enforces generic quota policy unless explicitly byp }, }); const resetAt = futureIso(); - quotaCache.setQuotaCache((connection as any).id, "openai", { + quotaCache.setQuotaCache(connection.id, "openai", { daily: { remainingPercentage: 10, resetAt }, }); @@ -179,38 +177,6 @@ test("getProviderCredentialsWithQuotaPreflight skips exhausted preflight account assert.equal((selected as any).connectionId, healthy.id); }); -test("getProviderCredentials includes per-account maxConcurrent caps", async () => { - const connection = await seedConnection("openai", { - name: "openai-concurrency-cap", - maxConcurrent: 2, - }); - - const selected = await auth.getProviderCredentials("openai"); - - assert.equal(selected.connectionId, connection.id); - assert.equal(selected.maxConcurrent, 2); -}); - -test("getProviderCredentials skips connections that exclude the requested model and selects the next eligible account", async () => { - const excluded = await seedConnection("openai", { - name: "excluded-first", - priority: 1, - providerSpecificData: { - excludedModels: ["gpt-4o*"], - }, - }); - const allowed = await seedConnection("openai", { - name: "allowed-second", - priority: 2, - apiKey: "sk-allowed", - }); - - const selected = await auth.getProviderCredentials("openai", null, null, "gpt-4o-mini"); - - assert.equal(selected.connectionId, allowed.id); - assert.notEqual(selected.connectionId, excluded.id); -}); - test("getProviderCredentialsWithQuotaPreflight returns allRateLimited when a forced connection is blocked by preflight", async () => { const blocked = await seedConnection("openai", { name: "quota-preflight-forced", @@ -237,6 +203,62 @@ test("getProviderCredentialsWithQuotaPreflight returns allRateLimited when a for assert.match(selected.lastError, /quota preflight/i); }); +test("getProviderCredentials keeps separate codex affinity per session", async () => { + await settingsDb.updateSettings({ fallbackStrategy: "round-robin", stickyRoundRobinLimit: 10 }); + const first = await seedConnection("codex", { + name: "codex-affinity-a", + lastUsedAt: new Date(Date.now() - 20_000).toISOString(), + }); + const second = await seedConnection("codex", { + name: "codex-affinity-b", + lastUsedAt: new Date(Date.now() - 10_000).toISOString(), + }); + + const sessionA1 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-a", + }); + const sessionB1 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-b", + }); + const sessionA2 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-a", + }); + const sessionB2 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-b", + }); + + assert.equal(sessionA1.connectionId, first.id); + assert.equal(sessionB1.connectionId, second.id); + assert.equal(sessionA2.connectionId, first.id); + assert.equal(sessionB2.connectionId, second.id); +}); + +test("getProviderCredentials rebinds codex session when affinity connection is excluded", async () => { + await settingsDb.updateSettings({ fallbackStrategy: "round-robin", stickyRoundRobinLimit: 10 }); + const first = await seedConnection("codex", { + name: "codex-affinity-excluded-a", + lastUsedAt: new Date(Date.now() - 20_000).toISOString(), + }); + const second = await seedConnection("codex", { + name: "codex-affinity-excluded-b", + lastUsedAt: new Date(Date.now() - 10_000).toISOString(), + }); + + const initial = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-excluded", + }); + const rebound = await auth.getProviderCredentials("codex", first.id, null, "gpt-5.5", { + sessionKey: "session-excluded", + }); + const sticky = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", { + sessionKey: "session-excluded", + }); + + assert.equal(initial.connectionId, first.id); + assert.equal(rebound.connectionId, second.id); + assert.equal(sticky.connectionId, second.id); +}); + test("resolveQuotaLimitPolicy normalizes Codex windows, thresholds, and defaults", () => { const normalized = auth.resolveQuotaLimitPolicy("codex", { limitPolicy: { @@ -264,7 +286,7 @@ test("resolveQuotaLimitPolicy normalizes Codex windows, thresholds, and defaults }); assert.deepEqual(defaults, { enabled: true, - thresholdPercent: 99, + thresholdPercent: 90, windows: ["session", "weekly"], }); assert.deepEqual(generic, { @@ -314,17 +336,17 @@ test("getProviderCredentials round-robin stays on the current account while belo priority: 2, }); - await providersDb.updateProviderConnection((current as any).id, { + await providersDb.updateProviderConnection(current.id, { lastUsedAt: new Date().toISOString(), consecutiveUseCount: 1, }); - await providersDb.updateProviderConnection((other as any).id, { + await providersDb.updateProviderConnection(other.id, { lastUsedAt: new Date(Date.now() - 60_000).toISOString(), consecutiveUseCount: 0, }); const selected = await auth.getProviderCredentials("openai"); - const updated = await providersDb.getProviderConnectionById((current as any).id); + const updated = await providersDb.getProviderConnectionById(current.id); assert.equal(selected.connectionId, current.id); assert.equal(updated.consecutiveUseCount, 2); @@ -428,7 +450,7 @@ test("getProviderCredentials retains terminal accounts for combo live tests", as const bypassed = await auth.getProviderCredentials("openai", null, null, null, { allowSuppressedConnections: true, }); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(blocked, null); assert.equal(bypassed.connectionId, connection.id); @@ -469,15 +491,9 @@ test("getProviderCredentials reports allRateLimited when every account is model- name: "gemini-model-lock-second", }); + await auth.markAccountUnavailable(first.id, 429, "too many requests", "gemini", "gemini-2.5-pro"); await auth.markAccountUnavailable( - (first as any).id, - 429, - "too many requests", - "gemini", - "gemini-2.5-pro" - ); - await auth.markAccountUnavailable( - (second as any).id, + second.id, 429, "too many requests", "gemini", @@ -504,7 +520,7 @@ test("getProviderCredentials auto-decays stale backoff metadata for recovered ac const selected = await auth.getProviderCredentials("openai"); await flushWrites(); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(selected.connectionId, connection.id); assert.equal(updated.backoffLevel, 0); @@ -522,7 +538,7 @@ test("getProviderCredentials falls back to a five-minute retry window when quota }, }); - quotaCache.setQuotaCache((connection as any).id, "openai", { + quotaCache.setQuotaCache(connection.id, "openai", { daily: { remainingPercentage: 0, resetAt: null }, }); @@ -547,10 +563,10 @@ test("getProviderCredentials prioritizes accounts that still have quota availabl apiKey: "sk-available", }); - quotaCache.setQuotaCache((exhausted as any).id, "openai", { + quotaCache.setQuotaCache(exhausted.id, "openai", { daily: { remainingPercentage: 0, resetAt: futureIso() }, }); - quotaCache.setQuotaCache((available as any).id, "openai", { + quotaCache.setQuotaCache(available.id, "openai", { daily: { remainingPercentage: 65, resetAt: futureIso() }, }); @@ -574,17 +590,17 @@ test("getProviderCredentials round-robin switches to the least recently used acc priority: 2, }); - await providersDb.updateProviderConnection((current as any).id, { + await providersDb.updateProviderConnection(current.id, { lastUsedAt: new Date().toISOString(), consecutiveUseCount: 2, }); - await providersDb.updateProviderConnection((fallback as any).id, { + await providersDb.updateProviderConnection(fallback.id, { lastUsedAt: new Date(Date.now() - 120_000).toISOString(), consecutiveUseCount: 0, }); const selected = await auth.getProviderCredentials("openai"); - const updated = await providersDb.getProviderConnectionById((fallback as any).id); + const updated = await providersDb.getProviderConnectionById(fallback.id); assert.equal(selected.connectionId, fallback.id); assert.equal(updated.consecutiveUseCount, 1); @@ -604,17 +620,17 @@ test("getProviderCredentials round-robin fallback mode excludes the failed accou priority: 2, }); - await providersDb.updateProviderConnection((failed as any).id, { + await providersDb.updateProviderConnection(failed.id, { lastUsedAt: new Date().toISOString(), consecutiveUseCount: 3, }); - await providersDb.updateProviderConnection((fallback as any).id, { + await providersDb.updateProviderConnection(fallback.id, { lastUsedAt: new Date(Date.now() - 120_000).toISOString(), consecutiveUseCount: 0, }); - const selected = await auth.getProviderCredentials("openai" as any, (failed as any).id); - const updated = await providersDb.getProviderConnectionById((fallback as any).id); + const selected = await auth.getProviderCredentials("openai", failed.id); + const updated = await providersDb.getProviderConnectionById(fallback.id); assert.equal(selected.connectionId, fallback.id); assert.equal(updated.consecutiveUseCount, 1); @@ -644,10 +660,10 @@ test("getProviderCredentials least-used prefers accounts that were never used", name: "least-used-never", priority: 9, }); - await providersDb.updateProviderConnection((recentlyUsed as any).id, { + await providersDb.updateProviderConnection(recentlyUsed.id, { lastUsedAt: new Date().toISOString(), }); - await providersDb.updateProviderConnection((neverUsed as any).id, { + await providersDb.updateProviderConnection(neverUsed.id, { lastUsedAt: null, }); @@ -668,10 +684,10 @@ test("getProviderCredentials least-used prefers the oldest timestamp when all ac priority: 1, }); - await providersDb.updateProviderConnection((oldest as any).id, { + await providersDb.updateProviderConnection(oldest.id, { lastUsedAt: new Date(Date.now() - 120_000).toISOString(), }); - await providersDb.updateProviderConnection((newest as any).id, { + await providersDb.updateProviderConnection(newest.id, { lastUsedAt: new Date().toISOString(), }); @@ -723,10 +739,10 @@ test("getProviderCredentials p2c prefers the account with more quota headroom ov }, }); - (quotaCache as any).setQuotaCache(nearLimit.id, "openai", { + quotaCache.setQuotaCache(nearLimit.id, "openai", { daily: { remainingPercentage: 12, resetAt: futureIso(180_000) }, }); - (quotaCache as any).setQuotaCache(healthy.id, "openai", { + quotaCache.setQuotaCache(healthy.id, "openai", { daily: { remainingPercentage: 78, resetAt: futureIso(180_000) }, }); @@ -786,7 +802,7 @@ test("getProviderCredentials exposes copilotToken when present in providerSpecif assert.equal(selected.copilotToken, "copilot-token-value"); }); -test("markAccountUnavailable keeps local 404 failures model-scoped with the local not-found cooldown", async () => { +test("markAccountUnavailable uses configured cooldowns for local 404 model lockouts", async () => { await settingsDb.updateSettings({ providerProfiles: { apikey: { @@ -807,13 +823,13 @@ test("markAccountUnavailable keeps local 404 failures model-scoped with the loca }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 404, "model not found", "openai", "local-model" ); - const updated = await (providersDb as any).getProviderConnectionById(connection.id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.equal(result.cooldownMs, 250); @@ -829,14 +845,14 @@ test("markAccountUnavailable applies a model-only lockout for Gemini 429 respons }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 429, "too many requests", "gemini", "gemini-2.5-pro" ); await flushWrites(); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); @@ -852,14 +868,14 @@ test("markAccountUnavailable applies a model-only lockout for compatible provide }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 429, "The upstream compatible service exhausted its capacity", "openai-compatible-custom-node", "custom-model-a" ); await flushWrites(); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); @@ -869,7 +885,7 @@ test("markAccountUnavailable applies a model-only lockout for compatible provide assert.equal(Number(updated.errorCode), 429); }); -test("markAccountUnavailable uses the unified configured api-key connection cooldown", async () => { +test("markAccountUnavailable honors configured api-key rate-limit cooldowns", async () => { await settingsDb.updateSettings({ providerProfiles: { apikey: { @@ -887,7 +903,7 @@ test("markAccountUnavailable uses the unified configured api-key connection cool }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 429, "too many requests", "openai", @@ -909,20 +925,20 @@ test("markAccountUnavailable stores Codex scope-specific cooldowns without a glo }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 429, "quota reached", "codex", "codex-spark-mini" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); const selected = await auth.getProviderCredentials("codex", null, null, "codex-spark-mini"); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); assert.equal(updated.testStatus, "unavailable"); assert.equal(updated.rateLimitedUntil, undefined); - assert.ok((updated.providerSpecificData as any).codexScopeRateLimitedUntil.spark); + assert.ok(updated.providerSpecificData.codexScopeRateLimitedUntil.spark); assert.equal(selected.allRateLimited, true); }); @@ -932,13 +948,13 @@ test("markAccountUnavailable returns without fallback on bad requests", async () }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 400, "schema mismatch", "openai", "gpt-4o" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.deepEqual(result, { shouldFallback: false, cooldownMs: 0 }); assert.equal(updated.testStatus, "active"); @@ -952,13 +968,8 @@ test("markAccountUnavailable preserves terminal statuses without overwriting the rateLimitedUntil: null, }); - const result = await auth.markAccountUnavailable( - (connection as any).id, - 503, - "upstream error", - "openai" - ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const result = await auth.markAccountUnavailable(connection.id, 503, "upstream error", "openai"); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.equal(result.cooldownMs, 0); @@ -973,13 +984,8 @@ test("markAccountUnavailable reuses an existing connection-wide cooldown", async rateLimitedUntil: retryAfter, }); - const result = await auth.markAccountUnavailable( - (connection as any).id, - 503, - "upstream error", - "openai" - ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const result = await auth.markAccountUnavailable(connection.id, 503, "upstream error", "openai"); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); @@ -1003,21 +1009,18 @@ test("markAccountUnavailable reuses an existing Codex scope cooldown", async () }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 429, "quota reached", "codex", "codex-spark-mini" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); assert.equal(updated.rateLimitedUntil, undefined); - (assert as any).equal( - (updated.providerSpecificData as any).codexScopeRateLimitedUntil.spark, - retryAfter - ); + assert.equal(updated.providerSpecificData.codexScopeRateLimitedUntil.spark, retryAfter); }); test("markAccountUnavailable uses a connection-wide cooldown for non-local 404 errors", async () => { @@ -1029,13 +1032,13 @@ test("markAccountUnavailable uses a connection-wide cooldown for non-local 404 e }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 404, "model not found", "openai", "gpt-missing" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.ok(result.cooldownMs > 0); @@ -1050,17 +1053,17 @@ test("markAccountUnavailable auto-disables permanently banned accounts when the }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 401, "Verify your account to continue", "openai", "gpt-4o" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, false); - assert.equal(updated.testStatus, "banned"); + assert.equal(updated.testStatus, "unavailable"); }); test("markAccountUnavailable leaves permanently banned accounts active when auto-disable is disabled", async () => { @@ -1070,17 +1073,17 @@ test("markAccountUnavailable leaves permanently banned accounts active when auto }); const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 401, "Verify your account to continue", "openai", "gpt-4o" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, true); - assert.equal(updated.testStatus, "banned"); + assert.equal(updated.testStatus, "unavailable"); }); test("markAccountUnavailable swallows auto-disable persistence errors", async () => { @@ -1114,17 +1117,17 @@ test("markAccountUnavailable swallows auto-disable persistence errors", async () try { const result = await auth.markAccountUnavailable( - (connection as any).id, + connection.id, 401, "Verify your account to continue", "openai", "gpt-4o" ); - const updated = await providersDb.getProviderConnectionById((connection as any).id); + const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, true); - assert.equal(updated.testStatus, "banned"); + assert.equal(updated.testStatus, "unavailable"); } finally { db.prepare = originalPrepare; }