Skip to content

Token display overstates usage for session-backed proxy providers — surface cache/delta or trim resent context #394

Closed

Description

@dezren39

Problem

For session-backed providers (the local CLI proxies: commandcode via ACP, muse-proxy, codex-proxy, agy-proxy), OpenCode's per-message token record shows the FULL resent context as fresh input on every turn and reports cache.read = 0, even when the downstream session served almost everything from cache. The display therefore overstates real usage for exactly the providers where the warm-session work (developing-today/code#480) is cutting cost, and any quota/failover decision made from these numbers is made from wrong data.

Evidence (2026-10-24, verified live)

End-to-end verification session ses_e914921a9ffeickeigSWt8x9Pr on the sandbox server, provider commandcode, model deepseek/deepseek-v4.1-flash, two turns against the resident-ACP commandcode proxy:

  • Turn 1 assistant message (msg_16eb6e0c3001Ntlxn2l6w7J4DV): tokens {input: 26103, output: 3, cache: {read: 0, write: 0}}
  • Turn 2 assistant message (msg_16eb93165001ZSOEC17Hs5PA5O): tokens {input: 26129, output: 56, cache: {read: 0, write: 0}}

Turn 2 was a delta turn against a persistent cmd session: the proxy sent ~32 chars of new content downstream (vs an 18,899-char full history) and ACP usage reported ~99% of replayed context as cache-read. None of that is visible in OpenCode's record — both turns read as ~26.1k fresh input.

Same shape on the other CLI proxies: the muse route carries ~27k tokens of CLI session scaffolding per call, all displayed as fresh input every turn.

Why it matters

  • The user tracks token usage and makes provider/quota decisions from OpenCode's numbers (plan allowances, failover choices in the keeper's registry). Systematically overstating proxy-provider usage skews those decisions.
  • As warm-session proxies land (code#480), the gap between displayed and real usage grows — the better the caching works, the more wrong the display looks by comparison (it never improves).

Fix options (either, or both)

  1. Display/accounting fix: when a provider response carries usage that distinguishes cache reads (or the proxy reports ACP/session usage), ingest it into the message token record (cache.read) and show net figures in the UI, so a cached turn reads as mostly cache, not fresh input.
  2. Bake the trimming into OpenCode: for providers known to be session-backed (a provider capability flag), OpenCode itself sends delta-only context after the first turn instead of resending the full history, and accounts tokens accordingly — moving the optimization from per-proxy hacks into the client.

Acceptance criteria

  • A two-turn conversation against a session-backed proxy provider shows turn 2 with the bulk of its context under cache read (or a materially reduced input figure), matching the proxy's own usage report within a reasonable tolerance.
  • Non-proxy providers' token records are unchanged.
  • The chosen approach is noted on code#480 so the proxy program and the client stay in agreement about who owns trimming.

Related: #9 (prompt cache optimization across model pools) — adjacent, not the same problem: #9 is about routing for cache hits; this is about the token record/display being wrong for session-backed providers, and optionally moving trimming into the client.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 26, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    Author

    Intake review

    Fleet monitor intake pass (2026-10-26).

    Verified state today — still live

    • No merged PR addresses cache/delta accounting for session-backed proxy providers (searched merged PRs 2026-10-26), and the token record described in the evidence (full resent context as fresh input, cache.read = 0 on a delta turn) has no counter-evidence on main.
    • The issue's own live evidence (2026-10-24, two-turn commandcode session, ~26.1k input on both turns vs ~32 chars of new downstream content) remains the reference measurement for the acceptance criteria.

    Related

    Risks

    • Quota/failover decisions are being made from these numbers today, so the overstatement is not cosmetic: it systematically penalises exactly the providers the warm-session work makes cheapest.
    • Option 2 (client-side delta trimming) is a much larger change than option 1 (accounting/display); taking both on at once is the main scope risk. Effort is set to l for that reason.

    Suggested approach

    • Land option 1 first: ingest proxy/ACP-reported usage into cache.read where a response carries it, so the record matches the proxy's own report. Treat option 2 as a follow-on decision recorded on code#480.

    Labels applied: kind/bug, priority/normal, effort/l, status/needs-triage, meta/enriched. The body already carried acceptance criteria, so it was not edited.

  3. dezren39 commented on Oct 28, 2026

    @dezren39
    Author

    🤖 Agent note · state: triaged — confirmed, fix routed

    • Agent: repo-queue-worker-shift-opencode (lane A)
    • Task: task 2108 (triage)
    • Verdict: the problem is still live at current main 5e77693704, confirmed by code citation (not the issue text alone):

    Evidence

    • packages/core/src/session/usage.ts:10-19 — the token record is a verbatim map of the AI-layer Usage: input ← nonCachedInputTokens, cache.read ← cacheReadInputTokens. Nothing downstream corrects it.
    • packages/ai/src/protocols/openai-chat.ts mapUsage (~:915-937) — the cached figure is derived only from prompt_tokens_details.cached_tokens ?? prompt_cache_hit_tokens ?? cached_tokens ?? cache_read_input_tokens, and packages/ai/src/protocols/shared.ts subtractTokens returns the inclusive total unchanged when that figure is undefined. A session-backed proxy whose cache/session usage arrives in any other shape (the issue's ACP usage report) therefore records all input as fresh and cache.read = 0 — exactly the 2026-10-24 measurement ({input: 26129, cache: {read: 0}} on a ~32-char delta turn).
    • A grep at 5e77693704 finds no file under packages/ naming commandcode, muse-proxy, codex-proxy, or agy-proxy: there is no session-backed capability flag and no ingestion of a downstream ACP session's usage into the record.

    Route: a scoped small_fix packet for the issue's option 1 (accounting) is enqueued in repo-health-opencode-pilot (hidden_files/packet-394-fix.json, dedupe repo-health:v1:fix:developing-today/opencode:issue:394): first pin the exact usage payload the developing-today/code#480 proxies return, then extend the protocol usage mapping so that figure lands in cacheReadInputTokens, with protocol + session-usage tests. Option 2 (client-side delta trimming) is excluded from the packet — it stays a decision to record on code#480, per the issue's acceptance criteria. The issue stays open for the fix.

  4. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed on Oct 28, 2026
  5. dezren39 commented on Oct 28, 2026

    @dezren39
    Author

    🤖 Dispatcher hand-back — executor-saturated · openchamber-dispatcher run 2026-10-28 18:00 CDT

  6. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  7. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Queue task 2900 investigation (Lane A) — payload pinned, root cause located outside this repo:

    What the proxies actually return (developing-today/code bin/, read at code main 2026-10-29):

    • commandcode-proxy.mjs reads the ACP prompt result's usage (result._meta.usage / result.usage, ~line 686) into promptTokens/completionTokens/cacheReadTokens (~line 691). Live-verified figures in its own comments: cacheReadTokens 21632/21778 and 19840/19934 (~99% of input). But the OpenAI-shaped usage it emits — streaming end-chunk and non-streaming body alike — contains only prompt_tokens, completion_tokens, total_tokens. cacheReadTokens is captured and never emitted.
    • muse-proxy.mjs: its session/tokenUsage handler (~line 616) takes only inputTokens/outputTokens, though MSP usage carries cacheReadTokens too (its own comment: 26225 cache-read of 26455 input). Emitted usage likewise has no cached field.
    • codex-proxy.mjs and agy-proxy.mjs: same shape — no cached field is read or emitted.

    This repo needs no code change: packages/ai/src/protocols/openai-chat.ts mapUsage already derives cacheReadInputTokens from usage.prompt_tokens_details.cached_tokens (first in its fallback chain) and nets nonCachedInputTokens via subtractTokens, for both streaming and non-streaming paths; packages/ai/test/provider/openai-chat.test.ts ("parses text and usage stream fixtures") already pins exactly that mapping. packages/core/src/session/usage.ts then records cache.read verbatim. The moment a proxy emits prompt_tokens_details: { cached_tokens: N }, turn-2 records show cache.read = N and only the remainder as fresh input.

    Set-remainder (tracked here per the cross-repo rule; do not re-file per-repo): developing-today/code proxies must emit prompt_tokens_details.cached_tokens from their session usage — noted on developing-today/code#480. Task 2900 is parked on that landing; it will then verify end-to-end and complete. Option 2 (client-side delta trimming) remains out of scope for this issue and is a separate decision on code#480.

  8. 15 remaining items

  9. added
    queue/blockedQueue task is blocked (reason recorded on the task)
    status/blockedBlocked on something named in the issue/PR
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Oct 29, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    Author

    🤖 Agent note · done — resolved end-to-end; closing

    • Agent: repo-worker-opencode (OpenChamber scheduled lane)
    • Session / ID: lane 3fe37e8a-c61b-419c-9e97-769ecd264d5d, queue task 7037 (triage)
    • Verdict: the accounting gap this issue describes is fixed at both ends on current main. The agent's numbers are no longer stuck at full-fresh-input / cache.read = 0 for session-backed proxy providers.

    Where the fix actually lives (option 1 — display/accounting)

    Proxy side — developing-today/code (PR anomalyco#2164, merged 2026-10-30T03:45:54Z, merge 28a70c2b52; verified at code main a8dbda7fc):

    • bin/commandcode-proxy.mjs (stream end-chunk :1303, non-stream body :1328), bin/muse-proxy.mjs, bin/codex-proxy.mjs, and bin/agy-proxy.mjs now emit
      usage.prompt_tokens_details.cached_tokens when the session's cache-read figure is > 0.
    • This is the missing link recorded in the 2026-10-29 investigation: the cached figure was captured but never put on the wire.

    Client side — this repo, already capable and unchanged by this triage (verified at origin/main 3cb0dcfa7e):

    • packages/ai/src/protocols/openai-chat.ts:917-932 — mapUsage reads prompt_tokens_details.cached_tokens into cacheReadInputTokens, and computes nonCachedInputTokens = input − (cached + cacheWrite).
    • packages/core/src/session/usage.ts:11-17 — tokens() maps input ← nonCachedInputTokens, cache.read ← cacheReadInputTokens.
    • Net effect: a delta turn on a session-backed proxy now records the replayed context under cache.read and shows a materially reduced input, instead of ~26k fresh input every turn.

    Acceptance criteria

    • ✅ Turn 2 context lands under cache read / reduced input, matching the proxy's own usage report — the proxy now transmits that report and the client ingests it (same field path the issue's evidence named).
    • ✅ Non-proxy providers unchanged — the added field is only emitted by the four session-backed proxies; mapUsage already handled it for every OpenAI-compatible provider before this change.
    • ✅ Approach agreement with the proxy program: option 1 (proxy reports cache, client accounts for it) is what landed; the alternative client-side trimming (option 2) was not taken. Cross-referencing Warm-session proxies: hold CLI/language-server sessions open instead of spawning per request developing-today/code#480 (warm-session proxies, now closed), whose work this completes the accounting for.

    No further fix task needed. Closing as completed.

  11. added
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    and removed
    status/blockedBlocked on something named in the issue/PR
    queue/blockedQueue task is blocked (reason recorded on the task)
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  12. developing-today commented on Nov 3, 2026

    @developing-today

    agent-note:v1
    state: reviewed (coverage loop-back)
    Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 8532 (triage, queued, priority 30) in repo-health-opencode-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
    Found via: coverage loop-back 2026-11-03

  13. dezren39 commented on Nov 3, 2026

    @dezren39
    Author

    Triage (2026-11-03, queue task 8532) — already closed at 14:11Z; task born stale

    Fresh state: #394 is CLOSED (closedAt 2026-11-03T14:11:29Z, stateReason COMPLETED). Nothing is in flight and no fix is needed.

    Closure evidence (already on this issue): the closing agent note from queue task 7037 records the resolution end-to-end — the display/accounting gap is fixed at both ends: developing-today/code PR anomalyco#2164 (merged 2026-10-30T03:45:54Z, 28a70c2b52) made commandcode/muse/codex/agy proxies emit usage.prompt_tokens_details.cached_tokens when the session cache-read figure is > 0, and the fork ingests that into the message token record.

    Why this task existed: the coverage loop-back posted "still live; in the system as task 8532" here at 16:39Z — 2h28m after the close — and this triage was queued from that read, then claimed at 16:54Z.

    This is now a pattern in one run (recorded for the fleet monitor). Three of the four tasks this lane claimed in ~15 minutes were loop-back re-queues whose source state had already moved: #1148 (closed 16:08Z, re-queued 16:32Z), #394 (closed 14:11Z, re-queued 16:39Z), and #1146 (live tracker, triaged twice already today, third task queued). Only #1154 carried a real delta. Suggested fixes, cheapest first: (1) re-check issue state at claim and settle with already_done in one step — what this lane does, so no work is wasted, only one claim each; (2) have the coverage pass read issue state after it finishes collecting, not per-item during the pass; (3) treat a triage verdict newer than ~24h as "in the system".

    Disposition: already_done, no follow-up task, no queue or status markers left on the closed issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/lLarge: multi-day or open-ended; expect staged check-inskind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedmeta/protocol-gapAn agent touched this without the required agent-note comment or labels; needs review of whypriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this itemuser/reviewNeeds a user-look: Muse reviews first and handles what it can

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions