Created
September 23, 2026 16:50
-
-
Save tonydzi/1992ce6cf95340ea000e67cdb33a4600 to your computer and use it in GitHub Desktop.
pydantic-ai #8551: where the 'no usage object' signal dies (models/openai.py L5171-5173), and whether RequestUsage.details can carry it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Where does pydantic-ai lose the "provider sent no usage object" signal? | |
| Context: pydantic/pydantic-ai#8551. Measured on pydantic-ai-slim 2.48.0, | |
| openai 3.19.0, opentelemetry-sdk 1.44.0, Python 3.12, macOS. | |
| Part 1 hits the real production mapper (pydantic_ai.models.openai._map_usage) | |
| with three responses; C is the positive control and must differ from A. | |
| Part 2 asks whether RequestUsage.details -- a field that already exists -- | |
| survives accumulation into RunUsage. | |
| Part 3 asks whether it reaches the OpenTelemetry span attributes unaided. | |
| The one-line change, at models/openai.py L5173: | |
| - return usage.RequestUsage() | |
| + return usage.RequestUsage(details={'usage_missing_requests': 1}) | |
| Observed with that line applied to the installed package, then reverted: | |
| stock 2.48.0 A == B -> True (unknown indistinguishable from free) | |
| patched A == B -> False (control A == C still False) | |
| reverted A == B -> True | |
| """ | |
| import dataclasses, json | |
| from openai.types import chat, CompletionUsage | |
| from pydantic_ai.models.openai import _map_usage | |
| from pydantic_ai import usage as u | |
| def completion(usage_obj): | |
| return chat.ChatCompletion( | |
| id='x', model='m', object='chat.completion', created=0, | |
| choices=[chat.chat_completion.Choice( | |
| finish_reason='stop', index=0, | |
| message=chat.ChatCompletionMessage(role='assistant', content='hi'))], | |
| usage=usage_obj) | |
| cases = { | |
| 'A provider sent no usage object': None, | |
| 'B provider sent a genuine zero ': CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0), | |
| 'C control: real tokens (must differ)': CompletionUsage(prompt_tokens=12, completion_tokens=8, total_tokens=20), | |
| } | |
| print('=== part 1: recording edge, pydantic_ai.models.openai._map_usage ===') | |
| out = {} | |
| for name, uo in cases.items(): | |
| ru = _map_usage(completion(uo), provider='openai', provider_url='https://api.openai.com/v1', model='m') | |
| out[name] = ru | |
| print(f'{name:38} in={ru.input_tokens:3} out={ru.output_tokens:3} details={ru.details} repr_eq_to_A={ru == out["A provider sent no usage object"]}') | |
| A, B, C = out.values() | |
| print() | |
| print('A == B (unknown indistinguishable from free):', A == B) | |
| print('A == C (control; must be False): ', A == C) | |
| print() | |
| print('=== part 2: does RequestUsage.details survive accumulation into RunUsage? ===') | |
| run = u.RunUsage() | |
| run.incr(u.RequestUsage(input_tokens=12, output_tokens=8)) # normal request | |
| run.incr(u.RequestUsage(details={'usage_missing_requests': 1})) # marked unknown | |
| run.incr(u.RequestUsage(details={'usage_missing_requests': 1})) # marked unknown | |
| print('run.requests =', run.requests) | |
| print('run.input_tokens =', run.input_tokens) | |
| print('run.details =', run.details) | |
| print('marker summed to 2:', run.details.get('usage_missing_requests') == 2) | |
| clean = u.RunUsage() | |
| clean.incr(u.RequestUsage(input_tokens=12, output_tokens=8)) | |
| print('control run (no marker) details:', clean.details, '-> absent:', 'usage_missing_requests' not in clean.details) | |
| print() | |
| print('=== part 3: does the marker reach the span attributes for free? ===') | |
| print('marked :', run.opentelemetry_attributes()) | |
| print('control :', clean.opentelemetry_attributes()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment