Skip to content

fix(pydantic_ai): capture Anthropic/Google reasoning tokens - #745

Merged
Luca Forstner (lforst) merged 2 commits into
mainfrom
fix/pydantic-ai-anthropic-reasoning-tokens
Sep 7, 2026
Merged

Luca Forstner (lforst) merged 2 commits into
mainfrom
fix/pydantic-ai-anthropic-reasoning-tokens

Conversation

@paultancre-bt

@paultancre-bt paultancre-bt commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

In the pydantic-ai integration, _extract_response_metrics reads reasoning tokens only from usage.details["reasoning_tokens"], the key pydantic-ai uses for OpenAI. But pydantic-ai stashes the same normalized quantity under a different key per provider:

Provider pydantic-ai details key
OpenAI reasoning_tokens
Anthropic thinking_tokens
Google thoughts_tokens

So completion_reasoning_tokens was silently empty for Anthropic and Google. Nothing failed, nothing warned the charts just look like reasoning was never used.

Reported by a customer: in their non-prod agent platform, 16,587 of 16,587 llm spans over 30 days had no reasoning metric, all provider = anthropic.

pydantic-ai's own source confirms the counts are equivalent, the Anthropic mapping comment (models/anthropic.py) says thinking tokens are "a readable subset of the output total ... matching reasoning_tokens on OpenAI and thoughts_tokens on Google."

Fix

Read whichever of the three normalized keys is present in details.

Verification

Reproduced end-to-end by driving pydantic-ai's real _map_usage from an Anthropic BetaMessage carrying output_tokens_details.thinking_tokens=180:

  • Before: completion_reasoning_tokens = None (OpenAI captured 180)
  • After: completion_reasoning_tokens = 180.0

Added a parametrized regression test covering all three provider keys.

Blast radius

  • Scoped to the reasoning-token read in _extract_response_metrics; no other metric or provider path changes.
  • The three keys are provider-unique in pydantic-ai (grepped 2.36.0 each appears in exactly one provider's mapping), so there's no key collision or ambiguity. OpenAI behavior is unchanged (reasoning_tokens still read first).

`_extract_response_metrics` only read `details["reasoning_tokens"]`, which is
the key pydantic-ai uses for OpenAI. Anthropic reasoning is stashed under
`thinking_tokens` and Google under `thoughts_tokens`, so `completion_reasoning_tokens`
was silently empty for those providers (reported for Anthropic in a customer trace
set: 16,587/16,587 llm spans with no reasoning metric, all provider=anthropic).

Read whichever of the three normalized keys is present. Verified end-to-end by
driving pydantic-ai's real `_map_usage` from an Anthropic response with thinking
tokens through the extractor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7846dfc38c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment on lines +2234 to +2235
usage = RequestUsage(input_tokens=10, output_tokens=20, details={details_key: 128})
response = SimpleNamespace(parts=[], usage=usage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cover provider token mapping with a cassette

In the checked test_pydantic_ai_integration matrix, this regression test manually inserts each expected key into RequestUsage.details, so it cannot catch the provider-specific failure that matters here: Anthropic or Google changing—or failing to populate—the key while the extractor test continues to pass. Exercise at least the Anthropic/Google mapping through a real provider response and checked-in cassette, keeping this synthetic test only as supplemental coverage.

AGENTS.md reference: AGENTS.md:L182-L184

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced the synthetic test with a real cassette to repro pre / post fix

Address review feedback: the synthetic parametrized test verifies the extractor
reads each provider key, but not that pydantic-ai actually populates
thinking_tokens from a real Anthropic response. Add a cassette-backed test that
runs a real Agent against Anthropic (extended thinking enabled) through pydantic-ai's
own _map_usage, asserting the leaf chat span surfaces completion_reasoning_tokens.
Keeps the synthetic test as supplemental coverage (per AGENTS.md VCR-first rule).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lforst
Luca Forstner (lforst) merged commit a16ed3f into main Sep 7, 2026
83 checks passed
@lforst
Luca Forstner (lforst) deleted the fix/pydantic-ai-anthropic-reasoning-tokens branch September 7, 2026 14:00
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.

2 participants