The additive contract delivers value at the mapper boundary — every
field is non-overlapping and non-negative, so any caller summing
arbitrary subsets is correct by construction. Two-line helpers that
just sum three or two known fields add API surface without paying for
themselves, and there are no in-tree consumers today. If v2 wants them
at integration time, the right place is a getter on the `Schema.Class`
(matching the `LLMResponse.text` / `reasoning` / `toolCalls` pattern in
the same file), not a static namespace helper.
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.
Defines a single invariant for `LLM.Usage`: every field is non-negative
and every meaningful aggregate is a *sum*, never a difference. Total
billable input = inputTokens + cacheReadInputTokens + cacheWriteInputTokens.
Total billable output = outputTokens + reasoningTokens. Adding two
non-negatives cannot underflow, so consumers can no longer reproduce the
underflow-then-clamp bug class fixed by #26620.
Each protocol mapper now enforces the contract at the provider boundary
via `ProviderShared.subtractTokens`, which clamps with `Math.max(0, …)`
for defense against provider bugs:
- OpenAI Chat / Responses: pull `cached_tokens` out of `prompt_tokens` /
`input_tokens`; pull `reasoning_tokens` out of `completion_tokens` /
`output_tokens`. The provider's `total_tokens` is preserved verbatim.
- Gemini: pull `cachedContentTokenCount` out of `promptTokenCount`.
Gemini already split visible candidates from thoughts.
- Bedrock: pull `cacheReadInputTokens` and `cacheWriteInputTokens` out of
`inputTokens`, matching AWS prompt-caching docs.
- Anthropic: already non-overlapping per the Messages API; pass through.
Adds `Usage.totalInput` / `Usage.totalOutput` helpers for callers that
want the merged view, and a regression test covering the clamp behavior.
The reasoning underflow fixed in #26620 was the most visible symptom of
a broader semantic inconsistency in this package: providers also disagreed
on whether `inputTokens` includes cache reads (Anthropic excluded;
OpenAI/Gemini/Bedrock included), which would silently double-subtract
the moment v2 wired LLM.Usage into Session.getUsage. Normalizing now,
pre-integration, closes both holes in one move.