3838from google .adk .models .lite_llm import _get_completion_inputs
3939from google .adk .models .lite_llm import _get_content
4040from google .adk .models .lite_llm import _get_provider_from_model
41+ from google .adk .models .lite_llm import _is_anthropic_provider
4142from google .adk .models .lite_llm import _is_anthropic_model
4243from google .adk .models .lite_llm import _message_to_generate_content_response
4344from google .adk .models .lite_llm import _MISSING_TOOL_RESULT_MESSAGE
@@ -4689,6 +4690,17 @@ def test_handles_litellm_logger_names(logger_name):
46894690# ── Anthropic thinking_blocks tests ─────────────────────────────
46904691
46914692
4693+ def test_is_anthropic_provider ():
4694+ """Verify _is_anthropic_provider matches known Claude provider prefixes."""
4695+ assert _is_anthropic_provider ("anthropic" )
4696+ assert _is_anthropic_provider ("bedrock" )
4697+ assert _is_anthropic_provider ("vertex_ai" )
4698+ assert _is_anthropic_provider ("ANTHROPIC" ) # case-insensitive
4699+ assert not _is_anthropic_provider ("openai" )
4700+ assert not _is_anthropic_provider ("" )
4701+ assert not _is_anthropic_provider (None )
4702+
4703+
46924704@pytest .mark .parametrize (
46934705 "model_string,expected" ,
46944706 [
@@ -4723,9 +4735,10 @@ def test_is_anthropic_model(model_string, expected):
47234735
47244736
47254737def test_extract_reasoning_value_prefers_thinking_blocks ():
4726- """thinking_blocks takes precedence over reasoning_content ."""
4738+ """thinking_blocks (Anthropic format with signatures) take priority ."""
47274739 thinking_blocks = [
4728- {"type" : "thinking" , "thinking" : "deep thought" , "signature" : "sig123" },
4740+ {"type" : "thinking" , "thinking" : "step 1" , "signature" : "c2lnX2E=" },
4741+ {"type" : "thinking" , "thinking" : "step 2" , "signature" : "c2lnX2I=" },
47294742 ]
47304743 message = {
47314744 "role" : "assistant" ,
@@ -4748,25 +4761,36 @@ def test_extract_reasoning_value_falls_back_without_thinking_blocks():
47484761 assert result == "flat reasoning"
47494762
47504763
4751- def test_convert_reasoning_value_to_parts_thinking_blocks_preserves_signature ():
4752- """thinking_blocks format produces parts with thought_signature ."""
4764+ def test_convert_reasoning_value_to_parts_preserves_base64_signature ():
4765+ """Base64 signatures are decoded to raw bytes on thought parts ."""
47534766 thinking_blocks = [
4754- {"type" : "thinking" , "thinking" : "step 1" , "signature" : "sig_abc " },
4755- {"type" : "thinking" , "thinking" : "step 2" , "signature" : "sig_def " },
4767+ {"type" : "thinking" , "thinking" : "step 1" , "signature" : "c2lnX2E= " },
4768+ {"type" : "thinking" , "thinking" : "step 2" , "signature" : "c2lnX2I= " },
47564769 ]
47574770 parts = _convert_reasoning_value_to_parts (thinking_blocks )
47584771 assert len (parts ) == 2
47594772 assert parts [0 ].text == "step 1"
47604773 assert parts [0 ].thought is True
4761- assert parts [0 ].thought_signature == b"sig_abc "
4774+ assert parts [0 ].thought_signature == b"sig_a "
47624775 assert parts [1 ].text == "step 2"
4763- assert parts [1 ].thought_signature == b"sig_def"
4776+ assert parts [1 ].thought_signature == b"sig_b"
4777+
4778+
4779+ def test_convert_reasoning_value_to_parts_raw_signature_falls_back_to_utf8 ():
4780+ """Non-base64 signatures are preserved as utf-8 bytes."""
4781+ thinking_blocks = [
4782+ {"type" : "thinking" , "thinking" : "step 1" , "signature" : "sig_raw" },
4783+ ]
4784+ parts = _convert_reasoning_value_to_parts (thinking_blocks )
4785+ assert len (parts ) == 1
4786+ assert parts [0 ].text == "step 1"
4787+ assert parts [0 ].thought_signature == b"sig_raw"
47644788
47654789
47664790def test_convert_reasoning_value_to_parts_skips_redacted_blocks ():
47674791 """Redacted thinking blocks are excluded from parts."""
47684792 thinking_blocks = [
4769- {"type" : "thinking" , "thinking" : "visible" , "signature" : "sig1 " },
4793+ {"type" : "thinking" , "thinking" : "visible" , "signature" : "c2lnMQ== " },
47704794 {"type" : "redacted" , "data" : "hidden" },
47714795 ]
47724796 parts = _convert_reasoning_value_to_parts (thinking_blocks )
@@ -4777,8 +4801,8 @@ def test_convert_reasoning_value_to_parts_skips_redacted_blocks():
47774801def test_convert_reasoning_value_to_parts_skips_empty_thinking ():
47784802 """Blocks with empty thinking text are excluded."""
47794803 thinking_blocks = [
4780- {"type" : "thinking" , "thinking" : "" , "signature" : "sig1 " },
4781- {"type" : "thinking" , "thinking" : "real thought" , "signature" : "sig2 " },
4804+ {"type" : "thinking" , "thinking" : "" , "signature" : "c2lnMQ== " },
4805+ {"type" : "thinking" , "thinking" : "real thought" , "signature" : "c2lnMg== " },
47824806 ]
47834807 parts = _convert_reasoning_value_to_parts (thinking_blocks )
47844808 assert len (parts ) == 1
@@ -4812,13 +4836,14 @@ async def test_content_to_message_param_anthropic_outputs_thinking_blocks():
48124836 content , model = "anthropic/claude-4-sonnet"
48134837 )
48144838 assert result ["role" ] == "assistant"
4815- assert "thinking_blocks" in result
4839+ assert result ["thinking_blocks" ] == [
4840+ {
4841+ "type" : "thinking" ,
4842+ "thinking" : "deep thought" ,
4843+ "signature" : "sig_round_trip" ,
4844+ }
4845+ ]
48164846 assert result .get ("reasoning_content" ) is None
4817- blocks = result ["thinking_blocks" ]
4818- assert len (blocks ) == 1
4819- assert blocks [0 ]["type" ] == "thinking"
4820- assert blocks [0 ]["thinking" ] == "deep thought"
4821- assert blocks [0 ]["signature" ] == "sig_round_trip"
48224847 assert result ["content" ] == "Hello!"
48234848
48244849
@@ -4839,43 +4864,45 @@ async def test_content_to_message_param_non_anthropic_uses_reasoning_content():
48394864
48404865
48414866@pytest .mark .asyncio
4842- async def test_anthropic_thinking_blocks_round_trip ():
4843- """End-to-end: thinking_blocks in response → Part → thinking_blocks out."""
4844- # Simulate LiteLLM response with thinking_blocks
4867+ async def test_anthropic_provider_thinking_blocks_round_trip ():
4868+ """End-to-end: thinking_blocks in response stay intact for Anthropic provider."""
48454869 response_message = {
48464870 "role" : "assistant" ,
48474871 "content" : "Final answer" ,
48484872 "thinking_blocks" : [
48494873 {
48504874 "type" : "thinking" ,
48514875 "thinking" : "Let me reason..." ,
4852- "signature" : "abc123signature " ,
4876+ "signature" : "c2lnX2E= " ,
48534877 },
48544878 ],
48554879 }
48564880
4857- # Step 1: Extract reasoning value
48584881 reasoning_value = _extract_reasoning_value (response_message )
48594882 assert isinstance (reasoning_value , list )
48604883
4861- # Step 2: Convert to parts (preserves signature)
48624884 parts = _convert_reasoning_value_to_parts (reasoning_value )
48634885 assert len (parts ) == 1
4864- assert parts [0 ].thought_signature == b"abc123signature "
4886+ assert parts [0 ].thought_signature == b"sig_a "
48654887
4866- # Step 3: Build Content for history
4867- all_parts = parts + [types .Part (text = "Final answer" )]
4888+ all_parts = parts + [
4889+ types .Part (text = "Final answer" ),
4890+ types .Part .from_function_call (name = "add" , args = {"a" : 1 , "b" : 2 }),
4891+ ]
48684892 content = types .Content (role = "model" , parts = all_parts )
48694893
4870- # Step 4: Convert back to message param for Anthropic
4871- result = await _content_to_message_param (
4872- content , model = "anthropic/claude-4-sonnet"
4873- )
4874- blocks = result ["thinking_blocks" ]
4875- assert len (blocks ) == 1
4876- assert blocks [0 ]["type" ] == "thinking"
4877- assert blocks [0 ]["thinking" ] == "Let me reason..."
4878- assert blocks [0 ]["signature" ] == "abc123signature"
4894+ msg = await _content_to_message_param (content , provider = "anthropic" )
4895+ assert isinstance (msg ["content" ], list )
4896+ assert msg ["content" ][0 ] == {
4897+ "type" : "thinking" ,
4898+ "thinking" : "Let me reason..." ,
4899+ "signature" : "c2lnX2E=" ,
4900+ }
4901+ assert msg ["content" ][1 ] == {"type" : "text" , "text" : "Final answer" }
4902+ assert msg ["tool_calls" ] is not None
4903+ assert len (msg ["tool_calls" ]) == 1
4904+ assert msg ["tool_calls" ][0 ]["function" ]["name" ] == "add"
4905+ assert msg .get ("reasoning_content" ) is None
48794906
48804907
48814908@pytest .mark .asyncio
@@ -4891,6 +4918,5 @@ async def test_content_to_message_param_anthropic_no_signature_falls_back():
48914918 result = await _content_to_message_param (
48924919 content , model = "anthropic/claude-4-sonnet"
48934920 )
4894- # Falls back to reasoning_content when no signatures present
48954921 assert result .get ("reasoning_content" ) == "thinking without sig"
48964922 assert "thinking_blocks" not in result
0 commit comments