Skip to content

Commit e4ed1fe

Browse files
committed
fix(models): re-encode anthropic thinking signatures
Cover the model-driven LiteLLM Anthropic round-trip with a regression test.
1 parent 793880f commit e4ed1fe

File tree

2 files changed

+38
-3
lines changed

2 files changed

+38
-3
lines changed

src/google/adk/models/lite_llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ async def _content_to_message_param(
903903
if part.text and part.thought_signature:
904904
sig = part.thought_signature
905905
if isinstance(sig, bytes):
906-
sig = sig.decode("utf-8")
906+
sig = base64.b64encode(sig).decode("utf-8")
907907
thinking_blocks.append({
908908
"type": "thinking",
909909
"thinking": part.text,

tests/unittests/models/test_litellm.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4820,7 +4820,7 @@ def test_convert_reasoning_value_to_parts_flat_string_unchanged():
48204820

48214821
@pytest.mark.asyncio
48224822
async def test_content_to_message_param_anthropic_outputs_thinking_blocks():
4823-
"""For Anthropic models, thinking_blocks are output instead of reasoning_content."""
4823+
"""Anthropic model messages base64-encode thought signatures."""
48244824
content = types.Content(
48254825
role="model",
48264826
parts=[
@@ -4839,12 +4839,47 @@ async def test_content_to_message_param_anthropic_outputs_thinking_blocks():
48394839
assert result["thinking_blocks"] == [{
48404840
"type": "thinking",
48414841
"thinking": "deep thought",
4842-
"signature": "sig_round_trip",
4842+
"signature": "c2lnX3JvdW5kX3RyaXA=",
48434843
}]
48444844
assert result.get("reasoning_content") is None
48454845
assert result["content"] == "Hello!"
48464846

48474847

4848+
@pytest.mark.asyncio
4849+
async def test_content_to_message_param_anthropic_model_round_trip_preserves_signature():
4850+
"""Decoded signatures are re-encoded when rebuilding Anthropic messages."""
4851+
response_message = {
4852+
"role": "assistant",
4853+
"content": "Final answer",
4854+
"thinking_blocks": [{
4855+
"type": "thinking",
4856+
"thinking": "Let me reason...",
4857+
"signature": "c2lnX2E=",
4858+
}],
4859+
}
4860+
4861+
parts = _convert_reasoning_value_to_parts(
4862+
_extract_reasoning_value(response_message)
4863+
)
4864+
content = types.Content(
4865+
role="model",
4866+
parts=parts + [types.Part(text="Final answer")],
4867+
)
4868+
4869+
result = await _content_to_message_param(
4870+
content,
4871+
provider="anthropic",
4872+
model="anthropic/claude-4-sonnet",
4873+
)
4874+
4875+
assert result["thinking_blocks"] == [{
4876+
"type": "thinking",
4877+
"thinking": "Let me reason...",
4878+
"signature": "c2lnX2E=",
4879+
}]
4880+
assert result.get("reasoning_content") is None
4881+
4882+
48484883
@pytest.mark.asyncio
48494884
async def test_content_to_message_param_non_anthropic_uses_reasoning_content():
48504885
"""For non-Anthropic models, reasoning_content is used as before."""

0 commit comments

Comments
 (0)