[Bug]: Gemini native tool history needs canonical functionCall and functionResponse keys
Check for existing issues
- I searched existing issues; this follows up #20557
What happened?
Gemini-native tool conversations can fail in two ways at Gemini-compatible gateways: returning a tool result can produce HTTP 400, or a later turn can return only reasoning followed by finish_reason: "stop", leaving the application without its next tool call or final answer
The outgoing history uses snake_case Part keys. Renaming only the immediate tool-result key from function_response to functionResponse fixes the original empty-Part error. That alone is insufficient for multiple turns: renaming the historical tool-call key from function_call to functionCall restores the missing next call on EasyRouter
In the controlled history comparison, the roles, IDs, thought signatures, function arguments, results and tool definitions stayed identical. The snake_case history produced reasoning-only stop; the canonical history produced the requested third tool call with the correct nonce from the preceding tool result
Google's REST Part representation uses functionCall and functionResponse. ProtoJSON permits both spellings, so this reports compatibility observed at Gemini-compatible gateways, not universal rejection by Google's endpoints
#20569 attempted a role change for #20557 and was reverted by #21051. This fix keeps roles unchanged and normalizes only the two direct protocol keys. Nested business JSON is preserved
User Flow
Before a fix: a tool-using application receives an error or stops before finishing its task
- Send
POST https://YOUR_PROXY_HOST/chat/completionswith a Gemini alias, a prompt requiring three sequential tools, and the tool definition - Execute each returned tool and send its result with the complete signed tool-call ID
- Continue until the required final answer arrives
- Observe HTTP 400 after a tool result, or HTTP 200 with reasoning-only
stopbefore the third call and no final answer
After a fix: the application finishes all three tool steps and answers
- Send
POST https://YOUR_PROXY_HOST/chat/completionswith a Gemini alias, a prompt requiring three sequential tools, and the tool definition - Execute each returned tool and send its result with the complete signed tool-call ID
- Continue until the required final answer arrives
- Observe three valid tool executions followed by the expected
FINAL_OKanswer
Proof the bug occurs
Observed proxy version: 1.93.2. The native serializer at merge base a8979fe05466346d78ab4573c8a61b53a4dd9cf6 emits both snake_case keys. A response-only fix allows some continuations but leaves the later-history failure
Sanitized reproduction configuration:
model_list:
- model_name: gemini-tool-test
litellm_params:
model: gemini/gemini-3.8-flash
api_base: os.environ/GEMINI_API_BASE
api_key: os.environ/GEMINI_API_KEY
Gateway bases used: https://easyrouter.io/v1beta and https://api.cometapi.com/v1beta. Each uses its own credentials and fresh signed tool calls
Use a get_step(n, nonce) tool that requires the nonce from the preceding result and generates a new unpredictable nonce for the next step. Require steps 1, 2 and 3 before the final marker. The failing EasyRouter run executed two valid steps, then returned reasoning without text or tool calls, followed by stop. Direct inspection of the proxy SSE confirmed the response was already incomplete before application conversion
The original tool-result failure was:
contents[2].parts[0].data: required oneof field 'data' must have one initialized field
The canonical-history-only comparison restored the missing call on EasyRouter. CometAPI accepted both call spellings in that comparison; its earlier generic HTTP 400 is not assigned the same cause
See the complete native-body generation and HTTP replay recipe in #41410. The latest run at 0c58f8372ef41ae9cbe686b764a885b764eb8ab1 completed three valid sequential tool steps and the final marker on both routes; the merge-base run failed with the empty-Part error. The evidence uses real model calls with synthetic inputs; serializer-body replay through an existing proxy is distinguished there from running a newly built proxy end to end
What part of LiteLLM is this about?
Proxy / Gemini native request serialization
What LiteLLM version are you on?
1.93.2; also reproduced in the upstream serializer at the merge base above
Source: BerriAI/litellm