WordPress / WordPress/php-ai-client

Surface `prompt_tokens_details.cached_tokens` (and parallel cache fields) in `TokenUsage`

Open
#236 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
308
Forks
84
Avg merge
7d 21h
Merged PRs (30d)
2

Description

Summary

OpenAI returns usage.prompt_tokens_details.cached_tokens on every text-generation response for cache-eligible models (gpt-4o family, gpt-5 family, etc.), but AbstractOpenAiCompatibleTextGenerationModel::parseGenerationResponse only reads the three top-level fields and discards the rest of the usage object before any downstream code can see it.

This makes OpenAI prompt caching unobservable from any caller — and OpenAI exposes cache hit rate only through this field. Caching is a major cost lever (50–75% input discount on hit), and without this number downstream applications cannot measure whether their prompt structure is actually cache-friendly, nor whether changes to that structure improved or regressed cache hit rate.

The same gap exists for Anthropic when an Anthropic provider implementation lands: Anthropic returns usage.cache_creation_input_tokens and usage.cache_read_input_tokens in the same response shape, and the current base class would discard those too.

Where the data is dropped

src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php (around lines 604–618):

if (isset($responseData['usage']) && is_array($responseData['usage'])) {
    $usage = $responseData['usage'];

    $tokenUsage = new TokenUsage(
        $usage['prompt_tokens'] ?? 0,
        $usage['completion_tokens'] ?? 0,
        $usage['total_tokens'] ?? 0
    );
} else {
    $tokenUsage = new TokenUsage(0, 0, 0);
}

// Use any other data from the response as provider-specific response metadata.
$additionalData = $responseData;
unset($additionalData['id'], $additionalData['choices'], $additionalData['usage']);

The prompt_tokens_details.cached_tokens value is present on the response, never read into TokenUsage, and then explicitly stripped from $additionalData so callers cannot recover it through the metadata passthrough either.

Symptom

Callers measuring per-request usage see promptTokens: N, no indication of which portion of N was served from cache. Multi-turn / repeated-prefix workloads are especially affected because that is exactly the case where caching helps most and the metric is most useful.

Suggested shape

Add nullable cache fields to TokenUsage, parsed when the provider supplies them, left null otherwise:

  • ?int $cachedTokens — for OpenAI's prompt_tokens_details.cached_tokens and Anthropic's cache_read_input_tokens.
  • ?int $cacheCreationTokens — for Anthropic's cache_creation_input_tokens. (No OpenAI analog today, since OpenAI's prompt caching is automatic and not write-tracked, but the field is useful for clean Anthropic surfacing and for any future provider that distinguishes cache-create vs cache-read.)

This follows the same precedent established by commit 305b80d ("Add support for thought tokens in token usage data") — both are provider-optional usage fields that benefit from being first-class on the DTO rather than buried in additionalData.

Relationship to #158

#158 (Improve handling of unprovided usage statistics) proposes making the existing TokenUsage properties nullable and the TokenUsage instance itself nullable on GenerativeAiResult. That refactor and this addition are aligned — they both push toward a more flexible TokenUsage shape — but they are independent enough to land in either order:

  • If #158 lands first, the new cache fields can be added as ?int from the start with no migration noise.
  • If this lands first, the new fields are ?int (the existing ones are int with 0 sentinels), and #158 then makes the existing fields catch up.

I'm happy to send a PR either way once a maintainer indicates a preference on which lands first and on the field naming.

Ecosystem signal

This is a widely-recognized gap in OpenAI-compatible clients right now; recent issues filed against other libraries with the same shape of bug include NousResearch/hermes-agent#25400, braintrustdata/braintrust-sdk-dotnet#53, PostHog/posthog-python#574, and BerriAI/litellm#22984. Including the field puts php-ai-client on the right side of the pattern as more callers start measuring cache hit rate as a first-class metric.

AI assistance

  • AI assistance: Yes
  • Tool(s): opencode (Claude Opus 4.7)
  • Used for: Drafted the issue body after Chris asked me to diagnose why a downstream Data Machine workload could not observe prompt-cache hit rate. I located the parsing gap, verified the discarded field, checked for existing issues/PRs in this repo and the wider ecosystem, and drafted this issue for Chris to review. Chris is the submitter and is responsible for the report; AI assistance is descriptive, not a disclaimer.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php around lines 604–618 and inspect the TokenUsage DTO, using commit 305b80d as precedent. Trace how usage is parsed and verify the new nullable cachedTokens and cacheCreationTokens fields are populated for provider-supplied values while remaining null otherwise; confirm existing usage behavior still works.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
api, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.