Skip to content

(MOT-4375) fix(harness,providers): session-total cost in the chip; stop double-billing cached tokens - #740

Merged
rohitg00 merged 4 commits into
mainfrom
fix/context-chip-session-cost
Aug 7, 2026
Merged

(MOT-4375) fix(harness,providers): session-total cost in the chip; stop double-billing cached tokens#740
rohitg00 merged 4 commits into
mainfrom
fix/context-chip-session-cost

Conversation

@rohitg00

@rohitg00 rohitg00 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Two defects found while chasing a "deepseek tokenizer counting seems wrong" report. The tokenizer was fine; the money was not.

1. Per-step cost read as a bouncing session total

The context chip's cost line showed the last generation step's bill. On providers with steep cache discounts the per-step number legitimately swings two orders of magnitude between cache-miss and cache-hit steps — so during a live turn the line bounced ($0.06 → $0.04 → $0.09 → $0.00014), and the final near-free cached step read as what the whole session cost.

  • ContextSnapshotV1 gains session_cost_usd, accumulated at the snapshot write site on top of the stored snapshot's total. The turn loop is the session's only writer, so the read-back is race-free; seeding from the store keeps the total honest across turns and harness restarts. Wire-schema golden regenerated.
  • The chip moves per-step cost onto the line it describes — last step 6 in · output 28 · $0.0127 — and renders session total $0.1415 as its own only-grows line, with a tooltip explaining why the step number swings and this one does not.

2. Cached tokens double-billed on six providers

The Usage contract: input is the cache-miss slice, disjoint from cache_read — pricing bills the splits additively. deepseek, anthropic, and claude-code report disjoint splits natively and were correct. openai, kimi, zai, llamacpp, openai-codex, and xai (both its chat-completions and Responses merges) mapped the wire's prompt_tokens / input_tokens total (which includes the cached slice) straight into input while also setting cache_read — billing every cached token at the full input rate plus the cache rate. On an agent loop resending a large cached prefix every turn, reported cost read near-double, and exactified context totals inflated the same way.

Each provider's usage merge now derives the miss slice (total − cached), with the contract documented at the site. Fixtures that pinned the double-count as expected are corrected (12-token prompt with 4 cached asserts 8 in · 4 cache_read); a new codex test pins the subtraction for the Responses API shape; the openai responses fixture has no cached slice, so its expectation correctly stays the full total.

That covers all nine providers on main; provider-groq is not on main yet (draft #712) and gets the same fix before it merges.

Verification

Live on a rig: two turns in one session — step cost $0.1288 (cold) then $0.0127 (99% cache hit), session total accumulating to $0.1415, popover rendering both lines as designed. Per-step billed token sequence for the reporting deepseek session confirmed strictly monotonic (7.5k → 105k over 14 steps), ruling out the tokenizer. Gates on all six crates: cargo fmt --check, cargo clippy --all-targets -D warnings, full test suites green.

Follow-up (not in this PR)

The deepseek curated catalog claims a 1,000,000-token window for both v4 models. If v4-flash is a 128k-class model, context percentages understate ~8× and compaction fires late — needs a spec check (tracked in MOT-4375).

Linear: MOT-4375.

…I-shaped wires

The Usage contract is `input` = the cache-MISS slice, disjoint from
`cache_read` — pricing bills the splits additively. deepseek and anthropic
report disjoint splits natively and were already correct. openai, kimi,
zai, llamacpp, and openai-codex mapped the wire's `prompt_tokens` /
`input_tokens` TOTAL — which includes the cached slice — straight into
`input` while also setting `cache_read`, billing every cached token at the
full input rate plus the cache rate. On an agent loop resending a large
cached prefix every turn, reported cost read near-double, and the
exactified context totals inflated the same way.

The miss slice is now derived (`total - cached`) in each provider's usage
merge, with the contract documented at the site. Test fixtures that pinned
the double-count as expected are corrected — a 12-token prompt with 4
cached asserts 8 in, 4 cache_read — and a new codex test pins the
subtraction for the Responses API shape. The openai `responses` fixture
carries no cached slice, so its expectation stays the full total.
…and chip

The chip's cost line showed the LAST generation step's bill. On providers
with steep cache discounts that number legitimately swings two orders of
magnitude between cache-miss and cache-hit steps, so during a live turn it
read as a bouncing total, and the final (heavily cached, near-free) step
read as what the whole session cost.

`ContextSnapshotV1` gains `session_cost_usd`, accumulated at the snapshot
write site on top of the stored snapshot's total — the turn loop is the
session's only writer, so the read-back is race-free, and seeding from the
store keeps the total honest across turns and harness restarts. Wire
schema golden regenerated for the new field.

The chip moves per-step cost onto the line it describes — `last step 6 in
· output 28 · $0.0127` — and renders `session total $0.1415` as its own
line that only grows, with a tooltip saying exactly why the step number
swings and this one does not.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 7, 2026 12:33pm
workers-tech-spec Ready Ready Preview Aug 7, 2026 12:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rohitg00, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 028a227a-a75f-40a8-a982-228ff4364d4c

📥 Commits

Reviewing files that changed from the base of the PR and between 534de30 and 35e13fe.

⛔ Files ignored due to path filters (1)
  • provider-xai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • harness/src/turn_loop.rs
  • harness/ui/src/lib/metrics.ts
  • provider-xai/src/responses.rs
  • provider-xai/src/sse.rs
  • provider-xai/src/upstream.rs
  • provider-xai/tests/integration.rs
📝 Walkthrough

Walkthrough

The PR adds cumulative session cost to context snapshots and displays it in the context UI. It also normalizes provider input usage by subtracting cached tokens and recording cache-read usage.

Changes

Session cost tracking

Layer / File(s) Summary
Snapshot cost contract
harness/src/context_snapshot.rs, harness/tests/golden/schemas/harness.metrics.json, harness/ui/src/lib/metrics.ts
ContextSnapshotV1 and ContextSnapshot now expose optional cumulative session cost. The schema and round-trip fixture reflect the field.
Turn cost accumulation
harness/src/turn_loop.rs
The turn loop adds reported generation cost to the persisted session total. New snapshots start with no session cost.
Cost display
harness/ui/src/context-chip/index.tsx
The context popover shows per-step cost and cumulative session cost when available.

Provider usage normalization

Layer / File(s) Summary
Cache-aware provider usage
provider-kimi/src/sse.rs, provider-llamacpp/src/sse.rs, provider-openai-codex/src/sse.rs, provider-openai/src/sse.rs, provider-zai/src/sse.rs
Usage merging records cached tokens and reports input tokens after subtracting cached tokens.
Stream usage expectations
provider-kimi/src/upstream.rs, provider-llamacpp/src/upstream.rs, provider-openai-codex/src/upstream.rs, provider-openai/src/upstream.rs, provider-zai/src/upstream.rs
Happy-stream assertions now expect cache-exclusive input usage. A Codex regression test verifies the calculation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Generation
  participant TurnLoop
  participant ContextSnapshot
  participant ContextPopover
  Generation->>TurnLoop: report usage and cost
  TurnLoop->>ContextSnapshot: accumulate session_cost_usd
  ContextSnapshot->>ContextPopover: provide snapshot and step usage
  ContextPopover->>ContextPopover: display step and session cost
Loading

Possibly related PRs

  • iii-hq/workers#602: Both changes extend usage accounting for session metrics.
  • iii-hq/workers#686: Both changes extend ContextSnapshotV1 and turn_loop with session-level accounting.
  • iii-hq/workers#734: Both changes expose cumulative session cost through context snapshot metrics.

Suggested reviewers: ytallo

Poem

A rabbit counts each token bright,
Cache-miss numbers now align just right.
Costs hop through snapshots neat,
The UI shows each session’s beat.
Carrots cheer the tests tonight! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes both primary changes: session-total cost display and prevention of double billing for cached tokens.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/context-chip-session-cost

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 55 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
harness/src/context_snapshot.rs (1)

428-428: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add an omitted-field compatibility fixture.

The test only covers session_cost_usd: Some(1.37). Add a pre-change JSON case without this field and assert that it deserializes to None. Also verify that None is omitted during serialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/context_snapshot.rs` at line 428, Add an omitted-field
compatibility fixture alongside the existing session_cost_usd case in the
relevant context snapshot test: deserialize pre-change JSON without
session_cost_usd and assert the field is None, then serialize that value and
verify session_cost_usd is omitted from the JSON output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@harness/src/turn_loop.rs`:
- Around line 742-759: Make the cumulative cost update around snapshot get/set
failure-safe and idempotent: preserve and propagate snapshot read or parse
errors instead of defaulting to zero, and serialize the read-modify-write using
the session lock or an atomic compare-and-set. Track the accounted generation
identified by `(turn_id, step)` and skip cost accumulation when that generation
was already applied, while retaining the existing total for missing or repeated
updates.

---

Nitpick comments:
In `@harness/src/context_snapshot.rs`:
- Line 428: Add an omitted-field compatibility fixture alongside the existing
session_cost_usd case in the relevant context snapshot test: deserialize
pre-change JSON without session_cost_usd and assert the field is None, then
serialize that value and verify session_cost_usd is omitted from the JSON
output.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2aaeb3a4-6ab6-45e7-991f-d2b8869d8cdc

📥 Commits

Reviewing files that changed from the base of the PR and between fc24e3c and 534de30.

⛔ Files ignored due to path filters (4)
  • provider-kimi/Cargo.lock is excluded by !**/*.lock
  • provider-llamacpp/Cargo.lock is excluded by !**/*.lock
  • provider-openai-codex/Cargo.lock is excluded by !**/*.lock
  • provider-openai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • harness/src/context_snapshot.rs
  • harness/src/turn_loop.rs
  • harness/tests/golden/schemas/harness.metrics.json
  • harness/ui/src/context-chip/index.tsx
  • harness/ui/src/lib/metrics.ts
  • provider-kimi/src/sse.rs
  • provider-kimi/src/upstream.rs
  • provider-llamacpp/src/sse.rs
  • provider-llamacpp/src/upstream.rs
  • provider-openai-codex/src/sse.rs
  • provider-openai-codex/src/upstream.rs
  • provider-openai/src/sse.rs
  • provider-openai/src/upstream.rs
  • provider-zai/src/sse.rs
  • provider-zai/src/upstream.rs

Comment thread harness/src/turn_loop.rs Outdated
Comment on lines +742 to +759
// Accumulate the session's running cost on top of the stored
// snapshot's total: the loop is the only writer per session, so the
// read-back is race-free, and seeding from the store keeps the total
// honest across turns and harness restarts.
let step_cost = outcome
.message
.usage
.as_ref()
.and_then(|u| u.cost_usd)
.unwrap_or(0.0);
let prior_cost =
crate::context_snapshot::get(&deps.iii, &record.session_id, cfg.session_timeout_ms)
.await
.ok()
.flatten()
.and_then(|prev| prev.session_cost_usd)
.unwrap_or(0.0);
snapshot.session_cost_usd = Some(prior_cost + step_cost);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make cumulative session-cost persistence failure-safe and idempotent.

The get(...).await.ok().flatten().unwrap_or(0.0) chain collapses a missing snapshot, a storage error, and a parse failure into the same zero value. If the read fails but the write succeeds, this code can replace the previous session total with only step_cost.

The read-modify-write also runs after Line 688 drops _guard and before Line 801 reacquires it. A replay after a restart can add the same turn_id and step again because this code does not check an already-accounted generation. Preserve read errors instead of writing a replacement total. Serialize the update with the session lock or an atomic compare-and-set, and make the (turn_id, step) update idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/turn_loop.rs` around lines 742 - 759, Make the cumulative cost
update around snapshot get/set failure-safe and idempotent: preserve and
propagate snapshot read or parse errors instead of defaulting to zero, and
serialize the read-modify-write using the session lock or an atomic
compare-and-set. Track the accounted generation identified by `(turn_id, step)`
and skip cost accumulation when that generation was already applied, while
retaining the existing total for missing or repeated updates.

Same defect as the five providers in the previous commit, missed because
xai is not on the running rig: both the chat-completions and Responses
usage merges mapped the wire's prompt/input token TOTAL — which includes
the cached slice — straight into `input` while also setting `cache_read`,
billing every cached token twice. The miss slice is now derived
(`total - cached`) at both sites, and the fixtures that pinned the
double-count as expected (12 total with 4 cached) now assert 8 in.

provider-claude-code audited in the same sweep: Anthropic wire reports
disjoint splits natively, correct as-is. That completes all nine
providers on main; groq gets the fix on draft #712 before it merges.
…e, nullable TS type

Review follow-up. The session-cost seed swallowed state read errors into
the first-step default: a transient failure reset the running total to
the current step's cost and displayed the fabricated number. A failed
read now leaves `session_cost_usd` unset for the step — the chip hides
the line rather than lying — with a warning log. Absent-snapshot
seeding (genuine first step) is unchanged.

No lock is added around the read-modify-write: turns are serialized per
session by the fifo harness-turn queue grouped on session_id, and steps
run sequentially within a turn, so the loop is the session's only
writer. The comment at the site now says why.

The TS snapshot type declares `session_cost_usd` nullable to match the
wire schema (`["number","null"]`); the chip already guarded with
`!= null`.
@rohitg00
rohitg00 merged commit 8bba13b into main Aug 7, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant