Skip to content

fix(acp): report cache token buckets under the anthropic_bucketed usage contract - #80

Merged
Yevanchen merged 1 commit into
mainfrom
fix/acp-forward-cached-usage
Jul 24, 2026
Merged

fix(acp): report cache token buckets under the anthropic_bucketed usage contract#80
Yevanchen merged 1 commit into
mainfrom
fix/acp-forward-cached-usage

Conversation

@Yevanchen

Copy link
Copy Markdown
Collaborator

Fill what changed. Use N/A for irrelevant or maintainer-only items.

Summary

  • Forward cachedReadTokens, cachedWriteTokens, and thoughtTokens from the ACP prompt-response usage payload in normalizePromptUsage (previously only input/output/total were kept, so cache buckets were silently dropped).
  • Declare ACP usage as anthropic_bucketed instead of openai_total_with_cached_breakdown, matching OpenCode's actual token semantics (fresh input excludes cache read/write buckets).

Why

  • The mosoo Cost dashboard for OpenCode (acp-fallback) agents only showed avg tokens: Cache Hit was always 0% and cache read/write spend was never billed, because the driver dropped the cache buckets OpenCode reports over ACP (opencode/src/acp/usage.ts buildUsage sends cachedReadTokens/cachedWriteTokens/thoughtTokens).
  • The contract label was also wrong: openai_total_with_cached_breakdown makes the API compute billableInput = inputTokens - cacheReadTokens, but OpenCode normalizes tokens.input to exclude cache tokens ("AI SDK v6 … Always subtract cache tokens", opencode/src/session/session.ts). With cache fields forwarded under the old contract, fresh input would be under-billed; without them, cached tokens vanish from billing entirely. anthropic_bucketed is the contract that matches this payload shape (same as the Claude harness).

Verification

  • Commands: bun test (1074 pass; 1 pre-existing failure in tests/acp-file-system.test.ts also fails on clean origin/main), bun run tc, bun run lint.
  • Manual steps: verified OpenCode's ACP usage shape against sst/opencode source (acp/usage.ts, session/session.ts:getUsage) and the ACP schema (Usage with cachedReadTokens/cachedWriteTokens); traced the mosoo API pipeline (normalizeUsageTokens + calculateUsageCost) to confirm anthropic_bucketed prices fresh input, cache reads, and cache writes at their own rates and fixes the Cache Hit ratio (cache_read / (fresh + cache_read)).
  • Not run: live OpenCode end-to-end run (test:live:opencode).

Impact

  • User/API/contract changes: new usage_event/session_model_call rows for ACP runs now carry cache token buckets and the anthropic_bucketed contract; Cost dashboards start showing non-zero Cache Hit and cache-adjusted spend for OpenCode agents. Historical rows are unaffected (they recorded zero cache tokens under the old label).
  • Generated files / GraphQL / DB / lockfile: N/A
  • Env or config changes: N/A
  • Risk and rollback: low; ACP-only payload change, API already accepts both contract values. Revert the commit to roll back.

Review

  • Closest review areas: src/runtimes/acp/acp-lifecycle-events.ts (normalizePromptUsage), usage-contract handling in mosoo apps/api/src/modules/cost/domain/usage-contract.ts.
  • Known trade-offs: the contract is chosen for OpenCode's semantics; if another ACP agent that reports cache-inclusive input tokens is onboarded later, contract selection may need to become per-agent. The runtime-reported cumulative session cost from usage_update is still superseded by the API's own token-based pricing at turn end (tracked separately).

…ge contract

The ACP prompt-response usage normalizer dropped cachedReadTokens,
cachedWriteTokens, and thoughtTokens, so OpenCode runs always recorded
zero cache usage: the cost dashboard showed 0% cache hit and cache
read/write spend was never billed.

The payload was also stamped openai_total_with_cached_breakdown, but
OpenCode reports fresh input tokens with cache buckets broken out
separately (Anthropic-style semantics), so billing would have
subtracted cache reads from an input count that never contained them.

Forward all token buckets from the ACP usage payload and declare the
anthropic_bucketed contract so the API prices fresh input, cache reads,
and cache writes at their own rates.
@Yevanchen

Copy link
Copy Markdown
Collaborator Author

Clean-context review (delegated): APPROVE-WITH-NOTES, merge recommended.

  • Bucket semantics verified end-to-end against the pinned opencode 1.18.4 source (session.ts adjusts input to EXCLUDE cache buckets) — consistent with the claude runtime's use of the same anthropic_bucketed contract; field names match exactly.
  • No double counting — traced the superproject pipeline (normalizeUsageTokens → calculateUsageCost, plus the reconciliation path storing raw values with single normalization): the OLD openai_total_with_cached_breakdown label was the actual under/over-billing risk; this PR fixes it.
  • Superproject consumability: anthropic_bucketed already in the UsageContract union and all emitted fields declared — no coordinated API change needed; a routine driver pin bump after merge makes it live (current pin d179ed2 predates this).
  • CI green; tests cover both event paths + replay fixture.

Non-blocking follow-ups (pre-existing, this PR strictly improves data): (1) ACP thoughtTokens are recorded but never priced (claude includes thinking in output; opencode splits it out) — track reasoning-token pricing as its own item; (2) totalTokens display semantics diverge between the two runtimes under the same contract (display-only).

@Yevanchen
Yevanchen merged commit e9f3e57 into main Jul 24, 2026
2 checks passed
@Yevanchen
Yevanchen deleted the fix/acp-forward-cached-usage branch July 24, 2026 10:20
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