Repository navigation
Description
Summary
session.tokens_cache_read (and the per-prompt info.tokens.cache.read) is never populated for OpenAI-compatible providers, even when the upstream returns usage.prompt_tokens_details.cached_tokens. The cache hit is real and billed at the discount rate by the upstream, but opencode attributes 0 cache-read tokens, so cache savings are invisible in the cost/usage UI and in the SDK info.tokens.
Confirmed for a litellm proxy fronting DeepSeek. Native OpenAI, Anthropic, and others appear unaffected (see contrast below).
Environment
- opencode 1.17.11
- Provider configured via the OpenAI-compatible route (
npm: "@ai-sdk/openai", custombaseURL), e.g. a litellm proxy athttps://…/v1frontingdeepseek-v4-flash.
Repro
- Point opencode at an OpenAI-compatible endpoint that returns
prompt_tokens_details.cached_tokens(litellm → DeepSeek does). - Using the SDK, create a session and send two prompts so turn 2 reuses turn 1's prefix (DeepSeek auto-caches the shared prefix).
const s = await client.session.create();
const long = "stable-context-block. ".repeat(400); // >1024 tokens
await client.session.prompt({ path: { id: s.data.id }, body: { parts: [{ type: "text", text: long + " Reply: OK1" }] } });
const p2 = await client.session.prompt({ path: { id: s.data.id }, body: { parts: [{ type: "text", text: "Reply: OK2" }] } });
console.log(p2.data?.info?.tokens?.cache_read); // → undefined- Inspect the session row:
tokens_cache_read = 0.
Evidence the upstream IS caching and reporting it
Same endpoint, same prefix, direct curl — both non-streaming and streaming return the cache hit:
# streaming final chunk (what opencode actually consumes), stream_options.include_usage=true
data: {"usage":{"prompt_tokens":6004,"prompt_tokens_details":{"cached_tokens":5888}, …}}cached_tokens: 5888 of 6004 prompt tokens (~98%) is present in the SSE usage chunk.
What the code already does correctly
packages/llm/src/protocols/openai-chat.tsmapUsagemaps it:const cached = usage.prompt_tokens_details?.cached_tokens return new Usage({ …, cacheReadInputTokens: cached, … })
openai-compatible-chat.tsreusesOpenAIChat.protocolend-to-end (protocol: OpenAIChat.protocol), so it inherits that mapping.packages/core/src/session/runner/publish-llm-event.tsreadsusage?.cacheReadInputTokens→cache.read, andprojector.tswritestokens_cache_read.
So the mapping exists; yet for the OpenAI-compatible route the value lands as 0 in the session.
Contrast (works elsewhere)
For Anthropic / Zhipu (glm) / MiniMax the same field is populated correctly — e.g. a local opencode DB shows glm-5.1 with 1.07 billion tokens_cache_read across 397 sessions, and a live 2-turn probe there reports non-zero cache.read. Only the OpenAI-compatible path records 0 under the same conditions.
Suspected area
Something between openai-chat.ts mapUsage (which sets cacheReadInputTokens) and the session's tokens_cache_read drops the value specifically on the OpenAI-compatible route — possibly the streaming parser not retaining usage from the final SSE chunk before mapUsage runs, or the event-publication path. I wasn't able to pin the exact line from source alone, but the repro + evidence above localizes it.
Impact
Any OpenAI-compatible provider (litellm, openrouter, vLLM, Ollama's compat endpoint, LM Studio, etc.) that returns prompt_tokens_details.cached_tokens gets zero cache attribution in opencode. Token-cache savings from session resume / long conversations are silently unmeasured.
Happy to follow up with a tighter bisection or a PR if you can point me at where the OpenAI-compatible route's streaming usage is finalized.
🤖 Generated with Claude Code
Activity
github-actions commentedon Jun 26, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
If you believe this was flagged incorrectly, please let a maintainer know.