From f6e19eaa4882720af36e51029b3f828b7a417b87 Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Wed, 15 Apr 2026 02:08:05 +0000 Subject: [PATCH 1/2] fix: omit system parameter when None in AnthropicLlm When system_instruction is None (e.g. during event compaction via LlmEventSummarizer), the Anthropic API rejects system=None with a 400 Bad Request. Use NOT_GIVEN instead so the parameter is omitted from the API call entirely. Fixes #5318 --- src/google/adk/models/anthropic_llm.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index a14c767f23..9d1e9cf054 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -401,11 +401,18 @@ async def generate_content_async( if llm_request.tools_dict else NOT_GIVEN ) + # Anthropic API rejects system=None; omit the parameter when no + # system instruction is set (e.g. during event compaction). + system_instruction = ( + llm_request.config.system_instruction + if llm_request.config and llm_request.config.system_instruction + else NOT_GIVEN + ) if not stream: message = await self._anthropic_client.messages.create( model=model_to_use, - system=llm_request.config.system_instruction, + system=system_instruction, messages=messages, tools=tools, tool_choice=tool_choice, @@ -414,7 +421,7 @@ async def generate_content_async( yield message_to_generate_content_response(message) else: async for response in self._generate_content_streaming( - llm_request, messages, tools, tool_choice + llm_request, messages, tools, tool_choice, system_instruction ): yield response @@ -424,6 +431,7 @@ async def _generate_content_streaming( messages: list[anthropic_types.MessageParam], tools: Union[Iterable[anthropic_types.ToolUnionParam], NotGiven], tool_choice: Union[anthropic_types.ToolChoiceParam, NotGiven], + system_instruction: Union[str, NotGiven] = NOT_GIVEN, ) -> AsyncGenerator[LlmResponse, None]: """Handles streaming responses from Anthropic models. @@ -433,7 +441,7 @@ async def _generate_content_streaming( model_to_use = self._resolve_model_name(llm_request.model) raw_stream = await self._anthropic_client.messages.create( model=model_to_use, - system=llm_request.config.system_instruction, + system=system_instruction, messages=messages, tools=tools, tool_choice=tool_choice, From 0eab1a4b993edc17a8fa16acadc512e89db996d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= <258577966+voidborne-d@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:07:03 +0000 Subject: [PATCH 2/2] test: add unit tests for system_instruction=None (#5318) Adds two tests covering the fix for #5318: - test_generate_content_async_none_system_instruction_non_streaming - test_generate_content_async_none_system_instruction_streaming Both verify that system=NOT_GIVEN (not None) is passed to the Anthropic API when system_instruction is unset, preventing 400 Bad Request errors during event compaction. --- tests/unittests/models/test_anthropic_llm.py | 121 +++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index fb44d5c8e7..35cf1a31bb 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -1350,3 +1350,124 @@ async def test_non_streaming_does_not_pass_stream_param(): mock_client.messages.create.assert_called_once() _, kwargs = mock_client.messages.create.call_args assert "stream" not in kwargs + + +# --- Test for system_instruction=None fix (#5318) --- + + +@pytest.mark.asyncio +async def test_generate_content_async_none_system_instruction_non_streaming(): + """When system_instruction is None, system should be NOT_GIVEN, not None. + + Regression test for #5318: AnthropicLlm.generate_content_async passes + system=None to the Anthropic API when no system instruction is set + (e.g. during event compaction via LlmEventSummarizer), which causes + a 400 Bad Request from the Anthropic API. + """ + llm = AnthropicLlm(model="claude-sonnet-4-20250514") + + mock_message = anthropic_types.Message( + id="msg_test_no_sys", + content=[ + anthropic_types.TextBlock( + text="Hello!", type="text", citations=None + ) + ], + model="claude-sonnet-4-20250514", + role="assistant", + stop_reason="end_turn", + stop_sequence=None, + type="message", + usage=anthropic_types.Usage( + input_tokens=5, + output_tokens=2, + cache_creation_input_tokens=0, + cache_read_input_tokens=0, + server_tool_use=None, + service_tier=None, + ), + ) + + mock_client = MagicMock() + mock_client.messages.create = AsyncMock(return_value=mock_message) + + # Config with system_instruction=None (as happens during event compaction) + llm_request = LlmRequest( + model="claude-sonnet-4-20250514", + contents=[Content(role="user", parts=[Part.from_text(text="Hi")])], + config=types.GenerateContentConfig( + system_instruction=None, + ), + ) + + with mock.patch.object(llm, "_anthropic_client", mock_client): + responses = [ + r async for r in llm.generate_content_async(llm_request, stream=False) + ] + + assert len(responses) == 1 + mock_client.messages.create.assert_called_once() + _, kwargs = mock_client.messages.create.call_args + # system should be NOT_GIVEN (omitted), NOT None + from anthropic import NOT_GIVEN + + assert kwargs["system"] is NOT_GIVEN, ( + f"Expected system=NOT_GIVEN but got system={kwargs['system']!r}. " + "Passing system=None causes Anthropic API 400 errors." + ) + + +@pytest.mark.asyncio +async def test_generate_content_async_none_system_instruction_streaming(): + """Streaming path should also omit system when system_instruction is None.""" + llm = AnthropicLlm(model="claude-sonnet-4-20250514") + + events = [ + MagicMock( + type="message_start", + message=MagicMock(usage=MagicMock(input_tokens=5, output_tokens=0)), + ), + MagicMock( + type="content_block_start", + index=0, + content_block=anthropic_types.TextBlock(text="", type="text"), + ), + MagicMock( + type="content_block_delta", + index=0, + delta=anthropic_types.TextDelta(text="Hi", type="text_delta"), + ), + MagicMock(type="content_block_stop", index=0), + MagicMock( + type="message_delta", + delta=MagicMock(stop_reason="end_turn"), + usage=MagicMock(output_tokens=1), + ), + MagicMock(type="message_stop"), + ] + + mock_client = MagicMock() + mock_client.messages.create = AsyncMock( + return_value=_make_mock_stream_events(events) + ) + + # Config with system_instruction=None + llm_request = LlmRequest( + model="claude-sonnet-4-20250514", + contents=[Content(role="user", parts=[Part.from_text(text="Hi")])], + config=types.GenerateContentConfig( + system_instruction=None, + ), + ) + + with mock.patch.object(llm, "_anthropic_client", mock_client): + _ = [r async for r in llm.generate_content_async(llm_request, stream=True)] + + mock_client.messages.create.assert_called_once() + _, kwargs = mock_client.messages.create.call_args + from anthropic import NOT_GIVEN + + assert kwargs["system"] is NOT_GIVEN, ( + f"Expected system=NOT_GIVEN but got system={kwargs['system']!r}. " + "Passing system=None causes Anthropic API 400 errors." + )