# PR #22761 evidence matrix (pass 1, read-only), head 0f40a81e0c, base 8acbefefc5

Scope: the change is only in the **legacy OpenCode 1.x adapter** (`packages/provider-opencode/src/server/adapter.ts`).
OpenCode 2 (`server/v2/adapter.ts:1613-1632`) already emitted `tokenUsage` from the last step at base.
`driver.ts:112 selectOpenCodeRuntimeAdapter` picks the adapter by probed CLI version. The local `opencode` is 1.18.18, so it goes to v1, the path this PR changes.

## How the code computes it (after)
- Only steps the turn owns count (`step-finish` whose assistant `parentID` is this turn's prompt). Dedupe uses part id.
- turnTokenUsage (billing, unchanged): sums over owned steps. input = in+read+write, cached = read, creation = write, output = out+reasoning, reasoning = reasoning. Status is `complete` only when the turn completed with nothing unresolved. Zero owned steps gives `{usageStatus:"unavailable"}`.
- tokenUsage (new): built from `lastStep`, the newest step that was *accumulated*. Emitted only on the terminal emit (completedAt != null). The running emits carry none, so the meter updates at turn end, not live. Fields: usedTokens = in+read+write+out (**reasoning excluded**), inputTokens = in+read+write, cachedInputTokens = read, outputTokens = out, reasoningOutputTokens = reasoning, maxTokens, updatedAt = completedAt. It is also emitted for interrupted/failed turns that have at least one owned step.
- **Not gated on OpenRouter.** It applies to every model on the v1 adapter.
- maxTokens: one `provider.list` per provider session, forked in the background with a 5 s timeout and errors ignored. The map is keyed `${provider.id}/${model.id}` → `model.limit.context` and looked up with `turn.modelSelection.model.trim()`. For OpenRouter, model.id contains `/`, so the key is `openrouter/z-ai/glm-5.3-flash`, which matches the slug.
  - Catalog still pending at turn end: maxTokens = null. Later turns in the same session get the value.
  - Catalog failed or timed out: null for the whole session, with no retry.
  - Slug not in the catalog: null.
  - The map is never refreshed during a session.
  - The lookup uses the thread's selected model, not the step's actual providerID/modelID.
- Client: the adapter's `provider_turn.updated` becomes the orchestration event `provider-turn.updated`. That event goes to `orchestration_v2_events` and the `orchestration_v2_projection_provider_turns.payload_json` projection (ProjectionStore.ts:690 keeps the prior tokenUsage when the next one is absent), then over WS into `packages/client-runtime/src/state/orchestrationV2Projection.ts:296` (`providerTurns[]`, same carry-forward). The web flow is `apps/web/src/lib/contextWindow.ts latestProviderTurnTokenUsage` (newest `updatedAt` across turns) → `deriveLatestContextWindowSnapshot` → `ContextWindowMeter`. With maxTokens null, the meter shows tokens without a %. The meter sits behind Settings > General > Legacy features > Context window indicator. Mobile has no consumer. IdleCompaction is gated to `claudeAgent`, so it is unaffected.

## Rows
| Row | Before (base) | After (head) | Observe | Test |
|---|---|---|---|---|
| R1 OpenRouter, 1 step (fixture as-is: prompt 12000/cached 1000/completion 32 → in 11000, read 1000, out 32) | tokenUsage **absent**. turnTokenUsage `{complete, input 12000, cached 1000, creation 0, output 32, reasoning 0, hasSubagents false}`. Meter hidden. | tokenUsage `{usedTokens 12032, maxTokens 128000 (fixture config limit), inputTokens 12000, cachedInputTokens 1000, outputTokens 32, reasoningOutputTokens 0}`. turnTokenUsage identical to before. Meter 12k/128k ≈ 9.4%. | provider_turns row with status=completed: `$.tokenUsage`, `$.turnTokenUsage`. Same payload on the `provider-turn.updated` event in orchestration_v2_events. Client providerTurns[].tokenUsage feeds the meter. | adapter.test.ts "reports an OpenRouter turn's last step ... ready" (tokenUsage only. It does not assert turnTokenUsage.) |
| R2 2 steps: s1 in 9000/read 500/out 40, s2 in 11000/read 1000/out 32 | tokenUsage absent. turnTokenUsage `{complete, input 21500, cached 1500, creation 0, output 72, reasoning 0}`. | tokenUsage = **s2 only**: `{used 12032, max 128000, input 12000, cached 1000, out 32, reasoning 0}`. turnTokenUsage unchanged `{input 21500, cached 1500, output 72}`. | Same as R1. | The new test pushes exactly these two steps but asserts only tokenUsage. Cumulative turnTokenUsage is covered by the existing "normalizes OpenCode step usage for %s turns" (34/6/8/14/4), which does not assert tokenUsage. **No single test asserts both together.** |
| R3a zero owned steps (no step-finish: e.g. 4xx before any step, interrupt before the first step) | tokenUsage absent. turnTokenUsage `{unavailable, main_agent, hasSubagents false}`. | Same. tokenUsage absent, no crash. | provider_turns row: tokenUsage NULL, turnTokenUsage unavailable. | "normalizes ... unavailable" asserts only turnTokenUsage. **tokenUsage absence is not asserted.** |
| R3b provider sends **no usage chunk** but the step completes | OpenCode still writes step-finish with zero tokens (to verify against 1.18.18). turnTokenUsage `{complete, all 0}`. tokenUsage absent. | **tokenUsage present** `{used 0, max 128000, input 0, cached 0, out 0, reasoning 0}`. Meter shows 0%. No crash. | Same. | none |
| R4 non-OpenRouter model on the v1 adapter (anthropic/…, opencode/…, custom) | tokenUsage absent. Billing as today. | **Changed:** billing unchanged, and tokenUsage is now present with maxTokens = catalog limit.context, or null if unknown. The meter now appears for every OpenCode 1.x thread. OpenCode 2 threads behave the same before and after. | Same. | The existing tests run on `anthropic/claude-sonnet` with no `provider.list` in the stub client, so they hit the catalog-error path. None asserts tokenUsage for a non-OpenRouter model. |
| Catalog pending/failed | n/a | maxTokens null, and the turn is not blocked. | Same. | The new test's "pending" variant covers pending. Failure/timeout is not covered (the old tests implicitly exercise a missing `provider.list`). |

## Wrong / overstated expectations in the brief
- R3 "tokenUsage absent": true only when there are zero step-finish parts. When the provider simply omits usage, OpenCode still emits a step-finish (zeros), so after the PR tokenUsage is **present with 0s**.
- R4 "unchanged": false. Nothing is gated on OpenRouter. Billing is unchanged, but the context meter now appears for all OpenCode 1.x models. The PR title understates the scope.

## Minor code notes (not blockers)
- `lastStep` follows accumulation order. Steps buffered as `unresolvedStepsByMessageId` are accumulated when their message.updated arrives, which can happen after a later step, so the meter could show an earlier step. This is rare.
- usedTokens excludes reasoning. That matches v2.

## Capture recipe (pass 2)
Run `env | grep ^T3CODE_ | cut -d= -f1 | xargs -r -n1 unset` (or `env -u` each one) before every command below. The OpenCode CLI on this machine is 1.18.18, which uses the v1 adapter.
1. `cp /home/ubuntu/wt/pr-22761/apps/server/scripts/opencode-openrouter-usage-fixture.ts /tmp/ev-22761/fixture.ts`, then add a `STUB_MODE` env var to the copy:
   - `single`: as shipped (R1).
   - `multi`: when the request has no `role:"tool"` message, stream a `tool_calls` delta (e.g. a `glob` call with `{"pattern":"*"}`) with `finish_reason:"tool_calls"` and usage `prompt 9500 / cached 500 / completion 40`. Otherwise reply with text and usage `12000/1000/32`. OpenCode then emits 2 step-finish parts, expected 21500/1500/72 billing and 12032 context (R2).
   - `nousage`: drop the trailing `{choices:[],usage}` chunk and `usage` from the non-stream reply (R3b).
   - `err`: respond `500` to every request (R3a, zero steps).
   - For R4, add a second provider to the generated config, e.g. `"anthropic": {options:{baseURL: stub/v1, apiKey}, models:{"stub-a":{limit:{context:64000}}}}`. Anthropic's wire format differs, so it is simpler to add an OpenAI-compatible custom provider id `acme` with `npm:"@ai-sdk/openai-compatible"` → model `acme/stub-a` (non-OpenRouter). Expected tokenUsage is present with max 64000.
2. `node /tmp/ev-22761/fixture.ts prepare /tmp/ev-22761/oc --port 18988`. Then `STUB_MODE=... node /tmp/ev-22761/fixture.ts serve --port 18988 &` and record `$!`. Restart it (killing only that PID) to switch modes.
3. Sanity check without T3: in /tmp/ev-22761/oc/project run `/tmp/ev-22761/oc/opencode run --model openrouter/z-ai/glm-5.3-flash --format json hi` and grep `step-finish`. This confirms the step count and the R3b zero-token claim.
4. Real server, before/after: two worktrees (base 8acbefefc5 via `git worktree add /tmp/ev-22761/base 8acbefefc5`, and the PR worktree). Each needs its own `.t3` (`vp run migrate-dev-db` is optional; an empty DB is acceptable for this row set). `vp run dev &` and record `$!`. Configure the OpenCode binary = /tmp/ev-22761/oc/opencode, enable Context window indicator, and send one message per mode. Optionally drive it through the UI for screenshots of the meter (needs user permission for browser use).
5. Extract per thread: `node apps/server/scripts/t3-sqlite-state.ts query --base-dir .t3 --sql "select status, json_extract(payload_json,'$.tokenUsage'), json_extract(payload_json,'$.turnTokenUsage') from orchestration_v2_projection_provider_turns where thread_id='<id>' order by ordinal"`. The same query against `orchestration_v2_events where event_type='provider-turn.updated'` shows exactly what was broadcast to clients. Only synthetic data is involved.
6. Unit proof: `vp test run packages/provider-opencode/src/server/adapter.test.ts` at head. At base, the new test file is absent, so you can only run the old tests.
