diff --git a/src/server/request-log.ts b/src/server/request-log.ts index 096a028bff..06cb2cad60 100644 --- a/src/server/request-log.ts +++ b/src/server/request-log.ts @@ -1847,16 +1847,20 @@ export function finishRequestAttempt( export function aggregateAttemptUsage( attempts: readonly PersistedUsageAttempt[], ): FinalizedUsageResult { - const status: UsageStatus = attempts.length > 0 - && attempts.every(attempt => attempt.usageStatus === "unsupported") + // A usage-less attempt that never reached upstream spent nothing (summary.ts `spentNothing`), + // so it cannot leave the row's usage unknown. + const measured = attempts.filter(attempt => attempt.usage + || (attempt.sendCount !== 0 && attempt.locallyAnswered !== true)); + const status: UsageStatus = measured.length > 0 + && measured.every(attempt => attempt.usageStatus === "unsupported") ? "unsupported" - : attempts.some(attempt => ( + : measured.some(attempt => ( attempt.usageStatus === "unreported" || attempt.usageStatus === "unsupported" )) ? "unreported" - : attempts.some(attempt => attempt.usageStatus === "estimated") + : measured.some(attempt => attempt.usageStatus === "estimated") ? "estimated" - : attempts.length > 0 + : measured.length > 0 ? "reported" : "unreported"; diff --git a/tests/usage/request-log-attempt-identity.test.ts b/tests/usage/request-log-attempt-identity.test.ts index e6ca57f4c1..2c01b4f7c8 100644 --- a/tests/usage/request-log-attempt-identity.test.ts +++ b/tests/usage/request-log-attempt-identity.test.ts @@ -56,4 +56,25 @@ describe("request log attempt identity", () => { }, }); }); + + test("a combo leg refused before any send does not make the served row unreported", () => { + // core-combo seals an input-admission refusal like this: no send, no usage. + const refused = beginRequestAttempt(1, "first", "model-a", "openai-chat"); + finishRequestAttempt(refused, 400, 1); + const served = beginRequestAttempt(2, "second", "model-b", "openai-chat"); + noteAttemptSend(served, undefined); + finishRequestAttempt(served, 200, 8, { inputTokens: 10, outputTokens: 2 }); + + expect(refused.usageStatus).toBe("unreported"); + expect(aggregateAttemptUsage([refused, served])).toEqual({ + status: "reported", + totalTokens: 12, + usage: { inputTokens: 10, outputTokens: 2, totalTokens: 12 }, + }); + // A leg that was sent and reported nothing still leaves the total unknown. + const sentSilent = beginRequestAttempt(1, "first", "model-a", "openai-chat"); + noteAttemptSend(sentSilent, undefined); + finishRequestAttempt(sentSilent, 502, 1); + expect(aggregateAttemptUsage([sentSilent, served]).status).toBe("unreported"); + }); }); diff --git a/tests/usage/request-log.test.ts b/tests/usage/request-log.test.ts index d4e805fc4b..4dbcf591ea 100644 --- a/tests/usage/request-log.test.ts +++ b/tests/usage/request-log.test.ts @@ -547,11 +547,9 @@ describe("request log metadata", () => { 1, { inputTokens: 4, outputTokens: 1 }, ); - const unreported = finishRequestAttempt( - beginRequestAttempt(2, "b", "m2", "openai-chat"), - 503, - 1, - ); + const sent = beginRequestAttempt(2, "b", "m2", "openai-chat"); + noteAttemptSend(sent, undefined); + const unreported = finishRequestAttempt(sent, 503, 1); expect(aggregateAttemptUsage([reported, unreported])).toMatchObject({ status: "unreported", usage: { inputTokens: 4, outputTokens: 1 },