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
This commit is contained in:
Nicolò Boschi 2026-03-13 16:43:01 +01:00 committed by GitHub
parent c7db770281
commit 21f9f46ca3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 16 additions and 4 deletions

View file

@ -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,
)
)

View file

@ -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(

View file

@ -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):

View file

@ -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"),