fix(vertex_ai): forward function_call id on Vertex Gemini 3+ tool turns#34603
Open
ljogeiger wants to merge 1 commit into
Open
fix(vertex_ai): forward function_call id on Vertex Gemini 3+ tool turns#34603ljogeiger wants to merge 1 commit into
ljogeiger wants to merge 1 commit into
Conversation
Vertex AI now accepts and returns `id` on functionCall and functionResponse parts for Gemini 3+ on the v1 endpoint, so the provider check added in BerriAI#28324 is stale. It silently drops the id for every Vertex caller, which breaks strict tool-call matching Gate the id on model version alone, which is what the code did before BerriAI#28324 and what Google AI Studio already does. `_forward_gemini_function_call_id` no longer takes `custom_llm_provider`, and the decision is resolved once in `_gemini_convert_messages_with_history` and passed to both converters as a bool rather than re-derived independently in each. The context caching path is covered by the same change, since it already passes `model` and the gate needs nothing else The `id` comments on `FunctionCall`, `FunctionResponse` and `HttpxFunctionCall` were also written by BerriAI#28324 and asserted the opposite of current behaviour, so they are corrected here
Contributor
Greptile SummaryThis PR enables strict tool-call ID matching for Gemini 3+ across Vertex AI and Google AI Studio.
Confidence Score: 5/5The PR appears safe to merge, with call and response IDs forwarded consistently only for recognized Gemini 3+ models. The changed conversion path applies one model-version decision to both sides of each tool turn, preserves older-model behavior, and includes focused regression coverage for the affected provider variants and ID transformations.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/prompt_templates/factory.py | Accepts the centralized forwarding decision and consistently strips thought-signature suffixes from call and response IDs. |
| litellm/llms/vertex_ai/gemini/transformation.py | Computes ID-forwarding capability once per conversion and passes it to both tool-turn converters. |
| litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py | Removes the provider-specific exclusion while retaining the existing Gemini 3+ version gate. |
| litellm/types/llms/vertex_ai.py | Updates type comments to reflect the revised version-gated provider contract. |
| tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py | Adds regression coverage for Vertex and AI Studio, older models, packed IDs, and unmatched tool responses. |
Reviews (1): Last reviewed commit: "fix(vertex_ai): forward function_call id..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
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.
TLDR
Problem this solves:
id. LiteLLM strips it from bothfunctionCallandfunctionResponseon every Vertex request, so Gemini cannot do strict tool-call matching and has to fall back to matching by function nameidon everyfunctionCalland expects it echoed back on the matchingfunctionResponse; when theidis missing,generateContentdoes not reject the request, it returns an empty response withfinish_reason: STOP. A tool-calling agent looks like it silently gave up on the last turn, which is hard to attribute to a missing field several layers downidon function_call/function_response parts". Google has since shipped the field to thev1aiplatform endpoint, so the premise no longer holds and the workaround is now the bugvertex_ai/with parallel tool calls is affected. Google AI Studio users are not, because fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns #28324 left that path aloneHow it solves it:
idon model version alone, which is what the code did before fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns #28324 and what Google AI Studio already does todaycustom_llm_providerfrom_forward_gemini_function_call_idand from both Gemini tool-call converters, since the provider branch was its only reader_gemini_convert_messages_with_historyand pass it down as a bool, instead of re-deriving it independently in each converterThis brings LiteLLM back in line with what Vertex documents for Gemini 3. Google's migration guide covers it under function calling strict response matching: the
id,nameand response count on everyfunctionResponsemust match thefunctionCallparts that preceded it, and addingidto all function response parts is listed as a required migration step. The Interactions API errors outright on a mismatch.generateContentdoes not, which is why this surfaces as degraded output rather than a 400, and why it went unnoticed after #28324 landedEchoing the
idis also the only thing that makes parallel tool calls unambiguous. Name-based matching works as long as each turn calls a distinct function, and breaks as soon as a model issues two calls to the same function with different arguments, which is common for search and lookup toolsRelevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy against real Vertex AI, no mocks. Config (
vertex_fc_id_test_config.yaml):Turn 1, get a real tool call back from Vertex:
{ "id": "ZX9IviSb__thought__AY89a18/oeibAN3n5I8cD...<truncated>", "function": { "arguments": "{\"location\": \"Boston, MA\"}", "name": "get_current_weather" } }ZX9IviSbis Vertex's ownfunctionCall.id, returned by thev1endpoint; the__thought__suffix is LiteLLM's existing thought-signature packing. Vertex returning an id here is the response-side confirmation thatv1now supports the field.Turn 2, feed that tool call and its result back:
Outbound request LiteLLM built for Google, from
litellm.log. Before this PR, onvertex_ai/gemini-3.6-flash, no id on either part:After this PR, same request, same model, id present on both parts and stripped of the thought-signature suffix:
Both returned 200 with the correct answer.
Older models are unaffected. Same turn-2 request against
vertex-gemini-25(vertex_ai/gemini-2.5-flash) still sends no id, which is required since those models reject the field:Type
🐛 Bug Fix
Changes
VertexGeminiConfig._forward_gemini_function_call_iddrops itscustom_llm_providerparameter and returns_is_gemini_3_or_newer(model). That parameter existed only to hold the!= "gemini"short-circuit, and nothing else read it.convert_to_gemini_tool_call_invokeandconvert_to_gemini_tool_call_resultnow takeforward_function_call_id: boolin place ofcustom_llm_provider(and, for the result converter,model, whose only reader was the same gate)._gemini_convert_messages_with_historyresolves the flag once and passes it to both. This removes the duplicated derivation and lets the result converter drop the function-localVertexGeminiConfigimport that existed to dodge a circular import.The
idfield comments onFunctionCall,FunctionResponseandHttpxFunctionCallintypes/llms/vertex_ai.pycame from #28324 and stated that Vertex rejects the field, which is no longer true; they now describe the version gate.custom_llm_provideron_gemini_convert_messages_with_historyitself is left in place. It is unused after this change, but it has three production call sites and a long tail of test callers, none of which have anything to do with this bug. Worth removing as a follow-up.Out of scope and unchanged:
countTokensstripsfunctionResponse.idunconditionally inllms/gemini/count_tokens/handler.py, which shifts token counts slightly but never correctness; the realtime transformation has its own_include_function_response_idreturningFalseand no seam to thread this through.QA runbook
Point a
vertex_ai/deployment at any Gemini 3+ model and run a two-turn tool-calling exchange, as in the proof of fix above. With--detailed_debug, the outboundgenerateContentbody should carry the sameidon thefunction_callpart and on the matchingfunction_responsepart, and the call should return 200. Repeat against a Gemini 2.5 deployment and confirm neither part carries anid. Google AI Studio (gemini/) behaviour should be byte-identical to before.Final Attestation
Tests live in
tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py. The two tests that encoded the old "Vertex must omit the id" contract are rewritten to the new one, and the Google AI Studio test is unchanged in meaning. Coverage: the gate is version-gated only; Gemini 3+ emits the id on both parts acrossvertex_ai,vertex_ai_betaandgemini; Gemini 2.5 emits the key on neither; a<id>__thought__<sig>tool-call id forwards stripped; and an unpairable tool result raises rather than shipping a half-formed payload.Mutation-checked against five mutants, all killed: the gate forced to
False, toTrue, and back to the old provider branch, plus each converter call site independently forced toFalse. That last pair is what makes a one-sided implementation impossible to pass, sincefunctionCall.idwithout a matchingfunctionResponse.idis exactly the shape that would break strict matching on Google's side.