refactor(llm): trim Usage helpers + Bedrock subtraction

Review pass:
- Drop `Pick<>` type aliases on `Usage.totalInput` / `Usage.totalOutput`
  — the helpers can take `Usage` directly since every field is optional.
- Collapse Bedrock's nested `subtractTokens(subtractTokens(...))` into a
  single subtraction against the summed cache subtotals.
- Drop arithmetic-walkthrough comments in test fixtures (the raw
  fixture values are right next to the expected outputs).
- Generalize the comment on `mapUsage` in `openai-chat.ts` so the
  rationale outlives the PR reference.
This commit is contained in:
Kit Langton 2026-05-10 13:22:49 -04:00
commit 478f3ae50c
6 changed files with 8 additions and 29 deletions

View file

@ -370,10 +370,8 @@ const mapFinishReason = (reason: string): FinishReason => {
// reasoning tokens for any current model.
const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
if (!usage) return undefined
const inputTokens = ProviderShared.subtractTokens(
ProviderShared.subtractTokens(usage.inputTokens, usage.cacheReadInputTokens),
usage.cacheWriteInputTokens,
)
const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
const inputTokens = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal)
return new Usage({
inputTokens,
outputTokens: usage.outputTokens,

View file

@ -292,9 +292,8 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
// OpenAI Chat reports `prompt_tokens` as the total prompt (cached tokens
// included) and `completion_tokens` as the total output (reasoning tokens
// included). The additive `LLM.Usage` contract pulls each subtotal out at
// the boundary so consumers never subtract — eliminating the underflow
// class addressed by opencode#26620.
// included). Pull each subtotal out at the boundary so the additive
// `LLM.Usage` contract holds and consumers never subtract.
const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
if (!usage) return undefined
const cached = usage.prompt_tokens_details?.cached_tokens

View file

@ -46,21 +46,12 @@ export class Usage extends Schema.Class<Usage>("LLM.Usage")({
}) {}
export namespace Usage {
type InputFields = Pick<Usage, "inputTokens" | "cacheReadInputTokens" | "cacheWriteInputTokens">
type OutputFields = Pick<Usage, "outputTokens" | "reasoningTokens">
/**
* Sum of every input-side category: non-cached input + cache reads +
* cache writes. Monotonic; cannot underflow under the additive contract.
*/
export const totalInput = (usage: InputFields) =>
/** Sum of every input-side category. Monotonic under the additive contract. */
export const totalInput = (usage: Usage) =>
(usage.inputTokens ?? 0) + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
/**
* Sum of every output-side category: visible output + reasoning.
* Monotonic; cannot underflow under the additive contract.
*/
export const totalOutput = (usage: OutputFields) => (usage.outputTokens ?? 0) + (usage.reasoningTokens ?? 0)
/** Sum of every output-side category. Monotonic under the additive contract. */
export const totalOutput = (usage: Usage) => (usage.outputTokens ?? 0) + (usage.reasoningTokens ?? 0)
}
export const RequestStart = Schema.Struct({

View file

@ -197,9 +197,6 @@ describe("Gemini route", () => {
expect(response.text).toBe("Hello!")
expect(response.reasoning).toBe("thinking")
expect(response.usage).toMatchObject({
// Additive contract: promptTokenCount=5 includes 1 cached, so
// inputTokens=4 + cacheReadInputTokens=1. Gemini already splits
// candidates from thoughts, so outputTokens=2 + reasoningTokens=1.
inputTokens: 4,
outputTokens: 2,
reasoningTokens: 1,

View file

@ -231,9 +231,6 @@ describe("OpenAI Chat route", () => {
type: "request-finish",
reason: "stop",
usage: {
// Additive contract: prompt_tokens=5 includes 1 cached, so
// inputTokens=4 (non-cached) + cacheReadInputTokens=1.
// completion_tokens=2 includes 0 reasoning, so outputTokens=2.
inputTokens: 4,
outputTokens: 2,
reasoningTokens: 0,

View file

@ -343,9 +343,6 @@ describe("OpenAI Responses route", () => {
reason: "stop",
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
usage: {
// Additive contract: input_tokens=5 includes 1 cached, so
// inputTokens=4 + cacheReadInputTokens=1.
// output_tokens=2 includes 0 reasoning, so outputTokens=2.
inputTokens: 4,
outputTokens: 2,
reasoningTokens: 0,