Skip to content

Commit 3f505d2

Browse files
GWealecopybara-github
authored andcommitted
fix(models): pass NOT_GIVEN to Anthropic when no system_instruction
When LlmRequest has no system_instruction (e.g. during event compaction via LlmEventSummarizer), AnthropicLlm passed system=None to the Anthropic SDK, which serializes to JSON null and the API rejects with 'system: Input should be a valid list'. Use NOT_GIVEN to omit the parameter entirely instead. Close google#5318 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 932664392
1 parent 04b0c4b commit 3f505d2

2 files changed

Lines changed: 98 additions & 3 deletions

File tree

src/google/adk/models/anthropic_llm.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,11 +553,14 @@ async def generate_content_async(
553553
else NOT_GIVEN
554554
)
555555
thinking = _build_anthropic_thinking_param(llm_request.config)
556+
system = NOT_GIVEN
557+
if llm_request.config.system_instruction is not None:
558+
system = llm_request.config.system_instruction
556559

557560
if not stream:
558561
message = await self._anthropic_client.messages.create(
559562
model=model_to_use,
560-
system=llm_request.config.system_instruction,
563+
system=system,
561564
messages=messages,
562565
tools=tools,
563566
tool_choice=tool_choice,
@@ -567,14 +570,15 @@ async def generate_content_async(
567570
yield message_to_generate_content_response(message)
568571
else:
569572
async for response in self._generate_content_streaming(
570-
llm_request, messages, tools, tool_choice, thinking
573+
llm_request, messages, system, tools, tool_choice, thinking
571574
):
572575
yield response
573576

574577
async def _generate_content_streaming(
575578
self,
576579
llm_request: LlmRequest,
577580
messages: list[anthropic_types.MessageParam],
581+
system: Union[str, types.Content, NotGiven],
578582
tools: Union[Iterable[anthropic_types.ToolUnionParam], NotGiven],
579583
tool_choice: Union[anthropic_types.ToolChoiceParam, NotGiven],
580584
thinking: Union[
@@ -591,7 +595,7 @@ async def _generate_content_streaming(
591595
model_to_use = self._resolve_model_name(llm_request.model)
592596
raw_stream = await self._anthropic_client.messages.create(
593597
model=model_to_use,
594-
system=llm_request.config.system_instruction,
598+
system=system,
595599
messages=messages,
596600
tools=tools,
597601
tool_choice=tool_choice,

tests/unittests/models/test_anthropic_llm.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from unittest.mock import AsyncMock
2222
from unittest.mock import MagicMock
2323

24+
from anthropic import NOT_GIVEN
2425
from anthropic import types as anthropic_types
2526
from google.adk import version as adk_version
2627
from google.adk.models import anthropic_llm
@@ -2134,3 +2135,93 @@ async def test_generate_content_async_pairs_invalid_tool_ids(
21342135
]
21352136
assert len(set(use_ids)) == expected_unique
21362137
assert set(use_ids) == set(result_ids)
2138+
2139+
2140+
@pytest.mark.asyncio
2141+
async def test_non_streaming_no_system_instruction_passes_not_given():
2142+
"""system=NOT_GIVEN when LlmRequest has no system_instruction."""
2143+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2144+
2145+
mock_message = anthropic_types.Message(
2146+
id="msg_test",
2147+
content=[
2148+
anthropic_types.TextBlock(text="ok", type="text", citations=None)
2149+
],
2150+
model="claude-sonnet-4-20250514",
2151+
role="assistant",
2152+
stop_reason="end_turn",
2153+
stop_sequence=None,
2154+
type="message",
2155+
usage=anthropic_types.Usage(
2156+
input_tokens=1,
2157+
output_tokens=1,
2158+
cache_creation_input_tokens=0,
2159+
cache_read_input_tokens=0,
2160+
server_tool_use=None,
2161+
service_tier=None,
2162+
),
2163+
)
2164+
2165+
mock_client = MagicMock()
2166+
mock_client.messages.create = AsyncMock(return_value=mock_message)
2167+
2168+
request = LlmRequest(
2169+
model="claude-sonnet-4-20250514",
2170+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2171+
)
2172+
assert request.config.system_instruction is None
2173+
2174+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2175+
_ = [r async for r in llm.generate_content_async(request, stream=False)]
2176+
2177+
mock_client.messages.create.assert_called_once()
2178+
_, kwargs = mock_client.messages.create.call_args
2179+
assert kwargs["system"] is NOT_GIVEN
2180+
2181+
2182+
@pytest.mark.asyncio
2183+
async def test_streaming_no_system_instruction_passes_not_given():
2184+
"""system=NOT_GIVEN on the streaming path when no system_instruction."""
2185+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2186+
2187+
events = [
2188+
MagicMock(
2189+
type="message_start",
2190+
message=MagicMock(usage=MagicMock(input_tokens=1, output_tokens=0)),
2191+
),
2192+
MagicMock(
2193+
type="content_block_start",
2194+
index=0,
2195+
content_block=anthropic_types.TextBlock(text="", type="text"),
2196+
),
2197+
MagicMock(
2198+
type="content_block_delta",
2199+
index=0,
2200+
delta=anthropic_types.TextDelta(text="ok", type="text_delta"),
2201+
),
2202+
MagicMock(type="content_block_stop", index=0),
2203+
MagicMock(
2204+
type="message_delta",
2205+
delta=MagicMock(stop_reason="end_turn"),
2206+
usage=MagicMock(output_tokens=1),
2207+
),
2208+
MagicMock(type="message_stop"),
2209+
]
2210+
2211+
mock_client = MagicMock()
2212+
mock_client.messages.create = AsyncMock(
2213+
return_value=_make_mock_stream_events(events)
2214+
)
2215+
2216+
request = LlmRequest(
2217+
model="claude-sonnet-4-20250514",
2218+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2219+
)
2220+
assert request.config.system_instruction is None
2221+
2222+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2223+
_ = [r async for r in llm.generate_content_async(request, stream=True)]
2224+
2225+
mock_client.messages.create.assert_called_once()
2226+
_, kwargs = mock_client.messages.create.call_args
2227+
assert kwargs["system"] is NOT_GIVEN

0 commit comments

Comments
 (0)