Skip to content

feat(usage): normalize variant cache-token field names across upstreams - #109

Merged
Menci merged 1 commit into
mainfrom
floway/cache-token-variants
Jun 25, 2026
Merged

feat(usage): normalize variant cache-token field names across upstreams#109
Menci merged 1 commit into
mainfrom
floway/cache-token-variants

Conversation

@Menci

@Menci Menci commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

The OpenAI-shape usage.prompt_tokens_details.cached_tokens convention is dominant but not universal among the "wild" OpenAI-compatible upstreams Floway routes to. Several providers emit cache-hit counts under different field names, and our extractors silently miscounted those rows as zero cache activity (the entire prompt landed on the bare input dimension).

A short cross-provider survey:

Cache-read shape Upstreams emitting it Covered before?
usage.prompt_tokens_details.cached_tokens (OpenAI canonical) OpenAI, Gemini OpenAI-compat, xAI, Mistral, OpenRouter, Groq, Cerebras, Zhipu, Doubao, Qwen main, llama.cpp, vLLM, SGLang
usage.prompt_cache_hit_tokens + usage.prompt_cache_miss_tokens (root level, prompt_tokens is the sum) DeepSeek
usage.cached_tokens (flat, no wrapper) Moonshot / Kimi, Cohere v2 native, Qwen Singapore legacy
HTTP header fireworks-cached-prompt-tokens + perf_metrics["cached-prompt-tokens"] (outside usage) Fireworks AI irreducible at this layer
Not reported Anthropic OpenAI-compat, Together, Perplexity, SiliconFlow, TGI, Ollama-compat n/a

A parallel divergence for cache-write under the OpenAI wrapper:

Cache-write shape Source Covered before?
prompt_tokens_details.cache_creation_input_tokens the gateway's Anthropic messages → chat-completions translation pair (Anthropic-native name forwarded under OpenAI's wrapper)
prompt_tokens_details.cache_write_tokens OpenRouter (Anthropic / explicit-Gemini / Alibaba-routed)

Change

  • New openAICacheTokensFromUsage(usage: unknown): { cacheRead, cacheWrite } helper in shared/telemetry/usage.ts. It sniffs the variants in explicit precedence order:
    • cache-read: prompt_tokens_details.cached_tokensprompt_cache_hit_tokens → flat cached_tokens
    • cache-write: prompt_tokens_details.cache_creation_input_tokensprompt_tokens_details.cache_write_tokens
  • Both per-API extractors (tokenUsageFromChatCompletionsUsage, tokenUsageFromCompletionsUsage) now call the helper instead of reading the fields directly. The chat extractor keeps its strict ChatCompletionsUsage parameter type — the variant sniff lives at the extraction boundary and the protocol type stays clean of non-standard fields.
  • Completions also gains cache-write subtraction so its three input dimensions stay disjoint when an upstream that emits cache-write happens to answer there (no-op when cache-write is 0).
  • Tests cover each variant shape on the helper and on both per-API extractors, plus the precedence behavior when multiple candidates are present, the null prompt_tokens_details case (Gemini cache miss), and that a real cached_tokens: 0 is honored rather than falling through to the next candidate.

Out of scope

  • Fireworks: HTTP-header + perf_metrics sibling, completely outside the usage block. Capturing it needs provider-specific extraction at the transport layer, deferred to a separate change.
  • vLLM streaming /v1/completions bug (vllm-project/vllm#44961, fixed by #44383 on main): pure upstream-side, no gateway code change can recover the omitted field. The Zhipu/GLM fork we observed predates that fix.

Test plan

  • pnpm run typecheck — clean
  • pnpm run lint — clean
  • pnpm run test — 3364 / 3364 pass (was 3295 on main; +16 new test cases across shared/telemetry/usage_test.ts, completions/usage_test.ts, chat-completions/usage_test.ts)
  • Manual end-to-end against a DeepSeek upstream once configured, to confirm a cache-hit request lands input_cache_read > 0 in the usage row

The OpenAI-shape usage block convention `prompt_tokens_details.cached_tokens`
is dominant but not universal. Several "wild" OpenAI-compatible upstreams
report cache hits under different field names — chief among them DeepSeek,
which puts `prompt_cache_hit_tokens` + `prompt_cache_miss_tokens` at the
usage root (and reports `prompt_tokens` as the sum), and Moonshot / Kimi,
Cohere v2 native, and Qwen Singapore legacy region, which all use a flat
top-level `cached_tokens` with no wrapper. Cache-write reads a parallel
divergence: our Anthropic messages → chat-completions translation pair
emits `cache_creation_input_tokens` under the wrapper, while OpenRouter
emits `cache_write_tokens` instead when routing to Anthropic / explicit-
Gemini / Alibaba.

Until now the chat-completions and completions extractors each read only
`prompt_tokens_details.cached_tokens` (and chat additionally
`cache_creation_input_tokens`), so DeepSeek's cache activity was silently
counted as bare input on the largest open-source-friendly cached upstream.

`openAICacheTokensFromUsage` in shared/telemetry/usage.ts now resolves
both cache-read and cache-write across the variant shapes in a single
helper, with explicit precedence (OpenAI canonical → DeepSeek root →
flat root; Anthropic-style cache_creation → OpenRouter cache_write).
Both per-API extractors call it, replacing their direct field reads;
the chat-completions extractor keeps its strict ChatCompletionsUsage
parameter type, since the helper accepts `unknown` and narrows
internally — the protocol type stays clean of non-standard fields and
the variant sniff lives only at the extraction boundary.

Fireworks remains an irreducible blind spot: it reports cache stats in
HTTP response headers and a kebab-case `perf_metrics` sibling, not in
`usage` at all. Provider-specific extraction would be needed; deferred.
@Menci
Menci merged commit 981c619 into main Jun 25, 2026
@Menci
Menci deleted the floway/cache-token-variants branch July 27, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant