GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-04-29Intermediate

Fixing 'Thoughts must be present in conversation history' in Gemini API: Thought Signatures in Multi-Turn Tool Calls

If you're hitting 'Thoughts must be present in conversation history when using thinking signature' in Gemini 2.5/3.x with multi-turn function calling, this guide walks through what's actually happening and how to fix it in five minutes — Python SDK, REST, and streaming all covered.

troubleshooting82thought-signaturesthinking7function-calling20gemini-api277

If you've seen this error, you're not alone

If you're building a multi-turn agent on Gemini 2.5 Pro or 3.x with thinking enabled, there's a good chance you've run into this:

google.api_core.exceptions.InvalidArgument: 400 ...
Thoughts must be present in conversation history when using thinking signature.

The official docs mention it almost in passing — "include the thought_signature in the conversation history" — and leave you to figure out the rest. I burned half a day on this when one of my agents started failing on the second tool turn during a production rollout, so I want to save you that time.

This guide covers exactly when this error fires and the smallest possible patch that fixes it. I'll show Python SDK and REST examples, and there's a streaming-specific section because that's where this bug likes to hide.

What's actually going on

Gemini 2.5/3.x thinking models do internal reasoning during a function-calling turn and emit the result as parts[].thought_signature — a base64-encoded opaque token. That token holds encrypted state that the model needs in order to continue the same line of reasoning on the next turn.

When you send your next request, you have to send the function-call part with that signature still attached as part of the conversation history. The model uses it to verify continuity. Strip it, and the API politely refuses to talk to you.

In practice, the signature gets accidentally dropped in a handful of ways:

  • You pull the function_call out of the response and rebuild a fresh Content object around it.
  • You serialise responses to a custom dict shape before storing them.
  • You serialise to JSON and pass to a different language SDK that drops null-valued fields.
  • An older version of LangChain or LlamaIndex re-creates the parts array.

The common cause: you did too much work on the response object. The fix is almost always to do less.

A useful mental model: think of thought_signature as a chain-of-custody seal on the model's reasoning. As long as the seal travels with the function-call part it was attached to, the model can pick up where it left off. Once the seal is removed, that piece of reasoning becomes inadmissible — the API has no way to verify continuity, so it refuses the turn rather than silently producing a worse answer. That framing made the error feel less arbitrary to me, and it points the debugger straight at the layers that touch parts[].

The fix in one line

Here's the minimum working pattern with the google-genai Python SDK:

import os
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
tools = [{
    "function_declarations": [{
        "name": "get_weather",
        "description": "Returns the current weather for a city.",
        "parameters": {
            "type": "OBJECT",
            "properties": {"city": {"type": "STRING"}},
            "required": ["city"],
        },
    }]
}]
 
config = types.GenerateContentConfig(
    tools=tools,
    thinking_config=types.ThinkingConfig(thinking_budget=1024),
)
 
# Turn 1
contents = [types.Content(
    role="user",
    parts=[types.Part.from_text(text="What's the weather in Tokyo right now?")],
)]
res1 = client.models.generate_content(
    model="gemini-2.5-pro", contents=contents, config=config,
)
 
# This single line is the whole fix:
contents.append(res1.candidates[0].content)
 
# Run the tool and feed the result back in
fc = res1.candidates[0].content.parts[0].function_call
tool_result = {"city": fc.args["city"], "temp_c": 18, "summary": "Sunny"}
 
contents.append(types.Content(
    role="user",
    parts=[types.Part.from_function_response(name=fc.name, response=tool_result)],
))
 
# Turn 2 (signature is preserved, so this succeeds)
res2 = client.models.generate_content(
    model="gemini-2.5-pro", contents=contents, config=config,
)
print(res2.text)
# Expected: "Tokyo is sunny and 18C right now." or similar

The whole story is in contents.append(res1.candidates[0].content). By appending the response's Content object verbatim — instead of unpacking it and rebuilding — every part keeps its thought_signature automatically.

The pattern that breaks it

This is the lookalike code that triggers the error every time:

# Bad: extracting just the function_call and rebuilding the Content
fc = res1.candidates[0].content.parts[0].function_call
contents.append(types.Content(
    role="model",
    parts=[types.Part(function_call=fc)],  # signature gets dropped here
))

The fix is the same one-liner:

# Good
contents.append(res1.candidates[0].content)

If your code persists conversation history to Redis or disk as JSON, double-check the serialiser. Pydantic's model_dump_json(exclude_none=True) will drop fields it considers absent, and that has bitten more than one team. I switched to exclude_none=False and it's been stable since. When this error reappears unexpectedly, the serialisation layer is usually where I look first.

Streaming: aggregate every chunk

If you're consuming responses with generate_content_stream, appending only the last chunk almost guarantees this error. You need to collect every part across every chunk and rebuild a single Content for history.

final_parts = []
function_call = None
 
for chunk in client.models.generate_content_stream(
    model="gemini-2.5-pro", contents=contents, config=config,
):
    if not chunk.candidates or not chunk.candidates[0].content.parts:
        continue
    for part in chunk.candidates[0].content.parts:
        final_parts.append(part)  # don't filter — collect everything
        if part.function_call:
            function_call = part.function_call
 
contents.append(types.Content(role="model", parts=final_parts))

In Gemini 3.x I've seen thought_signature arrive on intermediate chunks rather than the final one. Skipping any chunk's parts will silently delete it, and you won't know until the very next request fails. If your streaming agent is the one throwing this error, audit the chunk loop first.

REST API: same idea, watch your jq filters

If you're hitting generateContent directly with curl or fetch, the rule is identical: take candidates[0].content from the response and drop it as-is into your next request's contents array.

RESP1=$(curl -s \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @req1.json \
  "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent")
 
MODEL_TURN=$(echo "$RESP1" | jq '.candidates[0].content')
jq --argjson m "$MODEL_TURN" '.contents += [$m] | .contents += [{
  "role": "user",
  "parts": [{"functionResponse": {"name": "get_weather", "response": {"temp_c": 18}}}]
}]' req1.json > req2.json

One additional REST-specific tip: when you log requests for debugging, log the original JSON payload rather than a "pretty-printed" version that drops empty fields. I have lost an hour to a logging library that pruned null siblings of thoughtSignature and made the corresponding production payloads look identical to the broken ones.

If you happen to run something like jq 'del(.candidates[0].content.parts[].thoughtSignature)' to "clean up" the response — even just to make logs more readable — congratulations, you've reproduced this bug. Keep thoughtSignature in the JSON you send back.

Reproducing it locally in 30 seconds

If you want to confirm whether your code path is the one stripping signatures, here is a tiny self-contained reproduction that you can drop into a scratch file. It deliberately rebuilds the model turn the wrong way to make the error fire predictably.

import os
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
tools = [{"function_declarations": [{
    "name": "lookup",
    "description": "Look up a value by key.",
    "parameters": {"type": "OBJECT", "properties": {"key": {"type": "STRING"}}, "required": ["key"]},
}]}]
config = types.GenerateContentConfig(
    tools=tools,
    thinking_config=types.ThinkingConfig(thinking_budget=512),
)
 
contents = [types.Content(role="user", parts=[types.Part.from_text(text="Look up alpha please.")])]
res = client.models.generate_content(model="gemini-2.5-pro", contents=contents, config=config)
fc = res.candidates[0].content.parts[-1].function_call
 
# Comment out ONE of these to flip between broken and working
# contents.append(res.candidates[0].content)             # Working
contents.append(types.Content(role="model", parts=[types.Part(function_call=fc)]))  # Broken
 
contents.append(types.Content(
    role="user",
    parts=[types.Part.from_function_response(name=fc.name, response={"value": 42})],
))
 
client.models.generate_content(model="gemini-2.5-pro", contents=contents, config=config)

Run once with the broken append active and you should see the 400 within seconds. Switch the comment, rerun, and the second turn returns a normal text response. If your real codebase passes this minimal repro but still throws in production, you can be reasonably confident the issue is in serialisation, retry, or streaming — not in the basic call shape.

Checklist when nothing above works

A few less common causes worth ruling out:

  • SDK version. Use google-genai>=1.0.0. The legacy google-generativeai package is EOL and won't receive thinking-mode fixes.
  • Thinking budget set to 0. With thinking_budget=0, no signature is emitted, so the error disappears — but you also lose the entire benefit of thinking. If you didn't intend to disable it, set a positive budget.
  • Old LangChain or LlamaIndex. Both libraries reconstruct parts internally; older releases dropped signatures. Upgrade to the version that explicitly mentions thought signatures in its changelog.
  • Mixing Vertex AI and Google AI Studio. Signatures are scoped to the endpoint that issued them. A history captured against Studio will be rejected by Vertex AI and vice versa. Keep a conversation entirely on one endpoint.
  • Retry layers reusing partial state. If your retry middleware rebuilds the request rather than re-sending the original payload, it can strip the signature. Retry the original contents verbatim.

The Vertex/Studio mismatch in particular is easy to overlook during a production migration. If you're switching environments, plan to start fresh conversations rather than carrying history across.

Wrapping up

Strip away the noise and this error reduces to one habit: append the response content to your history without modifying it. Hold to that and roughly nine out of ten cases of "thoughts not present" disappear. The remaining cases almost always live in streaming aggregation or a Vertex/Studio mix-up — work those two before suspecting anything more exotic.

If you're still untangling other tool-calling issues, the symptom-by-symptom guide at Gemini Function Calling Not Working: Complete Troubleshooting Guide covers the surrounding territory. For agents that get stuck repeating the same call, see Fixing Tool-Calling Infinite Loops in Gemini API.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-01
Empty Output but finish_reason Is MAX_TOKENS on Gemini 2.5/3: Cause and Fix
Your prompt is just a few lines, yet a low maxOutputTokens on gemini-2.5-flash returns empty text with finish_reason MAX_TOKENS. The culprit is thinking tokens. Here are three fixes with working code.
API / SDK2026-05-30
Why Gemini 2.5 Pro Rejects thinkingBudget: 0 (and How to Fix It)
Setting thinkingBudget to 0 on Gemini 2.5 Pro returns a 400 INVALID_ARGUMENT error. Here is why the per-model thinking budget ranges differ, how to minimize thinking on Pro the right way, and when to switch to Flash, with Python and JavaScript examples.
API / SDK2026-05-09
Gemini API Returns 400 When You Set tools and responseSchema Together — Three Designs That Make Function Calling and Structured Output Coexist
You want function calling to fetch external data and a strict JSON shape for the final answer. Setting tools and responseSchema together returns 400. Here's why, plus three production-tested designs that make both work.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →