Skip to content

fix(ai): report OpenAI Agents cached tokens - #1102

Merged
marandaneto merged 3 commits into
PostHog:mainfrom
mosafariuk:fix/openai-agents-cache-reasoning-tokens
Oct 29, 2026
Merged

marandaneto merged 3 commits into
PostHog:mainfrom
mosafariuk:fix/openai-agents-cache-reasoning-tokens

Conversation

@mosafariuk

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

PostHogTracingProcessor never reports cached input tokens or reasoning tokens for real OpenAI Agents runs, on either model path:

  • Generation spans (OpenAIChatCompletionsModel): the Agents SDK builds span_data.usage with the counts nested under input_tokens_details.cached_tokens and output_tokens_details.reasoning_tokens (see agents.usage.model_usage_to_span_usage and openai_chatcompletions.py). The processor reads flat usage["cache_read_input_tokens"] and usage["reasoning_tokens"], which the SDK never sets.
  • Response spans (OpenAIResponsesModel, the default): the processor reads only input_tokens and output_tokens from response.usage, and ignores its input_tokens_details and output_tokens_details.

So $ai_cache_read_input_tokens and $ai_reasoning_tokens are always missing. Cached input is then costed at the full input rate, and reasoning usage is invisible.

This PR reads the nested fields on both paths, and keeps the flat keys for custom models that report them that way. The values are mapped the same way as the Python OpenAI wrapper's extract_openai_usage_from_response: cached_tokens → $ai_cache_read_input_tokens and reasoning_tokens → $ai_reasoning_tokens, each only when greater than zero. It's the Python counterpart of the cache-read part of PostHog/posthog-js#5056. Cache writes are left out, because the Python OpenAI wrapper doesn't report them either.

💚 How did you test it?

  • End to end with the real Agents SDK (openai-agents 0.18.3): a script runs Runner.run with OpenAIChatCompletionsModel and with OpenAIResponsesModel, against an httpx.MockTransport returning usage with 100 input, 60 cached, 50 output and 40 reasoning tokens, and the PostHog processor registered. On main, both runs capture $ai_cache_read_input_tokens: null and $ai_reasoning_tokens: null. With this change, both capture 60 and 40.
  • Regression tests in posthog/test/ai/openai_agents/test_processor.py:
    • test_generation_span_reads_nested_cache_and_reasoning_tokens builds the span usage with the SDK's own model_usage_to_span_usage(Usage(...)).
    • test_response_span_reads_cache_and_reasoning_tokens uses a real openai.types.responses.Response.
    • Both fail on main with KeyError: '$ai_cache_read_input_tokens' and pass with this change. The existing flat-key test (test_generation_span_with_reasoning_tokens) still passes.
  • Full suite: 5133 passed, 16 skipped. ruff format --check, ruff check, mypy | mypy-baseline filter and make public_api_check are clean.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • An AI coding agent helped me find this by comparing recent @posthog/ai fixes with the Python SDK and reading the Agents SDK's span-usage serialization. It drafted the fix and tests. I reviewed and ran all of it.
  • The existing tests passed because they used a flat usage dict, which isn't the shape the SDK produces. The new tests build usage with the SDK's own types and serializer.

The Agents SDK nests cached input and reasoning tokens under the usage
details fields: input_tokens_details/output_tokens_details on generation
span usage and on the Responses usage object of response spans. The
processor read flat keys that the SDK never sets, so both were always
dropped. Read the nested fields, keeping the flat keys for custom models.
@mosafariuk
mosafariuk requested a review from a team as a code owner October 22, 2026 10:21

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nested token extraction is verified on both Agents model paths. One cache-accounting issue remains for OpenAI-compatible Claude requests: the new cache count increases estimated input cost without an inclusive-reporting flag. See the inline reproduction.

AI-assisted review.

Comment thread posthog/ai/openai_agents/processor.py
@dustinbyrne
dustinbyrne requested a review from a team October 22, 2026 17:58
@dustinbyrne

Copy link
Copy Markdown
Contributor

and thank you for the pull request @mosafariuk! 🙌

OpenAI-shaped usage counts cached tokens inside input_tokens. Declare
$ai_cache_reporting_exclusive: False when the cache count comes from the
nested usage details, as the Node Agents processor does, so the cost backend
does not infer exclusive reporting from a Claude model name served through an
OpenAI-compatible endpoint. Flat custom-model keys keep their own convention,
and a caller-supplied value still takes precedence.
@mosafariuk

Copy link
Copy Markdown
Contributor Author

@dustinbyrne the inclusive-reporting fix is in 4de76ae, together with your regression test. Ready for another look when you have a moment.

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The inclusive cache-accounting correction is verified, including the compatible-Claude case. One remaining interaction affects Gemini through OpenRouter: reasoning is already included in the completion total, but forwarding it separately causes the backend to count it again. See the inline base/head reproduction.

AI-assisted review.

Comment thread posthog/ai/openai_agents/processor.py Outdated
@dustinbyrne
dustinbyrne requested a review from a team October 28, 2026 13:32
OpenAI-compatible providers such as OpenRouter count reasoning inside
completion_tokens, while the cost backend adds reasoning on top for Gemini
model names, so forwarding the nested count priced reasoning twice. Keep
the original flat reasoning_tokens key only; cached tokens are unchanged.
@mosafariuk mosafariuk changed the title fix(ai): report OpenAI Agents cached and reasoning tokens fix(ai): report OpenAI Agents cached tokens Oct 28, 2026

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@marandaneto
marandaneto merged commit 4d6e447 into PostHog:main Oct 29, 2026
42 of 43 checks passed
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.

3 participants