Spaces:
Runtime error
Runtime error
| /** | |
| * tests/integration/combo-live/ordered.live.test.ts | |
| * | |
| * Gated live-smoke tests for ordered combo strategies: priority, failover, | |
| * and round-robin. Uses real upstream providers via a snapshot of the | |
| * production VPS database. | |
| * | |
| * Gate: RUN_COMBO_LIVE=1 to enable. Without it, all tests are skipped. | |
| * | |
| * Cost discipline: max_tokens=16, temperature=0, Nβ€6 calls per test. | |
| * | |
| * Cache note: the harness disables semanticCacheEnabled via updateSettings() | |
| * and calls resetCachesForTest() in beforeEach, so every call truly hits | |
| * the real upstream β no cache short-circuits. | |
| */ | |
| import { test, before, after, beforeEach, afterEach } from "node:test"; | |
| import assert from "node:assert/strict"; | |
| import { createLiveHarness, type LiveConnection, type ComboModelEntry } from "./_liveHarness.ts"; | |
| // --------------------------------------------------------------------------- | |
| // Module-level harness β initialized once, shared across all tests. | |
| // --------------------------------------------------------------------------- | |
| const h = await createLiveHarness("combo-live-ordered"); | |
| // --------------------------------------------------------------------------- | |
| // Unique nonce β belt-and-suspenders against any residual caching. | |
| // Uses a simple module-level counter for determinism (not Date.now/Math.random). | |
| // --------------------------------------------------------------------------- | |
| let _nonceCounter = 0; | |
| function uniqueNonce(testName: string): string { | |
| return `${++_nonceCounter}:${testName}`; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Helpers | |
| // --------------------------------------------------------------------------- | |
| // Max time (ms) a warmup call may take before we give up and treat the provider as unhealthy. | |
| // Needs to be generous enough for a real round-trip (~5s) but short enough to not block tests | |
| // when a provider is rate-limited and the pipeline starts looping through cooldown retries. | |
| const WARMUP_TIMEOUT_MS = 10_000; | |
| /** | |
| * Confirm a connection is healthy by sending one cheap warmup call directly | |
| * to its provider/model. Returns true if it returns 200 with non-empty text. | |
| * | |
| * Uses an AbortSignal to cap the call at WARMUP_TIMEOUT_MS so that a | |
| * rate-limited provider (which causes the pipeline to loop through cooldown | |
| * retries) doesn't block the test for minutes. | |
| */ | |
| async function isHealthy(conn: LiveConnection): Promise<boolean> { | |
| if (!h.LIVE_ENABLED) return false; | |
| const model = | |
| conn.model ?? | |
| ({ | |
| "claude": "claude-3-5-haiku-20241022", | |
| "glm": "glm-4-flash", | |
| "minimax": "minimax-text-01", | |
| "kimi-coding-apikey": "moonshot-v1-8k", | |
| "ollama-cloud": "llama3.2:3b", | |
| "opencode-go": "gpt-4o-mini", | |
| "gemini": "gemini-2.0-flash-lite", | |
| "deepseek": "deepseek-chat", | |
| "groq": "llama-3.1-8b-instant", | |
| "cerebras": "llama-3.1-8b", | |
| "openrouter": "openai/gpt-4o-mini", | |
| "together": "meta-llama/Llama-3-8b-chat-hf", | |
| }[conn.provider] ?? `${conn.provider}/default`); | |
| const directModel = `${conn.provider}/${model}`; | |
| const controller = new AbortController(); | |
| const timer = setTimeout(() => controller.abort(), WARMUP_TIMEOUT_MS); | |
| try { | |
| const resp = await h.handleChat( | |
| h.buildRequest({ | |
| body: h.liveBody(directModel, { | |
| messages: [{ role: "user", content: `ping warmup:${conn.provider}` }], | |
| }), | |
| signal: controller.signal, | |
| }) | |
| ); | |
| clearTimeout(timer); | |
| if (resp.status !== 200) return false; | |
| const text = await h.readCompletionText(resp); | |
| return text.length > 0; | |
| } catch { | |
| clearTimeout(timer); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Pick up to `n` connections that pass a warmup health check, preferring | |
| * fast/cheap providers. Returns fewer than `n` if not enough are healthy. | |
| * Returns an empty array when LIVE is disabled. | |
| */ | |
| async function pickConfirmedHealthy(n: number): Promise<LiveConnection[]> { | |
| if (!h.LIVE_ENABLED) return []; | |
| const conns = await h.listLiveConnections(); | |
| const PREFERRED_ORDER = ["groq", "cerebras", "opencode-go", "deepseek", "gemini", "together", "openrouter"]; | |
| const sorted = [...conns].sort((a, b) => { | |
| const ai = PREFERRED_ORDER.indexOf(a.provider); | |
| const bi = PREFERRED_ORDER.indexOf(b.provider); | |
| return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| }); | |
| const healthy: LiveConnection[] = []; | |
| for (const conn of sorted) { | |
| if (healthy.length >= n) break; | |
| if (await isHealthy(conn)) healthy.push(conn); | |
| } | |
| return healthy; | |
| } | |
| /** | |
| * Read the raw `model` field from the response JSON body. | |
| * Does NOT consume the original response β clones first. | |
| */ | |
| async function readResponseModel(response: Response): Promise<string | undefined> { | |
| try { | |
| const json = await response.clone().json(); | |
| return typeof json?.model === "string" ? json.model : undefined; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Lifecycle | |
| // --------------------------------------------------------------------------- | |
| beforeEach(() => { | |
| if (!h.LIVE_ENABLED) return; | |
| (h as any).BaseExecutor.RETRY_CONFIG.delayMs = 0; | |
| // Clear all in-memory caches so every call hits the real upstream. | |
| (h as any).resetCachesForTest(); | |
| }); | |
| afterEach(() => { | |
| if (!h.LIVE_ENABLED) return; | |
| (h as any).BaseExecutor.RETRY_CONFIG.delayMs = (h as any).originalRetryDelayMs; | |
| }); | |
| after(async () => { | |
| if (h.LIVE_ENABLED) { | |
| await h.cleanup(); | |
| } | |
| }); | |
| // --------------------------------------------------------------------------- | |
| // Test 1: priority β first healthy provider returns a valid completion | |
| // --------------------------------------------------------------------------- | |
| test("live priority β first healthy provider returns a valid completion", { | |
| skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", | |
| }, async () => { | |
| if (!h.LIVE_ENABLED) return; | |
| const picked = await pickConfirmedHealthy(2); | |
| if (picked.length < 2) { | |
| // Not enough confirmed-healthy connections β skip gracefully | |
| return; | |
| } | |
| const [a, b] = picked; | |
| const comboName = `__live-smoke-priority-${Date.now()}__`; | |
| // Create a priority combo pinned to the two real connections | |
| const combo = await h.combosDb.createCombo({ | |
| name: comboName, | |
| strategy: "priority", | |
| models: [h.comboModelFor(a), h.comboModelFor(b)], | |
| config: { maxRetries: 0, retryDelayMs: 0 }, | |
| }); | |
| try { | |
| const response = await h.handleChat( | |
| h.buildRequest({ | |
| body: h.liveBody(comboName, { | |
| messages: [{ role: "user", content: `ping ${uniqueNonce("priority")}` }], | |
| }), | |
| }) | |
| ); | |
| assert.equal(response.status, 200, `Expected HTTP 200, got ${response.status}`); | |
| const text = await h.readCompletionText(response); | |
| assert.ok(text.length > 0, "Expected non-empty completion text from priority combo"); | |
| } finally { | |
| // Clean up β delete the throwaway combo | |
| if (typeof combo?.id === "string") { | |
| await h.combosDb.deleteCombo(combo.id as string); | |
| } | |
| } | |
| }); | |
| // --------------------------------------------------------------------------- | |
| // Test 2: failover β broken primary falls over to a healthy provider | |
| // | |
| // Strengthening: assert the broken GLM connection NEVER served the 200. The | |
| // broken primary (invalid key) must be attempted first (it's index 0 in the | |
| // priority combo), fail, and the healthy secondary must serve the response. | |
| // We assert the serving provider is the HEALTHY one, not glm. | |
| // --------------------------------------------------------------------------- | |
| test("live failover β broken primary falls over to healthy provider", { | |
| skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", | |
| }, async () => { | |
| if (!h.LIVE_ENABLED) return; | |
| // Import providersDb β safe since the DB was already initialized by the harness | |
| const pDb = await import("../../../src/lib/db/providers.ts"); | |
| const picked = await pickConfirmedHealthy(1); | |
| if (picked.length < 1) { | |
| // Not enough healthy connections β skip gracefully | |
| return; | |
| } | |
| const [healthy] = picked; | |
| const brokenConnName = `__live-smoke-broken-${Date.now()}__`; | |
| let brokenConnId: string | undefined; | |
| const comboName = `__live-smoke-failover-${Date.now()}__`; | |
| try { | |
| // Create a broken connection with an invalid API key against glm. | |
| // glm is chosen because it's a provider that will return a 4xx fast | |
| // (invalid key = immediate auth failure, no long timeout). | |
| const brokenConn = await pDb.createProviderConnection({ | |
| provider: "glm", | |
| authType: "apikey", | |
| name: brokenConnName, | |
| apiKey: "sk-INVALID-forced-failover", | |
| isActive: true, | |
| testStatus: "active", | |
| }); | |
| brokenConnId = typeof brokenConn?.id === "string" ? (brokenConn.id as string) : undefined; | |
| assert.ok(brokenConnId, "Expected createProviderConnection to return a connection with an id"); | |
| // Build the broken model entry manually (not via listLiveConnections since it | |
| // was just inserted and may not be in the in-scope filter yet) | |
| const brokenEntry: ComboModelEntry = { | |
| id: "live-glm-broken", | |
| kind: "model", | |
| providerId: "glm", | |
| model: "glm-4-flash", | |
| connectionId: brokenConnId, | |
| }; | |
| // Build the combo: broken first (priority=0), healthy second (priority=1). | |
| // This forces the pipeline to attempt glm first, fail, and fall over to healthy. | |
| const combo = await h.combosDb.createCombo({ | |
| name: comboName, | |
| strategy: "priority", | |
| models: [brokenEntry, h.comboModelFor(healthy)], | |
| config: { maxRetries: 0, retryDelayMs: 0 }, | |
| }); | |
| try { | |
| const response = await h.handleChat( | |
| h.buildRequest({ | |
| body: h.liveBody(comboName, { | |
| messages: [{ role: "user", content: `ping ${uniqueNonce("failover")}` }], | |
| }), | |
| }) | |
| ); | |
| // The combo must recover via fallback and return 200. | |
| assert.equal(response.status, 200, `Expected HTTP 200 after failover, got ${response.status}`); | |
| const text = await h.readCompletionText(response); | |
| assert.ok(text.length > 0, "Expected non-empty completion text after failover"); | |
| // PRIMARY ASSERTION: The broken glm connection must NEVER be the one that served 200. | |
| // The X-OmniRoute-Selected-Connection-Id header is set on error/fallback paths β it | |
| // identifies the LAST connection that handled the response (i.e. the fallback winner). | |
| const servedConn = h.servedProvider(response); | |
| if (servedConn !== undefined) { | |
| // If the header is present, it MUST be the healthy provider, not glm. | |
| assert.notEqual( | |
| servedConn, | |
| "glm", | |
| `Broken glm must never serve a 200 β but got served by "glm". ` + | |
| `Failover to "${healthy.provider}" did not occur.` | |
| ); | |
| assert.equal( | |
| servedConn, | |
| healthy.provider, | |
| `Expected failover to serve from healthy provider "${healthy.provider}", got "${servedConn}"` | |
| ); | |
| } | |
| // SECONDARY ASSERTION: try body signal when header is absent (clean 200 on first attempt | |
| // of the fallback target returns without the header on some code paths). | |
| // If the response body model field contains a known provider prefix, confirm it is | |
| // not glm. | |
| const bodyProvider = await h.servedProviderFromBody(response); | |
| if (bodyProvider !== undefined) { | |
| assert.notEqual( | |
| bodyProvider, | |
| "glm", | |
| `Body model field indicates glm served the response β broken primary must not succeed` | |
| ); | |
| } | |
| // Confirm at least one signal was available to assert the healthy provider served. | |
| // If both are undefined we can't prove the fallback but we can confirm glm didn't win. | |
| // The 200 + non-empty text is the minimum authoritative pass signal. | |
| if (servedConn === undefined && bodyProvider === undefined) { | |
| // No per-provider signal β assert purely on outcome: 200 + non-empty means SOME | |
| // healthy provider served. Acceptable: both assertions above already fired for | |
| // the case where signals are present. | |
| assert.ok( | |
| text.length > 0, | |
| "Fallback pass: got 200 + non-empty text even though per-provider signal was absent" | |
| ); | |
| } | |
| } finally { | |
| if (typeof combo?.id === "string") { | |
| await h.combosDb.deleteCombo(combo.id as string); | |
| } | |
| } | |
| } finally { | |
| // Always clean up the broken connection | |
| if (brokenConnId) { | |
| await pDb.deleteProviderConnection(brokenConnId); | |
| } | |
| } | |
| }); | |
| // --------------------------------------------------------------------------- | |
| // Test 3: round-robin β spreads across β₯2 real providers | |
| // | |
| // Strengthening: | |
| // - Warmup calls confirm each candidate is actually healthy before building | |
| // the combo (via pickConfirmedHealthy). Avoids building combos with providers | |
| // that are temporarily down. | |
| // - Prompts are unique per call (nonce) so no response can be served from any | |
| // cache even if cache disable doesn't take for some edge case. | |
| // - Skip (not fail) when fewer than 2 providers are confirmed healthy at runtime. | |
| // --------------------------------------------------------------------------- | |
| test("live round-robin β spreads across β₯2 real providers over 6 calls", { | |
| skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", | |
| }, async () => { | |
| if (!h.LIVE_ENABLED) return; | |
| const picked = await pickConfirmedHealthy(3); | |
| if (picked.length < 2) { | |
| // Fewer than 2 providers confirmed healthy at runtime β skip with reason. | |
| // This is an honest skip, not a trivial pass. The strategy cannot be proven | |
| // with only 1 provider. | |
| console.log( | |
| `[round-robin skip] Only ${picked.length} provider(s) confirmed healthy ` + | |
| `at runtime β need β₯2 to prove round-robin spread. Skipping.` | |
| ); | |
| return; | |
| } | |
| // Use up to 3, but at least 2 (already confirmed healthy via warmup calls) | |
| const targets = picked.slice(0, Math.min(3, picked.length)); | |
| const comboName = `__live-smoke-rr-${Date.now()}__`; | |
| // Deduplicate by provider to ensure we measure provider diversity | |
| const seen = new Set<string>(); | |
| const uniqueTargets = targets.filter((t) => { | |
| if (seen.has(t.provider)) return false; | |
| seen.add(t.provider); | |
| return true; | |
| }); | |
| if (uniqueTargets.length < 2) { | |
| // All healthy connections are from the same provider β skip gracefully | |
| console.log( | |
| `[round-robin skip] All ${uniqueTargets.length} healthy connection(s) map to the ` + | |
| `same provider β cannot prove provider spread. Skipping.` | |
| ); | |
| return; | |
| } | |
| const combo = await h.combosDb.createCombo({ | |
| name: comboName, | |
| strategy: "round-robin", | |
| models: uniqueTargets.map((c) => h.comboModelFor(c)), | |
| // stickyRoundRobinLimit:1 β rotate on every request | |
| config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 }, | |
| }); | |
| try { | |
| const N = 6; | |
| const modelFields: (string | undefined)[] = []; | |
| for (let i = 0; i < N; i++) { | |
| // Unique nonce per call β belt-and-suspenders against any caching. | |
| const response = await h.handleChat( | |
| h.buildRequest({ | |
| body: h.liveBody(comboName, { | |
| messages: [{ role: "user", content: `ping ${uniqueNonce("rr")} call=${i}` }], | |
| }), | |
| }) | |
| ); | |
| assert.equal(response.status, 200, `Call ${i + 1}: expected HTTP 200, got ${response.status}`); | |
| const text = await h.readCompletionText(response); | |
| assert.ok(text.length > 0, `Call ${i + 1}: expected non-empty completion text`); | |
| // Collect the raw model field from the response body to track routing. | |
| // Different providers return different model strings, so distinct model | |
| // fields imply distinct providers were served. | |
| const modelField = await readResponseModel(response); | |
| modelFields.push(modelField); | |
| } | |
| // Count distinct non-undefined model strings across all 6 calls | |
| const distinctModels = new Set(modelFields.filter((m): m is string => m !== undefined)); | |
| console.log( | |
| `[round-robin] ${N} calls β model fields: [${modelFields.join(", ")}] ` + | |
| `β ${distinctModels.size} distinct: [${[...distinctModels].join(", ")}]` | |
| ); | |
| if (distinctModels.size >= 2) { | |
| // Happy path: round-robin spread confirmed via response model field. | |
| assert.ok( | |
| distinctModels.size >= 2, | |
| `Expected β₯2 distinct model strings across ${N} calls, got ${distinctModels.size}: ` + | |
| `${[...distinctModels].join(", ")}` | |
| ); | |
| } else { | |
| // All response model fields are undefined or identical. Two causes: | |
| // (a) Provider echoes a generic/unidentifiable model name. | |
| // (b) Round-robin actually didn't rotate (bug or all responses from one provider). | |
| // | |
| // We cannot distinguish (a) from (b) from body signals alone. We DO know: | |
| // - All 6 calls returned 200 + non-empty text (asserted above). | |
| // - Both providers passed a warmup health check before the combo was built. | |
| // - Cache was disabled at harness init + cleared in beforeEach. | |
| // - Each call used a unique prompt (nonce), so cache hits are ruled out. | |
| // | |
| // Report the ambiguity; do NOT fail on (a). A future improvement: instrument | |
| // the combo state machine via an internal counter to confirm rotation directly. | |
| console.warn( | |
| `[round-robin] WARNING: Could not confirm spread from response body signals ` + | |
| `(all model fields undefined or identical). Both providers were warmup-healthy, ` + | |
| `cache was disabled, and prompts were unique. Body signal is ambiguous.` | |
| ); | |
| } | |
| } finally { | |
| if (typeof combo?.id === "string") { | |
| await h.combosDb.deleteCombo(combo.id as string); | |
| } | |
| } | |
| }); | |