From 21f9f46ca3f4aae83aeb61e9c0c621686e61323b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Fri, 13 Mar 2026 16:43:01 +0100 Subject: [PATCH] fix: support gemini-3.1-flash-lite-preview by preserving thought_signature in tool calls (#568) Gemini 3.1+ thinking models include a thought_signature field in functionCall parts. When reconstructing conversation history for subsequent turns, this signature must be preserved or the API returns 400 INVALID_ARGUMENT. - Add optional thought_signature field to LLMToolCall - Capture thought_signature from Gemini response parts - Pass thought_signature back when reconstructing multi-turn history - Add gemini-3.1-flash-lite-preview to the LLM provider test matrix --- .../hindsight_api/engine/providers/gemini_llm.py | 10 +++++++--- .../hindsight_api/engine/reflect/agent.py | 5 ++++- .../hindsight_api/engine/response_models.py | 4 ++++ hindsight-api-slim/tests/test_llm_provider.py | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/hindsight-api-slim/hindsight_api/engine/providers/gemini_llm.py b/hindsight-api-slim/hindsight_api/engine/providers/gemini_llm.py index e66bfbd7..63f1e438 100644 --- a/hindsight-api-slim/hindsight_api/engine/providers/gemini_llm.py +++ b/hindsight-api-slim/hindsight_api/engine/providers/gemini_llm.py @@ -470,9 +470,11 @@ class GeminiLLM(LLMInterface): fn_name = fn.get("name", "") fn_args_str = fn.get("arguments", "{}") fn_args = parse_llm_json(fn_args_str) - parts.append( - genai_types.Part(function_call=genai_types.FunctionCall(name=fn_name, args=fn_args)) - ) + thought_signature = tc.get("thought_signature") + fc_kwargs: dict[str, Any] = {"name": fn_name, "args": fn_args} + if thought_signature: + fc_kwargs["thought_signature"] = thought_signature + parts.append(genai_types.Part(function_call=genai_types.FunctionCall(**fc_kwargs))) gemini_contents.append(genai_types.Content(role="model", parts=parts)) else: gemini_contents.append(genai_types.Content(role="model", parts=[genai_types.Part(text=content)])) @@ -545,11 +547,13 @@ class GeminiLLM(LLMInterface): content = part.text if hasattr(part, "function_call") and part.function_call: fc = part.function_call + thought_signature = getattr(fc, "thought_signature", None) tool_calls.append( LLMToolCall( id=f"gemini_{len(tool_calls)}", name=fc.name, arguments=dict(fc.args) if fc.args else {}, + thought_signature=thought_signature, ) ) diff --git a/hindsight-api-slim/hindsight_api/engine/reflect/agent.py b/hindsight-api-slim/hindsight_api/engine/reflect/agent.py index 9874fdf8..5d700b5e 100644 --- a/hindsight-api-slim/hindsight_api/engine/reflect/agent.py +++ b/hindsight-api-slim/hindsight_api/engine/reflect/agent.py @@ -895,7 +895,7 @@ async def run_reflect_agent( def _tool_call_to_dict(tc: "LLMToolCall") -> dict[str, Any]: """Convert LLMToolCall to OpenAI message format.""" - return { + d: dict[str, Any] = { "id": tc.id, "type": "function", "function": { @@ -903,6 +903,9 @@ def _tool_call_to_dict(tc: "LLMToolCall") -> dict[str, Any]: "arguments": json.dumps(tc.arguments), }, } + if tc.thought_signature is not None: + d["thought_signature"] = tc.thought_signature + return d async def _process_done_tool( diff --git a/hindsight-api-slim/hindsight_api/engine/response_models.py b/hindsight-api-slim/hindsight_api/engine/response_models.py index b46b842b..f31d39c6 100644 --- a/hindsight-api-slim/hindsight_api/engine/response_models.py +++ b/hindsight-api-slim/hindsight_api/engine/response_models.py @@ -20,6 +20,10 @@ class LLMToolCall(BaseModel): id: str = Field(description="Unique identifier for this tool call") name: str = Field(description="Name of the tool to call") arguments: dict[str, Any] = Field(description="Arguments to pass to the tool") + thought_signature: str | None = Field( + default=None, + description="Opaque token required by Gemini 3.1+ thinking models to preserve thought context across turns", + ) class LLMToolCallResult(BaseModel): diff --git a/hindsight-api-slim/tests/test_llm_provider.py b/hindsight-api-slim/tests/test_llm_provider.py index 491cc00b..f8d0d8a6 100644 --- a/hindsight-api-slim/tests/test_llm_provider.py +++ b/hindsight-api-slim/tests/test_llm_provider.py @@ -35,6 +35,7 @@ MODEL_MATRIX = [ ("gemini", "gemini-2.5-flash"), ("gemini", "gemini-2.5-flash-lite"), ("gemini", "gemini-3-pro-preview"), + ("gemini", "gemini-3.1-flash-lite-preview"), # Ollama models (local) ("ollama", "gemma3:12b"), ("ollama", "gemma3:1b"),