diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 4ab2ef592b0e..ae7365ad2b57 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1,5 +1,6 @@ import { injectMemoryAndSkills } from "./chatCore/memorySkillsInjection.ts"; import { resolveChatCoreRequestSetup } from "./chatCore/requestSetup.ts"; +import { buildFailureUsageRecord } from "./chatCore/failureUsage.ts"; import { checkIdempotencyCache } from "./chatCore/idempotency.ts"; import { checkSemanticCache } from "./chatCore/semanticCache.ts"; import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts"; @@ -708,23 +709,23 @@ export async function handleChatCore({ payload?: unknown, maxDepth = 3 ): EffectiveServiceTier | null => resolveReportedServiceTierFor(provider, payload, maxDepth); + // Failure usage record building extracted to chatCore/failureUsage.ts (#3501); the handler keeps + // the fire-and-forget save + computes latencyMs, so the call sites stay byte-identical. const persistFailureUsage = (statusCode: number, errorCode?: string | null) => { - saveRequestUsage({ - provider: provider || "unknown", - model: model || "unknown", - tokens: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, reasoning: 0 }, - status: String(statusCode), - success: false, - latencyMs: Date.now() - startTime, - timeToFirstTokenMs: 0, - errorCode: errorCode || String(statusCode), - timestamp: new Date().toISOString(), - connectionId: connectionId || undefined, - apiKeyId: apiKeyInfo?.id || undefined, - apiKeyName: apiKeyInfo?.name || undefined, - serviceTier: effectiveServiceTier, - comboStrategy: isCombo ? comboStrategy || undefined : undefined, - }).catch(() => {}); + saveRequestUsage( + buildFailureUsageRecord({ + provider, + model, + connectionId, + apiKeyInfo, + effectiveServiceTier, + isCombo, + comboStrategy, + statusCode, + errorCode, + latencyMs: Date.now() - startTime, + }) + ).catch(() => {}); }; // Key-health updater extracted to chatCore/keyHealth.ts (#3501); bind the per-request log once diff --git a/open-sse/handlers/chatCore/failureUsage.ts b/open-sse/handlers/chatCore/failureUsage.ts new file mode 100644 index 000000000000..fd1860eba9f9 --- /dev/null +++ b/open-sse/handlers/chatCore/failureUsage.ts @@ -0,0 +1,39 @@ +/** + * chatCore failed-request usage record builder (Quality Gate v2 / Fase 9 — chatCore god-file + * decomposition, #3501). + * + * Pure core of handleChatCore's persistFailureUsage closure: builds the usage-history entry for a + * failed request (zeroed tokens/timing, success:false, the unknown/undefined fallbacks, and the + * combo-strategy gate). The handler keeps the impure parts byte-identically: it computes + * `latencyMs` (Date.now() - startTime) and fires the fire-and-forget saveRequestUsage(...).catch(). + */ + +export function buildFailureUsageRecord(opts: { + provider: string | null | undefined; + model: string | null | undefined; + connectionId: string | null | undefined; + apiKeyInfo: { id?: string; name?: string } | null | undefined; + effectiveServiceTier: string; + isCombo: boolean; + comboStrategy: string | null | undefined; + statusCode: number; + errorCode: string | null | undefined; + latencyMs: number; +}) { + return { + provider: opts.provider || "unknown", + model: opts.model || "unknown", + tokens: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, reasoning: 0 }, + status: String(opts.statusCode), + success: false, + latencyMs: opts.latencyMs, + timeToFirstTokenMs: 0, + errorCode: opts.errorCode || String(opts.statusCode), + timestamp: new Date().toISOString(), + connectionId: opts.connectionId || undefined, + apiKeyId: opts.apiKeyInfo?.id || undefined, + apiKeyName: opts.apiKeyInfo?.name || undefined, + serviceTier: opts.effectiveServiceTier, + comboStrategy: opts.isCombo ? opts.comboStrategy || undefined : undefined, + }; +} diff --git a/tests/unit/chatcore-failure-usage.test.ts b/tests/unit/chatcore-failure-usage.test.ts new file mode 100644 index 000000000000..cc20cf78240e --- /dev/null +++ b/tests/unit/chatcore-failure-usage.test.ts @@ -0,0 +1,79 @@ +// tests/unit/chatcore-failure-usage.test.ts +// Characterization of buildFailureUsageRecord — the failed-request usage payload builder extracted +// from handleChatCore's persistFailureUsage closure (chatCore god-file decomposition, #3501). Pure: +// the handler keeps the fire-and-forget saveRequestUsage(...).catch() call and computes latencyMs; +// this builds the record. Locks the unknown/undefined fallbacks, the zeroed token/timing fields, +// the combo-strategy gate, and the ISO timestamp. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildFailureUsageRecord } from "../../open-sse/handlers/chatCore/failureUsage.ts"; + +const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + +test("maps a fully-populated failure into the usage record", () => { + const r = buildFailureUsageRecord({ + provider: "openai", + model: "gpt-4o", + connectionId: "conn-1", + apiKeyInfo: { id: "key-1", name: "My Key" }, + effectiveServiceTier: "priority", + isCombo: true, + comboStrategy: "weighted", + statusCode: 429, + errorCode: "rate_limited", + latencyMs: 1234, + }); + assert.equal(r.provider, "openai"); + assert.equal(r.model, "gpt-4o"); + assert.deepEqual(r.tokens, { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, reasoning: 0 }); + assert.equal(r.status, "429"); + assert.equal(r.success, false); + assert.equal(r.latencyMs, 1234); + assert.equal(r.timeToFirstTokenMs, 0); + assert.equal(r.errorCode, "rate_limited"); + assert.match(String(r.timestamp), ISO); + assert.equal(r.connectionId, "conn-1"); + assert.equal(r.apiKeyId, "key-1"); + assert.equal(r.apiKeyName, "My Key"); + assert.equal(r.serviceTier, "priority"); + assert.equal(r.comboStrategy, "weighted"); +}); + +test("applies the unknown/undefined fallbacks", () => { + const r = buildFailureUsageRecord({ + provider: null, + model: undefined, + connectionId: null, + apiKeyInfo: null, + effectiveServiceTier: "standard", + isCombo: false, + comboStrategy: "priority", + statusCode: 502, + errorCode: null, + latencyMs: 7, + }); + assert.equal(r.provider, "unknown"); + assert.equal(r.model, "unknown"); + assert.equal(r.errorCode, "502"); // falls back to String(statusCode) + assert.equal(r.connectionId, undefined); + assert.equal(r.apiKeyId, undefined); + assert.equal(r.apiKeyName, undefined); + // isCombo false → comboStrategy is dropped even when supplied + assert.equal(r.comboStrategy, undefined); +}); + +test("combo strategy is included only for combo requests", () => { + const combo = buildFailureUsageRecord({ + provider: "x", model: "y", connectionId: null, apiKeyInfo: null, + effectiveServiceTier: "standard", isCombo: true, comboStrategy: "round-robin", + statusCode: 500, errorCode: "boom", latencyMs: 1, + }); + assert.equal(combo.comboStrategy, "round-robin"); + + const comboNoStrategy = buildFailureUsageRecord({ + provider: "x", model: "y", connectionId: null, apiKeyInfo: null, + effectiveServiceTier: "standard", isCombo: true, comboStrategy: null, + statusCode: 500, errorCode: "boom", latencyMs: 1, + }); + assert.equal(comboNoStrategy.comboStrategy, undefined); +});