Client or integration
Claude Code
Area
Streaming
Summary
The Anthropic Messages stream emitted by opencodex reports usage: { input_tokens: 0, output_tokens: 0 } in the message_start frame. The real input size (including cache read/creation) only arrives in the terminal message_delta.usage.
Real Anthropic populates message_start.message.usage.input_tokens with the actual prompt size at the start of the turn. Clients that follow the documented contract and read usage from the first frame — e.g. Paseo's context-window meter (packages/server/src/server/agent/providers/claude/agent.ts in getpaseo/paseo: readStreamRequestInputTokens reads message_start, readStreamRequestOutputTokens reads message_delta, and the sum drives contextWindowUsedTokens) — therefore see only the output tokens of the previous turn and show a nearly-empty context ring (e.g. "a few hundred tokens used" of 1M) while /context in Claude Code correctly shows ~97k.
Claude Code itself does not rely on the first frame, so compaction and cost accounting are unaffected — this is a display-contract gap for third-party clients only.
Reproduction
- Start opencodex (
ocx start) with any provider behind the Claude surface (reproduced with an openai-chat adapter provider; version 2.56.0, macOS, bun runtime).
- Send a small streaming request directly through the proxy:
curl -sN http://127.0.0.1:10100/v1/messages \
-H "Authorization: Bearer <token>" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"<provider>/<model>","max_tokens":16,"stream":true,"messages":[{"role":"user","content":"reply with one word"}]}'
Observed:
event: message_start
data: {"type":"message_start","message":{...,"usage":{"input_tokens":0,"output_tokens":0}}}
...
event: message_delta
data: {"type":"message_delta","delta":{...},"usage":{"input_tokens":17,"output_tokens":147,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}
Expected (real Anthropic): message_start.message.usage.input_tokens already carries the full prompt size for this turn (and cache_read_input_tokens / cache_creation_input_tokens when caching applies).
- In a real session,
~/.opencodex/usage.jsonl confirms the terminal usage is correct — latest turn recorded inputTokens: 87567, cachedInputTokens: 57920 — while Paseo's meter showed only a few hundred tokens for the same session. So the data exists; only the first frame is zero.
Root cause (2.56.0 source)
src/claude/outbound.ts:
messageSnapshot(model) (L192) hardcodes usage: { input_tokens: 0, output_tokens: 0 }.
responsesSseToAnthropicSse emits message_start with that snapshot (L283) as soon as the stream starts, long before upstream usage is known.
- The real usage only reaches the client in
finish() via anthropicUsage() on message_delta (L355).
I understand the upstream Responses stream may not provide prompt usage until the end, so a faithful fix may not always be possible at message_start time. But two intermediate options would already unblock first-frame readers:
- If the adapter knows the prompt size early (e.g. from an estimate or an early upstream usage frame), emit it in
message_start.
- Alternatively, emit a synthetic
message_delta with a usage update as soon as input usage becomes known, since Paseo-style readers sum message_start input + latest message_delta output.
Related prior art: #508 (Kiro under-reporting fixed in v2.7.41 via contextTotalTokens) — same user-visible symptom class, different code path.
Version
2.56.0
Operating system
macOS 15.5 (Darwin 25.5.0)
Provider and model
tianyi / glm-5.3-oc (openai-chat adapter, surface: claude)
Logs or error output
event: message_start
data: {"type":"message_start","message":{"id":"msg_7629b195d7e044088b9fbda77ff5e9c4","type":"message","role":"assistant","content":[],"model":"tianyi/glm-5.3-oc","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":17,"output_tokens":147,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}
Checks
Client or integration
Claude Code
Area
Streaming
Summary
The Anthropic Messages stream emitted by opencodex reports
usage: { input_tokens: 0, output_tokens: 0 }in themessage_startframe. The real input size (including cache read/creation) only arrives in the terminalmessage_delta.usage.Real Anthropic populates
message_start.message.usage.input_tokenswith the actual prompt size at the start of the turn. Clients that follow the documented contract and read usage from the first frame — e.g. Paseo's context-window meter (packages/server/src/server/agent/providers/claude/agent.tsin getpaseo/paseo:readStreamRequestInputTokensreadsmessage_start,readStreamRequestOutputTokensreadsmessage_delta, and the sum drivescontextWindowUsedTokens) — therefore see only the output tokens of the previous turn and show a nearly-empty context ring (e.g. "a few hundred tokens used" of 1M) while/contextin Claude Code correctly shows ~97k.Claude Code itself does not rely on the first frame, so compaction and cost accounting are unaffected — this is a display-contract gap for third-party clients only.
Reproduction
ocx start) with any provider behind the Claude surface (reproduced with anopenai-chatadapter provider; version 2.56.0, macOS, bun runtime).Observed:
Expected (real Anthropic):
message_start.message.usage.input_tokensalready carries the full prompt size for this turn (andcache_read_input_tokens/cache_creation_input_tokenswhen caching applies).~/.opencodex/usage.jsonlconfirms the terminal usage is correct — latest turn recordedinputTokens: 87567, cachedInputTokens: 57920— while Paseo's meter showed only a few hundred tokens for the same session. So the data exists; only the first frame is zero.Root cause (2.56.0 source)
src/claude/outbound.ts:messageSnapshot(model)(L192) hardcodesusage: { input_tokens: 0, output_tokens: 0 }.responsesSseToAnthropicSseemitsmessage_startwith that snapshot (L283) as soon as the stream starts, long before upstream usage is known.finish()viaanthropicUsage()onmessage_delta(L355).I understand the upstream Responses stream may not provide prompt usage until the end, so a faithful fix may not always be possible at
message_starttime. But two intermediate options would already unblock first-frame readers:message_start.message_deltawith a usage update as soon as input usage becomes known, since Paseo-style readers summessage_startinput + latestmessage_deltaoutput.Related prior art: #508 (Kiro under-reporting fixed in v2.7.41 via
contextTotalTokens) — same user-visible symptom class, different code path.Version
2.56.0
Operating system
macOS 15.5 (Darwin 25.5.0)
Provider and model
tianyi / glm-5.3-oc (openai-chat adapter, surface: claude)
Logs or error output
Checks