From 384eb91ff6830e79ae9868b514d74ad0d3afff97 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Sat, 22 Aug 2026 14:00:47 +0800 Subject: [PATCH] fix(models): populate LlmResponse.model_version in AnthropicLlm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AnthropicLlm never set model_version on either path, so the resolved snapshot id (model aliases resolve server-side, e.g. claude-sonnet-5 -> a dated snapshot) was lost for every direct-API response. Every other first-party wrapper already records it: google_llm from the Gemini response, lite_llm from response.model, apigee_llm from the payload. Downstream consumers rely on it — event telemetry, persisted Event rows, and the gen_ai.response.model span attribute. - message_to_generate_content_response: model_version=message.model - streaming: capture event.message.model at message_start, yield it on the final aggregated response (guarded on str so partial/mock streams that omit the field cannot poison the field) Fixes #6847 Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> --- src/google/adk/models/anthropic_llm.py | 7 ++ tests/unittests/models/test_anthropic_llm.py | 85 +++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index f6622628d6..3d6716e24f 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -709,6 +709,7 @@ def message_to_generate_content_response( role="model", parts=parts, ), + model_version=message.model, usage_metadata=usage_metadata, finish_reason=to_google_genai_finish_reason(message.stop_reason), ) @@ -1012,6 +1013,7 @@ async def _generate_content_streaming( cached_input_tokens: int | None = None cache_creation_tokens: int | None = None stop_reason: Optional[anthropic_types.StopReason] = None + model_version: str | None = None async for event in raw_stream: if event.type == "message_start": @@ -1019,6 +1021,10 @@ async def _generate_content_streaming( output_tokens = event.message.usage.output_tokens thinking_tokens = _extract_thinking_token_count(event.message.usage) cached_input_tokens = _extract_cached_token_count(event.message.usage) + # Guard on str: keeps partial/mock streams that omit the field from + # poisoning model_version with a non-string. + if isinstance(event.message.model, str): + model_version = event.message.model cache_creation_tokens = _extract_cache_creation_token_count( event.message.usage ) @@ -1149,6 +1155,7 @@ async def _generate_content_streaming( yield LlmResponse( content=types.Content(role="model", parts=all_parts), + model_version=model_version, usage_metadata=usage_metadata, finish_reason=to_google_genai_finish_reason(stop_reason), partial=False, diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index 1f25a21700..41a524cf1f 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -16,7 +16,6 @@ import json import os import re -import sys from unittest import mock from unittest.mock import AsyncMock from unittest.mock import MagicMock @@ -38,7 +37,6 @@ from google.adk.models.llm_request import LlmRequest from google.adk.models.llm_response import LlmResponse from google.genai import types -from google.genai import version as genai_version from google.genai.types import Content from google.genai.types import Part import httpx @@ -3457,3 +3455,86 @@ def test_anthropic_config_allows_thinking_budget_without_thinking_level(): assert config.effort == "high" assert config.thinking_config.thinking_budget == 2048 assert config.thinking_config.thinking_level is None + + +def test_message_to_generate_content_response_sets_model_version(): + """The served snapshot id must land in LlmResponse.model_version (#6847).""" + from google.adk.models.anthropic_llm import ( + message_to_generate_content_response, + ) + + message = anthropic_types.Message( + id="msg_model_version", + content=[ + anthropic_types.TextBlock(text="ok", 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=1, + output_tokens=2, + cache_creation_input_tokens=0, + cache_read_input_tokens=0, + server_tool_use=None, + service_tier=None, + ), + ) + + response = message_to_generate_content_response(message) + + assert response.model_version == "claude-sonnet-4-20250514" + + +@pytest.mark.asyncio +async def test_streaming_final_response_carries_model_version(): + """Streaming: message_start's resolved model id reaches the final yield.""" + llm = AnthropicLlm(model="claude-sonnet-4-20250514") + + events = [ + MagicMock( + type="message_start", + message=MagicMock( + model="claude-sonnet-4-20250514", + usage=MagicMock(input_tokens=10, 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=3), + ), + MagicMock(type="message_stop"), + ] + + mock_client = MagicMock() + mock_client.messages.create = AsyncMock( + return_value=_make_mock_stream_events(events) + ) + + llm_request = LlmRequest( + model="claude-sonnet-4-20250514", + contents=[Content(role="user", parts=[Part.from_text(text="Hi")])], + ) + + with mock.patch.object(llm, "_anthropic_client", mock_client): + responses = [ + r async for r in llm.generate_content_async(llm_request, stream=True) + ] + + final = responses[-1] + assert final.partial is False + assert final.model_version == "claude-sonnet-4-20250514"