Skip to content

fix(grok): report the per-turn token usage grok already writes - #1060

Merged
edwin-zvs merged 1 commit into
mainfrom
fix-grok-token-usage
Aug 1, 2026
Merged

fix(grok): report the per-turn token usage grok already writes#1060
edwin-zvs merged 1 commit into
mainfrom
fix-grok-token-usage

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

grok sessions contributed nothing to the token tally, the lineage turn counts, or the new fleet meter. They reported only the context gauge.

The cause

The adapter believed grok exposed no per-call usage — it says so in its own comment:

contextTokensUsed / contextWindowTokens … the only per-session token figures its files expose (its chat/updates streams carry no billing usage split)

That's not true (any more). Every prompt closes with a turn_completed record in the session's updates.jsonl:

{
  "method": "_x.ai/session/update",
  "params": {
    "sessionId": "95db93dc-…",
    "update": {
      "sessionUpdate": "turn_completed",
      "usage": {
        "inputTokens": 98232, "outputTokens": 2785, "totalTokens": 101017,
        "cachedReadTokens": 65792, "cacheCreationTokens": 0, "reasoningTokens": 924,
        "modelCalls": 3, "costUsdTicks": 1013276000,
        "modelUsage": { "grok-4.5-build": { "inputTokens": 98232, "…": "" } }
      }
    }
  }
}

The adapter already reads this file for subagent updates and was walking straight past it.

What the shape settles

  • inputTokens is the whole prompt side, cachedReadTokens a subset of it. totalTokens is exactly input + output (98232 + 2785 = 101017), which is what proves cache reads are already inside inputTokens rather than additive — exactly the split Cost requires (tokens_cached ⊆ tokens_in).
  • modelUsage keys match ModelChanged. They're spelled grok-4.5-build, identical to chat_history.jsonl's model_id, which is what grok_model_change reports. Verified on disk before relying on it — a divergence here would split one model into two colored series in the meter (spec 0167).
  • One record per prompt, not cumulative. Consecutive turns read 98232 then 95270; a cumulative counter would only climb. So no delta arithmetic, and the caller's line cursor hands each line over exactly once.
  • Subagent records carry their own sessionId and are skipped, so a child's usage can't land on the root's tally.

What I deliberately left out

costUsdTicks is plainly a scaled integer, but nothing states the scale. Reading it as nano-dollars gives $1.01 for that turn; plausible published rates for the model put it nearer $0.34. I'm not shipping a dollar figure that might be off by 3×, so this reports volume only, like every other wrapper adapter. Easy to add once the scale is confirmed.

reasoningTokens is not added to outputTokenstotalTokens accounting says it's already inside it.

Verification

Replayed a real 2-turn session's updates.jsonl through the new parser:

Cost model=grok-4.5-build in=98232 out=2785 cached=65792
Cost model=grok-4.5-build in=95270 out=3816 cached=57088
→ 2 Cost events; tally in=193502 out=6601 cached=122880

Tests use that record verbatim — nothing about the shape is invented. Six new cases: the real split, subagent isolation, fallback when modelUsage is absent, ordinary chunks ignored (their _meta.totalTokens is a running counter that must not be mistaken for a turn total), zero-usage turns emitting nothing, and multi-model turns splitting per model.

Not verified live — that needs a real grok turn against the API, which I didn't run. The parser is exercised against real data; the wiring is a three-line insertion into the existing, already-exercised updates.jsonl poll loop. First grok turn after merge should show tokens on the session and a grok-4.5-build series in the meter.

Also flips supports_cost: true in the adapter's capabilities, and corrects the stale comment.

Binaries

Touches crates/adapter-grok → the code ships in construct-adapter-grok, spawned by the daemon; run the workspace construct as usual.

grok sessions contributed nothing to the token tally, the lineage turn
counts, or the fleet meter — the adapter reported only the context gauge
from `signals.json`, on the belief (recorded in its own comment) that
grok's streams carry no usage split.

They do. Every prompt closes with a `turn_completed` record in the
session's `updates.jsonl` carrying a real usage object: `inputTokens`,
`outputTokens`, `cachedReadTokens`, and a `modelUsage` map breaking the
same figures out per model. The adapter already reads that file for
subagent updates and was walking straight past this.

`totalTokens` is exactly input + output, which is what establishes that
`inputTokens` is the whole prompt side and `cachedReadTokens` a subset of
it — the split `Cost` wants. The `modelUsage` keys are spelled exactly as
`chat_history.jsonl`'s `model_id`, which is what this adapter's
`ModelChanged` carries, so per-model attribution can't disagree with the
session's own model reports and split one model into two series.

Records belonging to a subagent carry that agent's `sessionId` and are
skipped, so a child's usage can't land on the root's tally. The caller's
line cursor hands each line over once, so no dedupe is needed, and a
resume skips prior lines as it already did for the transcript.

`costUsdTicks` is deliberately ignored: it is plainly a scaled integer but
nothing states the scale, and a dollar figure wrong by a factor of ten is
worse than none. Volume only, as every other wrapper adapter reports.

Verified by replaying a real 2-turn session's `updates.jsonl`: two Cost
events, 193.5k in / 6.6k out / 122.9k cached. Tests use a verbatim record
from that file.
@edwin-zvs
edwin-zvs merged commit 4dbfb0f into main Aug 1, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the fix-grok-token-usage branch August 1, 2026 02:39
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