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:
parent
c7db770281
commit
21f9f46ca3
4 changed files with 16 additions and 4 deletions
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue