Skip to content

fix(ai): preserve Gemini tool call IDs in captured generations - #1150

Merged
marandaneto merged 2 commits into
PostHog:mainfrom
mosafariuk:fix/gemini-tool-call-ids
Oct 30, 2026
Merged

marandaneto merged 2 commits into
PostHog:mainfrom
mosafariuk:fix/gemini-tool-call-ids

Conversation

@mosafariuk

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Gemini returns an id on function calls (FunctionCall.id) and on function responses (FunctionResponse.id). The Python SDK drops both from captured generations:

  • $ai_output_choices: the non-streaming formatter (format_gemini_response), the per-chunk extractor and the streaming assembler (format_gemini_streaming_output) all build the function block without the ID.
  • $ai_input: _format_part normalizes function_call and function_response parts without their IDs.

So a Gemini tool result can't be matched to the call that produced it, unlike OpenAI and Anthropic captures, which keep tool-call IDs.

This is the Python counterpart of PostHog/posthog-js#5087 (output IDs) and the ID handling in its Gemini input normalization. It uses the same shape: id on the function block, and tool_use_id on a function result. The ID is added only when Gemini returned one as a string, so calls without an ID keep their current shape.

💚 How did you test it?

  • Regression tests in posthog/test/ai/gemini/test_gemini.py, built from real google.genai.types objects:
    • test_function_call_ids_in_output_choices: parametrized over streaming and non-streaming, and over an ID versus no ID. The cases with an ID fail on main and pass with this change. The no-ID cases pass either way, which shows the existing shape is unchanged.
    • test_function_call_ids_in_input_history: a function_call and a function_response with the same ID in contents. The test asserts id and tool_use_id in $ai_input. It fails on main and passes with this change.
  • Existing Gemini tests (103 passed) that use MagicMock parts pass unchanged. A mocked part answers any attribute, so the ID is only kept when it's a real str.
  • Full suite: 5830 passed. The only 3 failures are the fork-based tests in test_metrics_autocapture.py, which also fail for me on unmodified main on macOS and are unrelated. ruff format --check, ruff check, mypy | mypy-baseline filter and make public_api_check are clean (no public API change).
  • End to end: I patched Models.generate_content and generate_content_stream to return a real GenerateContentResponse containing FunctionCall(id="call_649034", ...). On main, both paths capture the call without its ID. With this change, both include "id": "call_649034". The async client uses the same converter functions.

📝 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, then drafted the fix and tests. I reviewed and ran all of it.
  • I kept the posthog-js #5087 shape (id on calls, tool_use_id on results). I typed the new blocks as Dict[str, Any] instead of importing FormattedFunctionCall into the converter, so the public API snapshot doesn't change.

Gemini returns IDs on function calls and function responses, but the
captured $ai_output_choices and $ai_input dropped them, so tool results
could not be matched to their calls. Keep the call ID on output (streaming
and non-streaming) and on input history calls, and the result's ID as
tool_use_id, matching PostHog/posthog-js#5087.
@mosafariuk
mosafariuk requested a review from a team as a code owner October 29, 2026 16:08

@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 enabled auto-merge (squash) October 30, 2026 13:11
@marandaneto

Copy link
Copy Markdown
Member

thanks @mosafariuk

@marandaneto
marandaneto merged commit a61e64d into PostHog:main Oct 30, 2026
42 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.

2 participants