diff --git a/src/routing/analytics.ts b/src/routing/analytics.ts index 3eb2e540a14..b6c8c7db1d5 100644 --- a/src/routing/analytics.ts +++ b/src/routing/analytics.ts @@ -12,7 +12,7 @@ */ import type { PersistedUsageEntry } from "../usage/log"; -import { estimateRequestCost, serviceTierContext } from "../usage/cost"; +import { computeEntryCost } from "../usage/summary"; import { openRequestHistoryIndex, requestHistoryDb } from "./history/indexer"; export const ANALYTICS_MAX_ROWS = 50_000; @@ -159,19 +159,10 @@ function cooldownTriggering(entry: PersistedUsageEntry | null, status: number): }); } -function successCostUsd( - row: Pick, - entry: PersistedUsageEntry, -): number | null { - if (!entry.usage) return null; - const estimate = estimateRequestCost({ - provider: row.provider, - model: row.model, - usage: entry.usage, - usageStatus: entry.usageStatus, - serviceTier: serviceTierContext(entry), - }); - return estimate ? estimate.cost.total : null; +/** Same attempt-aware pricing as /api/usage, so a refused retry before a measured send is priced. */ +function successCostUsd(entry: PersistedUsageEntry): number | null { + const { isPriced, costTotal } = computeEntryCost(entry); + return isPriced ? costTotal : null; } export async function computeRoutingAnalytics( @@ -246,7 +237,7 @@ export async function computeRoutingAnalytics( if (kind === "success") { const entry = parseEntry(row.rowJson); if (entry) { - rowCostUsd = successCostUsd(row, entry); + rowCostUsd = successCostUsd(entry); if (rowCostUsd !== null) { costTotalUsd += rowCostUsd; costCount += 1; diff --git a/tests/routing/routing-analytics.test.ts b/tests/routing/routing-analytics.test.ts index 68e00e77fdb..882f5f49e21 100644 --- a/tests/routing/routing-analytics.test.ts +++ b/tests/routing/routing-analytics.test.ts @@ -12,6 +12,7 @@ import { } from "../../src/usage/log"; import { closeRequestHistoryIndex } from "../../src/routing/history/indexer"; import { computeRoutingAnalytics } from "../../src/routing/analytics"; +import { computeEntryCost } from "../../src/usage/summary"; import type { OcxConfig } from "../../src/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; @@ -172,6 +173,24 @@ describe("routing analytics (RI-03)", () => { expect(result.priceCoverage).toBe(0); }); + test("a refused retry before a measured send is priced, matching /api/usage", async () => { + const send = { provider: "openai", model: "gpt-5.5", adapter: "openai-responses", durationMs: 10, sendCount: 1, recoveryKinds: [] }; + const attempts = [ + { ...send, ordinal: 1, status: 429, usageStatus: "unreported" as const }, + { ...send, ordinal: 2, status: 200, usageStatus: "reported" as const, usage: { inputTokens: 100, outputTokens: 10 } }, + ]; + const row = entry("r1", { + timestamp: 1, status: 200, durationMs: 20, provider: "openai", model: "gpt-5.5", + usageStatus: "unreported", usage: { inputTokens: 100, outputTokens: 10 }, attempts, + }); + appendUsageEntry(row); + + const result = await computeRoutingAnalytics({}); + expect(result.priceCoverage).toBe(1); + expect(result.estimatedCostUsdTotalSuccessful).toBe(computeEntryCost(row).costTotal); + expect(result.estimatedCostUsdTotalSuccessful).toBeGreaterThan(0); + }); + test("filters scope the analysis", async () => { appendUsageEntry(entry("r1", { timestamp: 1, status: 200, durationMs: 100, provider: "a" })); appendUsageEntry(entry("r2", { timestamp: 2, status: 200, durationMs: 100, provider: "b", model: "m2" }));