Files
cursor-byok/server
kevin9327 ab4d3adee5 fix(model): stop token accumulation from erasing counts a later call omits
`Usage::add_assign` folds every field through

    fn sum(left: Option<u64>, right: Option<u64>) -> Option<u64> {
        left?.checked_add(right?)
    }

so `sum(Some(900), None)` is `None`, not `Some(900)`. Adding a record that does
not report a field therefore does not leave the running total alone - it wipes
it.

Both accumulators are per-turn and run over every provider call in the turn:
`run/engine.rs::accumulate_usage` (the compaction call plus each model cycle)
and `cursor/conversation/output.rs::turn_usage`, which is what `turn_ended`
reports to Cursor.

Providers report these fields inconsistently *across calls of one turn*, which
is all it takes. `openai_usage` reads `cache_read_tokens` from
`prompt_tokens_details`, an object most OpenAI-compatible gateways omit until
the prompt cache warms: call 1 (cold) yields `None`, call 2 yields
`Some(1024)`, and the turn reports `None`. The same happens to
`reasoning_tokens` when only some cycles reason, and to `total_tokens` on
gateways that omit it from the streaming usage chunk. Any tool-using turn makes
more than one call, so this is the normal case rather than an edge case.

Treat an unreported count as zero and keep the field unknown only when neither
side reported it. `checked_add` also becomes `saturating_add`: with the new
rule, silently turning an overflow into `None` would be the same erasure by
another route, and token counts never approach `u64::MAX` anyway.
`context_input_tokens` is untouched.

Before (with `left?.checked_add(right?)` restored):
  cargo test -p cursor-server --lib model::observability
  -> 2 failed: assertion `left == right` failed: left: None, right: Some(900)
After:
  cargo test -p cursor-server --lib model::observability -> 3 passed
2026-08-31 19:39:02 +09:00
..
2026-08-30 13:35:03 +08:00