Skip to content

[Bug]: OpenCode threads via OpenRouter never show context-window usage in the composer #8388

Description

@muckybuzzwoo

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Connect an OpenCode provider instance in T3 Code with an OpenRouter API key.
  2. Start a new thread and select any OpenRouter-routed model (tested: z-ai/glm-5.3-flash, poolside/laguna-s-2.1:free).
  3. Send several real messages back and forth so real usage accumulates.
  4. Inspect the context-window meter next to the composer send button.

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

Workaround

None found in-app. Running opencode directly in a terminal shows a working "Context X% used" indicator outside of T3 Code.

Activity

  1. IcTxDiogo commented on Sep 8, 2026

    @IcTxDiogo

    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 handleSubscribedEvent branch for case "message.updated" tracks role/text but discards event.properties.info.tokens — the AssistantMessage.tokens ({ input, output, reasoning, cache: { read, write } }, plus providerID/modelID) that the OpenCode SDK already delivers on every assistant message update. So the adapter never emits thread.token-usage.updated, and the existing thread.token-usage.updated -> context-window.updated pipeline (which Claude and Codex both feed) never sees an OpenCode reading. Same class as #8382, but a different code path: #8382 is the shared ACP usage_update gap, 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, maxTokens from live provider.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.updated rows; 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} with usedTokens == input + cache.read holding on every snapshot (54/54 checked), and maxTokens matching the model's real context window. The reporter is right that running opencode directly shows a working indicator — that indicator reads exactly these AssistantMessage.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).

  2. zolped commented on Sep 11, 2026

    @zolped

    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 "forward info.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.updated thread activities, and the projection (buildContextWindowActivityPayload in apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts) creates those only from thread.token-usage.updated runtime events with a finite usedTokens >= 0. OpenCodeAdapter.ts never emits that event at all, and takeOpenCodeTurnTokenUsage() (apps/server/src/provider/Layers/OpenCodeAdapter.ts:428, accumulator at :415–426) returns usage without usedTokens or modelContextWindow — even though the TurnTokenUsage contract already carries modelContextWindow (CodexAdapter sets it, which is why Codex threads render). ClaudeAdapter emits the same event via its emitThreadTokenUsage path.

    The context window is already reachable from this adapter. The bundled OpenCode SDK exposes config.providers() (GET /config/providers), which returns models[id].limit.context — including limit values declared in opencode.json for custom providers. Verified live against OpenCode 1.18.30: a custom openai-compatible provider with an explicit limit reports context: 1048576, and zai-coding-plan/glm-5.3-flash reports 1000000 from 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.updated rows exist only for Codex-driver threads; zero rows for every OpenCode thread across providers, and identical behavior across server builds 0.0.39-nightly.20260902.1260 → 20260906.1303.

    Fix sketch (contained in OpenCodeAdapter.ts — no contract/server/web changes needed):

    1. Resolve limit.context once per providerID/modelID via the existing SDK client (config.providers()), cached like the other SDK call wrappers.
    2. In the accumulator / takeOpenCodeTurnTokenUsage(), set usedTokens = inputTokens + outputTokens (inputTokens already includes cache read/write at :421 — same context-occupancy semantics Codex uses) and modelContextWindow from step 1.
    3. Emit thread.token-usage.updated through 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.limit for auto/manual /compact, so compaction behavior is unaffected either way. Happy to follow up with a small PR referencing this issue if useful.

  3. SpyrosPsarras commented on Oct 5, 2026

    @SpyrosPsarras

    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 /provider reports 1,000,000 for the model).

    What I verified:

    • OpenCodeAdapterV2 does not implement getModelContextWindow and never sets tokenUsage on provider turns. In my state DB, 0 of 167 OpenCode provider turns have tokenUsage.
    • So handoffBudget has no window and no measured usage. It falls back to the 128,000 default and uses nativeContextEstimate, which sums history bytes. Above about 96 KB of history the budget is 0.
    • A throwaway unit test against main with the affected thread's numbers (no window, no usage, 256,495-byte estimate) gives budget 0, and deliverContextHandoffs fails with ContextHandoffBudgetError. With modelContextWindow: 1_000_000 the same input gives 16,000.
    • On the affected thread, every send after the first failure died in deliverContextHandoffs right after prepareProviderHandoff. Switching model, /compact and 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 a cancelled run, 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.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions