Skip to content

feat: canonical token buckets across harnesses and the result row - #212

Open
eugeneng04 wants to merge 3 commits into
gke-labs:mainfrom
eugeneng04:feat/token-buckets-design
Open

feat: canonical token buckets across harnesses and the result row#212
eugeneng04 wants to merge 3 commits into
gke-labs:mainfrom
eugeneng04:feat/token-buckets-design

Conversation

@eugeneng04

@eugeneng04 eugeneng04 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Implements one canonical token schema across the agent harnesses and carries the new buckets through to the result row. (First commit is the design doc; second is the implementation.)

{"input", "cached", "cache_write", "reasoning", "output", "total"}   # None = unreported, never a fabricated 0

Why

  • input means different things per provider today: Anthropic reports the uncached prompt, while OpenAI/Gemini report the full prompt including cache hits — so raw cross-harness token/cost comparisons are systematically skewed, and with prompt caching on, cached tokens dominate a multi-turn run's prompt.
  • Reasoning tokens are inconsistently bucketed (separate vs. folded into output).
  • results/normalize.py flattened everything to input/output, dropping cached/reasoning at the row layer.

The anchor rule: input excludes cached tokens (Anthropic's convention, and the one usage leaderboards report).

What's implemented

  • Shared schemaagents/result.py: TOKEN_BUCKETS + empty_tokens().
  • api harnessextract_tokens canonicalizes each provider shape: Anthropic passes through (already non-cached) with cache read/write buckets; Gemini subtracts cached_content_token_count (a documented subset of prompt_token_count) and adds tool-result input tokens; OpenAI subtracts prompt_tokens_details.cached_tokens and splits reasoning_tokens out of completion_tokens. Total prefers the provider total, else the bucket sum.
  • gemini CLI harness — the terminal result.stats block maps to the canonical dict (input = full input − cached, empirically a subset; reasoning derived from the total gap).
  • Row layerResultRow gains cachedTokens / reasoningTokens (additive nullable fields, no SCHEMA_VERSION bump; historical rows stay valid), normalize_tokens reads the canonical keys first with legacy provider aliases kept for old results.json records, and the dashboard schema.d.ts + ingest validator accept the new fields absent-tolerantly.

Not in this PR

  • openclaw still passes provider-native usage through (the aliases flatten it; cached/reasoning stay None until it is canonicalized).
  • antigravity emits the canonical shape via its own decoder (feat(antigravity): read token usage incl. cached from conversation DB #211); folding its local helper into the shared one is a one-line follow-up once both land.
  • The stacked input/cache/output dashboard chart (consumes the new row fields).

Verification

  • 892 Python tests green (updated + new: per-provider extract_tokens cases incl. cache/reasoning subtraction, canonical + legacy + fall-through normalizer cases, row round-trip with the new fields, the pinned producer↔dashboard interface test); ruff clean on changed files.
  • site/ingest/load.mjs change mirrors the existing intOrNull pattern (absent-tolerant for historical rows); JS tests not run locally (no node) — CI covers them.

Design rationale and provider-convention verification: docs/designs/token-accounting.md (included). Related: #211.

Proposes one canonical six-bucket token schema (input, cached, cache_write, reasoning, output, total; None = unreported) that every harness maps onto, with per-provider normalization rules anchored on 'input excludes cached tokens'. Covers the row-schema widening (cached/reasoning on ResultRow + normalizer aliases), moving the shared shape out of the antigravity harness, migration for historical rows, and open questions. The antigravity harness already emits this shape; this design promotes it to the shared contract.
Implements the unified token accounting design (docs/designs/token-accounting.md): one canonical six-bucket token schema — input (non-cached), cached, cache_write, reasoning, output (excludes reasoning), total — with None for unreported buckets, never a fabricated 0.

- agents/result.py: shared TOKEN_BUCKETS + empty_tokens().
- api harness: extract_tokens canonicalizes each provider shape — Anthropic input_tokens passes through (already non-cached) with cache read/write buckets; Gemini subtracts cached_content_token_count (a subset of prompt_token_count) and adds tool_use_prompt tokens; OpenAI subtracts prompt_tokens_details.cached_tokens and splits completion_tokens_details.reasoning_tokens out of output. Total prefers the provider total, else the bucket sum.
- gemini CLI harness: the terminal result.stats block maps to the canonical dict (input = full input − cached; reasoning derived from the total gap).
- results/normalize.py + ResultRow: rows carry cachedTokens/reasoningTokens (additive nullable fields — no SCHEMA_VERSION bump; legacy provider aliases keep historical results.json readable); dashboard schema.d.ts and the ingest validator accept the new fields absent-tolerantly.
- openclaw still passes provider-native usage through (aliases flatten it; cached/reasoning None until canonicalized); antigravity emits the shape via its own decoder (gke-labs#211).
@eugeneng04 eugeneng04 changed the title docs: unified token accounting design (canonical token buckets) feat: canonical token buckets across harnesses and the result row Jul 20, 2026
@eugeneng04 eugeneng04 closed this Jul 20, 2026
@eugeneng04 eugeneng04 reopened this Jul 20, 2026
Comment thread devops_bench/results/row.py
Review follow-up: the leaderboard cost formula (input*p_in + cached*p_cached + cache_write*p_write + output*p_out) needs cache-creation counts, which bill at a premium — dropping them undercounts cache-heavy runs. normalize_tokens now returns all six buckets (with legacy total_tokens/total_token_count and cache_creation_input_tokens aliases for historical records), ResultRow/schema.d.ts gain cacheWriteTokens/totalTokens as additive nullable fields, and the ingest validator accepts them absent-tolerantly. Note: total semantics vary for pre-canonical records (some eras exclude cached/reasoning); documented on the row.
eugeneng04 added a commit to eugeneng04/devops-bench that referenced this pull request Jul 20, 2026
Map Anthropic usage onto the canonical six-bucket shape (input, cached, cache_write, reasoning, output, total; None = unreported): input_tokens passes through as the non-cached prompt, cache reads and cache writes become separate buckets (writes bill at a premium, so folding them into cached — the previous shape — mis-prices cache-heavy runs), reasoning stays None (Anthropic bills thinking inside output_tokens), and total is the full footprint. The per-turn accumulator fallback produces the same shape, empty/no-usage streams yield all-None (never a fabricated 0), and errored paths carry the canonical shape too. Bucket tuple is harness-local until the unified token schema (gke-labs#212) lands. Also trims narration comments in the parser and agent.
eugeneng04 added a commit to eugeneng04/devops-bench that referenced this pull request Jul 22, 2026
Map Anthropic usage onto the canonical six-bucket shape (input, cached, cache_write, reasoning, output, total; None = unreported): input_tokens passes through as the non-cached prompt, cache reads and cache writes become separate buckets (writes bill at a premium, so folding them into cached — the previous shape — mis-prices cache-heavy runs), reasoning stays None (Anthropic bills thinking inside output_tokens), and total is the full footprint. The per-turn accumulator fallback produces the same shape, empty/no-usage streams yield all-None (never a fabricated 0), and errored paths carry the canonical shape too. Bucket tuple is harness-local until the unified token schema (gke-labs#212) lands. Also trims narration comments in the parser and agent.
jessie1111101 added a commit that referenced this pull request Jul 22, 2026
Adds turns (agent trajectory steps = tool calls + text turns) as a per-run
efficiency field, unblocking the Phase 2 efficiency axes.

- results/row.py: ResultRow.turns (int | None); normalize.py derives it from the
  record's trajectory length (None when no trajectory captured, 0 for a run that
  took no steps).
- site: schema.d.ts + load.mjs (optional validation) + PROTOCOL.md document it.
- tests: normalize turns counting + loader validation.

Independent of the token-buckets work (#212) — turns comes from the trajectory,
not token usage — but touches the same row/normalize/schema/load files, so
expect a trivial rebase on whichever of {this, #212, #195, #206} lands second.
pytest results green; site vitest 97/97.

@jessie1111101 jessie1111101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm, but we may want to port this PR to kubernetes-sigs rather than merge here

@eugeneng04

Copy link
Copy Markdown
Collaborator Author

Migration note: partial port to kubernetes-sigs

The portable slice of this PR is up as kubernetes-sigs/devops-bench#38 — the parts that touch modules already migrated upstream:

  • agents/result.py — shared TOKEN_BUCKETS + empty_tokens()
  • agents/cli/gemini_cli/parsing.py (+ test)
  • results/normalize.py, results/row.py (+ test)

Still to port once their modules migrate (they target code not yet on kubernetes-sigs):

  • agents/api/agent.py extract_tokens canonicalization (+ tests/unit/agents/api/test_agents_api_agent.py) — the api harness hasn't migrated.
  • site/ingest/load.mjs and site/src/lib/schema.d.tssite/ hasn't migrated.
  • docs/designs/token-accounting.md + the docs/README.md index line.

When those modules land upstream, port the remaining hunks so the token schema is complete there.

jessie1111101 added a commit that referenced this pull request Jul 28, 2026
Adds turns (agent trajectory steps = tool calls + text turns) as a per-run
efficiency field, unblocking the Phase 2 efficiency axes.

- results/row.py: ResultRow.turns (int | None); normalize.py derives it from the
  record's trajectory length (None when no trajectory captured, 0 for a run that
  took no steps).
- site: schema.d.ts + load.mjs (optional validation) + PROTOCOL.md document it.
- tests: normalize turns counting + loader validation.

Independent of the token-buckets work (#212) — turns comes from the trajectory,
not token usage — but touches the same row/normalize/schema/load files, so
expect a trivial rebase on whichever of {this, #212, #195, #206} lands second.
pytest results green; site vitest 97/97.
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.

2 participants