Skip to content

[Bug][claude-surface] message_start reports usage.input_tokens: 0 — third-party clients reading the first frame under-report context usage (Paseo) #4857

Description

@ygc3817922006-sketch

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

  1. 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).
  2. 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).

  1. 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

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

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

    bugSomething isn't workingstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions