Repository navigation
[Bug]: OpenCode threads via OpenRouter never show context-window usage in the composer #8388
Description
Activity
- marked [Bug]: OpenCode harness does not expose native usage, pricing, and context data #8661 as a duplicate of this issue
on Aug 29, 2026 Reproduced the same defect class on T3 0.0.36 with a non-OpenRouter OpenCode setup, so this doesn't look OpenRouter-specific.
Root cause (read out of the 0.0.36 server bundle): OpenCode uses its own SDK-based provider adapter, and its
handleSubscribedEventbranch forcase "message.updated"tracks role/text but discardsevent.properties.info.tokens— theAssistantMessage.tokens({ input, output, reasoning, cache: { read, write } }, plusproviderID/modelID) that the OpenCode SDK already delivers on every assistant message update. So the adapter never emitsthread.token-usage.updated, and the existingthread.token-usage.updated -> context-window.updatedpipeline (which Claude and Codex both feed) never sees an OpenCode reading. Same class as #8382, but a different code path: #8382 is the shared ACPusage_updategap, while 0.0.36 OpenCode goes through this SDK adapter.Fix validated locally: emit the snapshot from
message.updated(assistant role + tokens present) through the adapter's existing event idiom, using OpenCode's own canonical semantics from the latest assistant message —usedTokens = input + cache.read + cache.write,totalProcessedTokens = input + output + reasoning + cache.read + cache.write,maxTokensfrom liveprovider.list()model metadata (model.limit.context). No historic summation; a later smaller (post-compaction) snapshot supersedes.Live-validated in the composer: pre-fix, OpenCode threads (including one with 7 completed turns) had zero
context-window.updatedrows; post-fix, turns emit them and the meter populates and keeps updating, e.g.{"usedTokens":199101,"inputTokens":2252,"cachedInputTokens":196849,"outputTokens":305,"reasoningOutputTokens":70,"totalProcessedTokens":199476,"maxTokens":1048576}withusedTokens == input + cache.readholding on every snapshot (54/54 checked), andmaxTokensmatching the model's real context window. The reporter is right that runningopencodedirectly shows a working indicator — that indicator reads exactly theseAssistantMessage.tokens, which T3 was dropping. Covered with regression tests for the snapshot semantics, the memoized/in-flight-deduped metadata lookup (transient failures emit without a denominator and retry), compaction (latest-wins, never summed), and interrupt-safety (abort never erases the last valid snapshot).Adding to the Sep 8 root-cause read — I traced the full pipeline end to end on current
main(not just the bundle), and the fix needs one more piece than "forwardinfo.tokens". Posting in case it saves triage time.Why forwarding turn usage alone won't light the meter. The composer ring renders exclusively from
context-window.updatedthread activities, and the projection (buildContextWindowActivityPayloadinapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts) creates those only fromthread.token-usage.updatedruntime events with a finiteusedTokens >= 0.OpenCodeAdapter.tsnever emits that event at all, andtakeOpenCodeTurnTokenUsage()(apps/server/src/provider/Layers/OpenCodeAdapter.ts:428, accumulator at:415–426) returns usage withoutusedTokensormodelContextWindow— even though theTurnTokenUsagecontract already carriesmodelContextWindow(CodexAdaptersets it, which is why Codex threads render).ClaudeAdapteremits the same event via itsemitThreadTokenUsagepath.The context window is already reachable from this adapter. The bundled OpenCode SDK exposes
config.providers()(GET /config/providers), which returnsmodels[id].limit.context— includinglimitvalues declared inopencode.jsonfor custom providers. Verified live against OpenCode 1.18.30: a custom openai-compatible provider with an explicitlimitreportscontext: 1048576, andzai-coding-plan/glm-5.3-flashreports1000000from models.dev metadata. So the "no meter" symptom is adapter-level, not metadata-level: it reproduces with any OpenCode provider, with or without models.dev data.Empirical confirmation (from T3's local state DB,
projection_thread_activities):context-window.updatedrows exist only for Codex-driver threads; zero rows for every OpenCode thread across providers, and identical behavior across server builds0.0.39-nightly.20260902.1260→20260906.1303.Fix sketch (contained in
OpenCodeAdapter.ts— no contract/server/web changes needed):- Resolve
limit.contextonce perproviderID/modelIDvia the existing SDK client (config.providers()), cached like the other SDK call wrappers. - In the accumulator /
takeOpenCodeTurnTokenUsage(), setusedTokens = inputTokens + outputTokens(inputTokensalready includes cache read/write at:421— same context-occupancy semantics Codex uses) andmodelContextWindowfrom step 1. - Emit
thread.token-usage.updatedthrough the adapter's existing runtime-event pipeline at the points where usage is already taken (turn completion:1131, step-finish:2430, idle reconciliation:2651).
Display-only: OpenCode enforces its own
model.limitfor auto/manual/compact, so compaction behavior is unaffected either way. Happy to follow up with a small PR referencing this issue if useful.- Resolve
Another consequence of the missing OpenCode telemetry, on the V2 orchestrator: long OpenCode threads can get stuck on
ContextHandoffBudgetError("Insufficient context allowance for the provider handoff…") even when the model has a 1M window.Version: 0.0.46-nightly.20261003.2632, Linux x64, OpenCode 1.18.34, OpenCode models served through a plugin that sets
limit.context(OpenCode's/providerreports 1,000,000 for the model).What I verified:
OpenCodeAdapterV2does not implementgetModelContextWindowand never setstokenUsageon provider turns. In my state DB, 0 of 167 OpenCode provider turns havetokenUsage.- So
handoffBudgethas no window and no measured usage. It falls back to the 128,000 default and usesnativeContextEstimate, which sums history bytes. Above about 96 KB of history the budget is 0. - A throwaway unit test against
mainwith the affected thread's numbers (no window, no usage, 256,495-byte estimate) gives budget 0, anddeliverContextHandoffsfails withContextHandoffBudgetError. WithmodelContextWindow: 1_000_000the same input gives 16,000. - On the affected thread, every send after the first failure died in
deliverContextHandoffsright afterprepareProviderHandoff. Switching model,/compactand restarting did not help.
What I could not reproduce: the trigger. The budget only matters once a failed run with no provider turn has to be replayed (
delta_since_target_last_seen). A missing worktree, an unknown model and a mid-turn server restart all left a provider turn or acancelledrun, so a fresh 144 KB thread kept working.This is the same chain as #12931 for Pi, fixed there partly by implementing
getModelContextWindow. For OpenCode the window is already available from the server (config.providers()/limit.context, as noted above); the adapter does not read it. Related: #15997.
Before submitting
Area
apps/server
Steps to reproduce
Expected behavior
The context-window meter should appear and update with used/max tokens, same as it does for Codex and Claude threads. Issue #7622 states OpenCode already sends turn usage to the context meter in general.
Actual behavior
The meter area next to the send button stays completely empty, with no ring and no percentage, even after multiple completed turns with real token usage.
Impact
Minor bug or occasional failure
Version or commit
0.0.34
Environment
Windows 11, T3 Code desktop app, OpenCode provider connected via OpenRouter (API key)
Logs or stack traces
None. No error is shown, the meter is simply absent.
Technical notes
thread.token-usage.updated. The OpenCode-via-OpenRouter path may have the same gap, possibly because some OpenRouter models (especially free ones) omit or delay theusagefield in their API response.Workaround
None found in-app. Running
opencodedirectly in a terminal shows a working "Context X% used" indicator outside of T3 Code.