feat(ai): capture $ai_span for LLM tool calls in provider wrappers - #799
Draft
marco-g-pm wants to merge 1 commit into
Draft
feat(ai): capture $ai_span for LLM tool calls in provider wrappers#799marco-g-pm wants to merge 1 commit into
marco-g-pm wants to merge 1 commit into
Conversation
When a generation response contains tool calls, the OpenAI/Anthropic/Gemini wrappers now emit one child `$ai_span` (`$ai_span_type: "tool"`) per requested tool — carrying the tool name, arguments, and tool-call id — nested under the generation via `$ai_parent_id`. Generation events also now carry an `$ai_span_id` so the spans can parent to them. Covers the sync, async, and streaming capture paths (all converge on the shared helpers in `utils.py`). The wrapper never executes the tool, so these spans have no duration or result; span capture is best-effort and never breaks the LLM call. Updated the OpenAI/Anthropic/Gemini tool-call tests to assert the new span and its parent linkage. Generated-By: PostHog Code Task-Id: 150f5a08-87af-4dce-911e-770c06855fd1
Contributor
posthog-python Compliance ReportDate: 2026-07-29 17:43:49 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
Raised from a PostHog Slack thread while comparing the OpenTelemetry integration with the
@posthog/aiwrapper for direct OpenAI usage. A recurring question was where tool calls show up: the wrapper captured which tools the model chose only as data inside$ai_output_choices, never as spans, so tool calls didn't appear as nodes in the trace tree the way framework integrations (OpenAI Agents SDK, LangChain) render them.This adds first-class tool-call spans to the direct-SDK wrappers so a tool call the LLM requests becomes a real
$ai_spanin the trace.Why: so users on the OpenAI/Anthropic/Gemini wrappers get tool calls visualized as spans in the trace tree without adopting a full framework or OTel.
What changed
$ai_spanper requested tool ($ai_span_type: "tool") with the tool name, arguments, and tool-call id, nested under the generation via$ai_parent_id.$ai_span_idso the tool spans can parent to them.Scope / known limitation
The wrapper observes the model's response (the request to call a tool) but never executes the tool, so these spans have no duration or result — they mark which tools were chosen. Timing/results of the actual tool execution would need the execution instrumented directly (a follow-up
with client.trace_tool(...)-style helper is the natural next step). Span capture is best-effort and is wrapped so it can never break the underlying LLM call.💚 How did you test it?
Added/updated tests asserting the new
$ai_span, its properties, and its parent linkage to the generation across OpenAI, Anthropic, and Gemini (sync, async, streaming). Ran the affected suites locally:Live-API integration tests and the LangChain callback tests were not run (they require provider API keys and are unaffected — LangChain uses a separate callback path).
📝 Checklist
sampo addto generate a changeset file (added.sampo/changesets/ai-tool-call-spans.md)🤖 Agent context
Autonomy: Human-driven (agent-assisted) — directed by @marco-g-pm from a Slack thread.
Authored by the PostHog Slack app (Claude Code). The thread weighed the OTel integration vs. the wrapper for direct SDKs; the agreed gap was that the wrapper captures tool choice as generation data but emits no spans. There was a design fork between (a) a passive span derived from the response and (b) an execution-timing helper the user wraps their tool call in. This PR implements (a) — it matches the "automatic capture on response" behavior requested and needs no call-site changes — and documents (b) as the follow-up for real duration/result. Implemented in the shared
utils.pycapture helpers so all three providers and all three call modes are covered from one place.Created with PostHog from a Slack thread