Function Calling Gotchas
Three breaking changes in Gemini 3.5 Flash that affect tool-use / function-calling workflows. One causes a 400, two are silent.
FunctionResponse: id + name required
Missing either field โ HTTP 400. Check every tool call handler.
thinking_level + thinking_budget conflict
Using both in ThinkingConfig โ HTTP 400. Remove thinking_budget.
Multi-turn token blowup
Thought preservation is now ON. Thinking tokens accumulate silently in chat history.
FunctionResponse: id + name required
Both id and name fields are required in every FunctionResponse. Missing either one returns HTTP 400. This applies to Python SDK, TypeScript SDK, and REST API.
# โ BROKEN in gemini-3.5-flash โ missing id and name
tool_response_part = {
"functionResponse": {
"response": {"result": "72ยฐF and sunny"} # no id, no name
}
}
# Result: HTTP 400 Bad Request# โ CORRECT in gemini-3.5-flash
from google.generativeai import types
# After receiving a function call from the model:
# function_call.id = "call_abc123"
# function_call.name = "get_weather"
tool_response_part = types.Part(
function_response=types.FunctionResponse(
id=function_call.id, # โ match the call's id exactly
name=function_call.name, # โ match the function name exactly
response={"result": "72ยฐF and sunny"}
)
)
# Then send back:
response = model.generate_content([user_message, model_response, tool_response_part])// TypeScript SDK โ FunctionResponse with id + name required
const functionResponse = {
functionResponse: {
id: functionCall.id, // โ required
name: functionCall.name, // โ required
response: {
result: "72ยฐF and sunny"
}
}
};
const result = await model.generateContent([
userMessage,
modelResponse,
{ role: "tool", parts: [functionResponse] }
]);thinking_level + thinking_budget conflict โ 400
thinking_budget is deprecated and cannot be combined with thinking_level in the same ThinkingConfig. Using both throws HTTP 400. Remove thinking_budget entirely.
# โ BREAKS with 400 โ do not mix thinking_level + thinking_budget
generation_config = GenerationConfig(
thinking_config=ThinkingConfig(
thinking_level="high", # โ these two
thinking_budget=8192, # โ cannot coexist โ HTTP 400
)
)
# โ CORRECT โ pick one
generation_config = GenerationConfig(
thinking_config=ThinkingConfig(
thinking_level="high", # use thinking_level only
# no thinking_budget
)
)Multi-turn token blowup from thought preservation
Thought preservation is now ON by default. Every turn in a chat session appends the model's thinking tokens to the conversation history. At thinking_level="high", that's ~8K tokens per turn added to context โ leading to exponential cost growth and potential context overflow in long agentic loops.
# โ Token blowup โ multi-turn with thought preservation ON (default)
chat = model.start_chat(history=[])
# Turn 1: ~8K thinking tokens (high) + output
r1 = chat.send_message("Analyze this large codebase...")
# chat.history now includes 8K thinking tokens
# Turn 2: model sees previous 8K thinking + output + new prompt
r2 = chat.send_message("Now write tests for the main module...")
# chat.history grows to ~16K thinking + 2 outputs
# By turn 10: ~80K thinking tokens in context
# Cost spikes, context window exhausted
# โ FIX A: disable thought preservation for chat
model = genai.GenerativeModel(
"gemini-3.5-flash",
generation_config=GenerationConfig(
thinking_config=ThinkingConfig(
thinking_level="high",
thoughts_in_content=False, # thinking tokens NOT added to history
)
)
)
# โ FIX B: trim history after each turn
MAX_TURNS = 5
if len(chat.history) > MAX_TURNS * 2:
chat.history = chat.history[-(MAX_TURNS * 2):]Parallel function calls โ id matching
When the model returns multiple function calls in one response, each has a unique id. You must match the correct id back in each FunctionResponse โ especially important when executing tools in parallel.
# Parallel function calling โ id matching matters more now
# The model can return multiple function calls in one response
response = model.generate_content(prompt_with_tools)
# Each function_call has a unique id
function_calls = response.candidates[0].content.parts
# [FunctionCall(id="call_001", name="search"), FunctionCall(id="call_002", name="fetch")]
# Execute them (optionally in parallel)
import asyncio
results = await asyncio.gather(
execute_tool("search", fc1.args),
execute_tool("fetch", fc2.args),
)
# Return responses โ MUST match ids back
tool_parts = [
types.Part(function_response=types.FunctionResponse(
id=function_calls[0].function_call.id, # "call_001"
name=function_calls[0].function_call.name, # "search"
response={"result": results[0]},
)),
types.Part(function_response=types.FunctionResponse(
id=function_calls[1].function_call.id, # "call_002"
name=function_calls[1].function_call.name, # "fetch"
response={"result": results[1]},
)),
]