AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

OpenAI-Compatible Responses Discard Nested Token Usage Details In UsageMetadata

langchain-openai's `_create_usage_metadata` only extracts known keys (`audio`, `cached_tokens`, `reasoning_tokens`) from `prompt_tokens_details` and `completion_tokens_details`, causing provider-specific fields like `text_tokens`, `accepted_prediction_tokens`, and `rejected_prediction_tokens` to be silently lost.

mediumConfidence 92%LangChainAffected V1.1.12

Origin Analysis

In `langchain_openai/chat_models/base.py`, `_create_usage_metadata` explicitly builds `input_token_details` and `output_token_details` by selecting only a hardcoded subset of nested token detail fields. Unknown keys returned by OpenAI-compatible providers are discarded, violating the documented extensibility of `UsageMetadata`.
1. Set up a ChatOpenAI client with base_url pointing to an OpenAI-compatible endpoint (e.g., DashScope) and stream=True. 2. Send a chat request with stream_options={'include_usage': True}. 3. Inspect the final AIMessageChunk.usage_metadata; observe that `input_token_details` only contains `cache_read` and `output_token_details` only `reasoning`, while raw usage contained `text_tokens`, `accepted_prediction_tokens`, etc.

Fixing Code Block

def _create_usage_metadata(oai_token_usage: dict, service_tier: str | None = None) -> UsageMetadata: input_tokens = oai_token_usage.get('prompt_tokens', 0) output_tokens = oai_token_usage.get('completion_tokens', 0) total_tokens = oai_token_usage.get('total_tokens', input_tokens + output_tokens) service_tier_prefix = f'{service_tier}:' if service_tier else '' prompt_details = oai_token_usage.get('prompt_tokens_details') or {} completion_details = oai_token_usage.get('completion_tokens_details') or {} input_token_details: dict = {} output_token_details: dict = {} # Preserve standard aliases with service tier prefix if prompt_details.get('audio_tokens') is not None: input_token_details['audio'] = prompt_details['audio_tokens'] if prompt_details.get('cached_tokens') is not None: input_token_details[f'{service_tier_prefix}cache_read'] = prompt_details['cached_tokens'] if completion_details.get('audio_tokens') is not None: output_token_details['audio'] = completion_details['audio_tokens'] if completion_details.get('reasoning_tokens') is not None: output_token_details[f'{service_tier_prefix}reasoning'] = completion_details['reasoning_tokens'] # Preserve all other provider-specific non-null fields for key, value in prompt_details.items(): if key not in {'audio_tokens', 'cached_tokens'} and value is not None: input_token_details[key] = value for key, value in completion_details.items(): if key not in {'audio_tokens', 'reasoning_tokens'} and value is not None: output_token_details[key] = value return UsageMetadata( input_tokens=input_tokens, output_tokens=output_tokens, total_tokens=total_tokens, input_token_details=input_token_details, output_token_details=output_token_details, )
The updated function preserves the existing standard aliases (audio, cache_read, reasoning) but iterates over any remaining keys in the nested details dictionaries and includes them verbatim when non-null. This ensures provider-specific fields such as `text_tokens`, `accepted_prediction_tokens`, and `rejected_prediction_tokens` are retained in `UsageMetadata` without breaking current behavior.

Edge Case Audit

Modifying `_create_usage_metadata` affects all OpenAI-compatible responses. Providers may send arbitrary or volatile fields; storing them directly could increase metadata size and potentially introduce keys that conflict with future LangChain standard fields. Before deploying, verify that `UsageMetadata` serialization downstream (e.g., to JSON or tracing) tolerates unknown keys. If a rollback is necessary, restore the original method or pin `langchain-openai==1.1.12` until an official release includes this change.

Ecosystem Topology