#41409·litellm

[Bug]: Gemini native tool history needs canonical functionCall and functionResponse keys

Author: henry867Created Sep 16, 2026Updated Sep 17, 2026
Labelsllm translation

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

  1. Send POST https://YOUR_PROXY_HOST/chat/completions with a Gemini alias, a prompt requiring three sequential tools, and the tool definition
  2. Execute each returned tool and send its result with the complete signed tool-call ID
  3. Continue until the required final answer arrives
  4. Observe HTTP 400 after a tool result, or HTTP 200 with reasoning-only stop before the third call and no final answer

After a fix: the application finishes all three tool steps and answers

  1. Send POST https://YOUR_PROXY_HOST/chat/completions with a Gemini alias, a prompt requiring three sequential tools, and the tool definition
  2. Execute each returned tool and send its result with the complete signed tool-call ID
  3. Continue until the required final answer arrives
  4. Observe three valid tool executions followed by the expected FINAL_OK answer

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