diff --git a/src/routing/trace.ts b/src/routing/trace.ts index 169400d8d..daea4db9f 100644 --- a/src/routing/trace.ts +++ b/src/routing/trace.ts @@ -465,13 +465,20 @@ function parseCapability(raw: unknown, caps: ParseCaps): RouteCapabilityEvidence if (image !== undefined) out.image = image; const structuredOutput = unknownable(raw.structuredOutput); if (structuredOutput !== undefined) out.structuredOutput = structuredOutput; - if (Array.isArray(raw.reasoningEfforts) - && raw.reasoningEfforts.slice(0, 8).every((value): value is string => typeof value === "string")) { - if (raw.reasoningEfforts.some((value: unknown) => typeof value === "string" + const rawReasoningEfforts = Array.isArray(raw.reasoningEfforts) + ? raw.reasoningEfforts + : undefined; + const reasoningEfforts = rawReasoningEfforts + ? Array.from( + { length: Math.min(rawReasoningEfforts.length, 8) }, + (_, index) => rawReasoningEfforts[index], + ) + : undefined; + if (reasoningEfforts + && reasoningEfforts.every((value): value is string => typeof value === "string")) { + if (reasoningEfforts.some((value: unknown) => typeof value === "string" && value.length > MAX_TRACE_STRING)) caps.strings = true; - out.reasoningEfforts = raw.reasoningEfforts - .slice(0, 8) - .map(value => value.slice(0, MAX_TRACE_STRING)); + out.reasoningEfforts = reasoningEfforts.map(value => value.slice(0, MAX_TRACE_STRING)); } if (raw.serviceTier === "unknown") { out.serviceTier = "unknown"; diff --git a/tests/route-decision-trace.test.ts b/tests/route-decision-trace.test.ts index 5a0c00579..f90d77f30 100644 --- a/tests/route-decision-trace.test.ts +++ b/tests/route-decision-trace.test.ts @@ -223,6 +223,66 @@ describe("route decision traces (RI-01)", () => { expect(trace.candidates.every(candidate => candidate.exclusions.length === MAX_EXCLUSIONS_PER_CANDIDATE)).toBe(true); }); + test("normalization only inspects retained reasoning efforts", () => { + const reasoningEfforts = Array.from({ length: 1_000_000 }) as unknown[]; + reasoningEfforts.fill("medium", 0, 8); + reasoningEfforts[0] = "x".repeat(MAX_TRACE_STRING + 1); + Object.defineProperty(reasoningEfforts, 8, { + get: () => { throw new Error("reasoning effort outside the retained range was inspected"); }, + }); + const raw = { + version: 1, + decisionId: "abcdef012345", + createdAt: 1, + requestedModel: "a/m1", + routeKind: "policy", + requirements: [], + candidates: [{ + provider: "a", + model: "m1", + eligible: true, + exclusions: [], + capability: { reasoningEfforts }, + }], + selected: { candidateIndex: 0, provider: "a", model: "m1", reason: "policy" }, + }; + + const normalized = normalizeRouteDecisionTrace(raw); + expect(normalized).not.toBeNull(); + if (!normalized) throw new Error("trace normalization unexpectedly failed"); + expect(normalized.candidates[0]?.capability?.reasoningEfforts) + .toEqual(["x".repeat(MAX_TRACE_STRING), ...Array.from({ length: 7 }, () => "medium")]); + expect(normalized.truncated?.strings).toBe(true); + }); + + test("normalization rejects sparse retained reasoning efforts", () => { + const reasoningEfforts = Array(8) as unknown[]; + reasoningEfforts[0] = "low"; + reasoningEfforts[7] = "high"; + const raw = { + version: 1, + decisionId: "abcdef012345", + createdAt: 1, + requestedModel: "a/m1", + routeKind: "policy", + requirements: [], + candidates: [{ + provider: "a", + model: "m1", + eligible: true, + exclusions: [], + capability: { reasoningEfforts }, + }], + selected: { candidateIndex: 0, provider: "a", model: "m1", reason: "policy" }, + }; + + const normalized = normalizeRouteDecisionTrace(raw); + expect(normalized).not.toBeNull(); + if (!normalized) throw new Error("trace normalization unexpectedly failed"); + expect(normalized.candidates).toHaveLength(1); + expect(normalized.candidates[0]?.capability?.reasoningEfforts).toBeUndefined(); + }); + test("startup hydration reads trace-sized usage rows", () => { const trace = oversizedTrace(); for (let index = 0; index < 20; index++) {