diff --git a/src/strands/models/gemini.py b/src/strands/models/gemini.py index c24d91a0d..df34c5849 100644 --- a/src/strands/models/gemini.py +++ b/src/strands/models/gemini.py @@ -141,12 +141,16 @@ def _format_request_content_part(self, content: ContentBlock) -> genai.types.Par ) if "toolUse" in content: + # Use skip_thought_signature_validator to bypass Gemini 3 Pro's strict validation + # This is a temporary workaround until we implement proper thought signature preservation + # See: https://ai.google.dev/gemini-api/docs/thought-signatures return genai.types.Part( function_call=genai.types.FunctionCall( args=content["toolUse"]["input"], id=content["toolUse"]["toolUseId"], name=content["toolUse"]["name"], ), + thought_signature=b"skip_thought_signature_validator", ) raise TypeError(f"content_type=<{next(iter(content))}> | unsupported type") diff --git a/tests/strands/models/test_gemini.py b/tests/strands/models/test_gemini.py index a8f5351cc..6999b652d 100644 --- a/tests/strands/models/test_gemini.py +++ b/tests/strands/models/test_gemini.py @@ -289,6 +289,8 @@ async def test_stream_request_with_tool_use(gemini_client, model, model_id): "id": "c1", "name": "calculator", }, + # Skip validator is used as a temporary workaround for Gemini 3 Pro + "thought_signature": "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I=", }, ], "role": "model", @@ -299,6 +301,35 @@ async def test_stream_request_with_tool_use(gemini_client, model, model_id): gemini_client.aio.models.generate_content_stream.assert_called_with(**exp_request) +@pytest.mark.asyncio +async def test_stream_request_with_tool_use_includes_thought_signature_skip(gemini_client, model, model_id): + """Test that toolUse includes skip_thought_signature_validator for Gemini 3 Pro compatibility.""" + messages = [ + { + "role": "assistant", + "content": [ + { + "toolUse": { + "toolUseId": "tool1", + "name": "get_weather", + "input": {"city": "Seattle"}, + }, + }, + ], + }, + ] + await anext(model.stream(messages)) + + call_args = gemini_client.aio.models.generate_content_stream.call_args + contents = call_args.kwargs["contents"] + + # Verify the thought_signature is set to skip validator + part = contents[0]["parts"][0] + assert "thought_signature" in part + # Base64 encoded "skip_thought_signature_validator" + assert part["thought_signature"] == "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I=" + + @pytest.mark.asyncio async def test_stream_request_with_tool_results(gemini_client, model, model_id): messages = [ diff --git a/tests_integ/models/test_model_gemini.py b/tests_integ/models/test_model_gemini.py index f9da8490c..0d623bd89 100644 --- a/tests_integ/models/test_model_gemini.py +++ b/tests_integ/models/test_model_gemini.py @@ -175,3 +175,28 @@ def test_agent_structured_output_image_input(assistant_agent, yellow_img, yellow tru_color = assistant_agent.structured_output(type(yellow_color), content) exp_color = yellow_color assert tru_color == exp_color + + +@pytest.mark.parametrize("model_id", ["gemini-2.5-flash", "gemini-3-pro-preview"]) +def test_agent_multiturn_tool_use(model_id, tools, system_prompt): + """Test multi-turn conversation with tool use. + + Gemini 3 Pro requires thought_signature in multi-turn function calling. + Without skip_thought_signature_validator, this fails with: + "Function call is missing a thought_signature in functionCall parts" + + Validates fix for issue #1199. + """ + model = GeminiModel( + client_args={"api_key": os.getenv("GOOGLE_API_KEY")}, + model_id=model_id, + params={"temperature": 0.15}, + ) + agent = Agent(model=model, tools=tools, system_prompt=system_prompt) + + result1 = agent("What is the current time in New York?") + assert "12:00" in result1.message["content"][0]["text"].lower() + + # Second turn with tool use history - this is where Gemini 3 Pro would fail + result2 = agent("And what is the weather there?") + assert "sunny" in result2.message["content"][0]["text"].lower()