2929from contextlib import asynccontextmanager
3030from contextlib import contextmanager
3131import logging
32+ import re
3233from typing import Final
3334from typing import TYPE_CHECKING
3435
5455from opentelemetry .semconv .attributes .error_attributes import ERROR_TYPE
5556from opentelemetry .semconv .schemas import Schemas
5657from opentelemetry .trace import Span
58+ from opentelemetry .trace import Status
59+ from opentelemetry .trace import StatusCode
5760from opentelemetry .util .types import AttributeValue
5861from typing_extensions import deprecated
5962
6063from .. import version
6164from ..utils .env_utils import is_enterprise_mode_enabled
65+ from ..utils .model_name_utils import extract_model_name
6266from ..utils .model_name_utils import is_gemini_model
6367from ._experimental_semconv import maybe_log_completion_details
6468from ._experimental_semconv import set_operation_details_attributes_from_request
98102 from ..models .llm_request import LlmRequest
99103 from ..models .llm_response import LlmResponse
100104 from ..tools .base_tool import BaseTool
105+ from ..workflow ._base_node import BaseNode
101106
102107tracer = trace .get_tracer (
103108 instrumenting_module_name = "gcp.vertex.agent" ,
@@ -212,10 +217,19 @@ def trace_tool_call(
212217 ):
213218 span .set_attribute (GEN_AI_AGENT_NAME , agent .name )
214219
220+ failure_type : str | None = None
215221 if error is not None :
216- span .set_attribute (ERROR_TYPE , resolve_error_type (error ))
222+ failure_type = resolve_error_type (error )
223+ span .record_exception (error )
217224 elif error_type is not None :
218- span .set_attribute (ERROR_TYPE , error_type )
225+ failure_type = error_type
226+ if failure_type is not None :
227+ span .set_attribute (ERROR_TYPE , failure_type )
228+ # Without an explicit error status the span renders as successful, which
229+ # hides tools that reported a failure as a response dict instead of
230+ # raising. The description repeats the type rather than the error message
231+ # so no tool content lands in an attribute the content toggle cannot gate.
232+ span .set_status (Status (StatusCode .ERROR , failure_type ))
219233
220234 # Special case for client side association with a remote tool call
221235 if (
@@ -742,14 +756,7 @@ def _use_extra_generate_content_attributes(
742756
743757
744758def _is_gemini_agent (agent : BaseAgent ) -> bool :
745- from ..agents .llm_agent import LlmAgent
746-
747- if not isinstance (agent , LlmAgent ):
748- return False
749-
750- model = agent .model if agent .model != "" else agent ._default_model
751- model_name = model if isinstance (model , str ) else model .model
752- return is_gemini_model (model_name )
759+ return is_gemini_model (_agent_model_name (agent ))
753760
754761
755762def _set_common_generate_content_attributes (
@@ -770,10 +777,11 @@ def _use_native_generate_content_span_stable_semconv(
770777 telemetry_config : TelemetryConfig | None = None ,
771778) -> Iterator [GenerateContentSpan ]:
772779 telemetry_config = telemetry_config or TelemetryConfig ()
780+ system_name = _resolve_gen_ai_system_name (llm_request .model )
773781 with tracer .start_as_current_span (
774782 f"generate_content { llm_request .model or '' } "
775783 ) as span :
776- span .set_attribute (GEN_AI_SYSTEM , _guess_gemini_system_name () )
784+ span .set_attribute (GEN_AI_SYSTEM , system_name )
777785 _set_common_generate_content_attributes (
778786 span , llm_request , common_attributes
779787 )
@@ -783,10 +791,10 @@ def _use_native_generate_content_span_stable_semconv(
783791 LogRecord (
784792 event_name = GEN_AI_SYSTEM_MESSAGE_EVENT ,
785793 body = system_message_body (llm_request , telemetry_config ),
786- attributes = {GEN_AI_SYSTEM : _guess_gemini_system_name () },
794+ attributes = {GEN_AI_SYSTEM : system_name },
787795 )
788796 )
789- user_message_attributes = {GEN_AI_SYSTEM : _guess_gemini_system_name () }
797+ user_message_attributes = {GEN_AI_SYSTEM : system_name }
790798 if (
791799 telemetry_config .should_add_content_to_logs
792800 and log_only_common_attributes
@@ -871,7 +879,9 @@ def trace_generate_content_result(span: Span | None, llm_response: LlmResponse):
871879 LogRecord (
872880 event_name = GEN_AI_CHOICE_EVENT ,
873881 body = choice_body (llm_response , TelemetryConfig ()),
874- attributes = {GEN_AI_SYSTEM : _guess_gemini_system_name ()},
882+ attributes = {
883+ GEN_AI_SYSTEM : _inference_system_name (None , llm_response )
884+ },
875885 )
876886 )
877887
@@ -916,7 +926,11 @@ def trace_inference_result(
916926 body = choice_body (
917927 llm_response , telemetry_config or TelemetryConfig ()
918928 ),
919- attributes = {GEN_AI_SYSTEM : _guess_gemini_system_name ()},
929+ attributes = {
930+ GEN_AI_SYSTEM : _inference_system_name (
931+ invocation_context , llm_response
932+ )
933+ },
920934 )
921935 )
922936
@@ -927,3 +941,78 @@ def _guess_gemini_system_name() -> str:
927941 if is_enterprise_mode_enabled ()
928942 else GenAiSystemValues .GEMINI .name .lower ()
929943 )
944+
945+
946+ # Anthropic models reach ADK either as a bare `claude-*` id (the built-in
947+ # Anthropic backend, and the Vertex `publishers/anthropic/models/...` path once
948+ # normalized) or behind a LiteLLM `anthropic/...` prefix, which the generic
949+ # prefix rule below already covers.
950+ _ANTHROPIC_MODEL_PATTERN : Final = re .compile (r"^claude[-.]" , re .IGNORECASE )
951+
952+ # Leading segments of a resource-path model id, e.g. a Model Garden path like
953+ # `projects/<p>/locations/<l>/publishers/<pub>/models/<m>` or a tuned-model id
954+ # like `tunedModels/<id>`. They name a resource collection, never a provider,
955+ # so the provider-prefix rule must not read one as one.
956+ _RESOURCE_COLLECTION_SEGMENTS : Final = frozenset ({
957+ "endpoints" ,
958+ "locations" ,
959+ "models" ,
960+ "projects" ,
961+ "publishers" ,
962+ "tunedmodels" ,
963+ })
964+
965+
966+ def _resolve_gen_ai_system_name (model : str | None ) -> str :
967+ """Returns the `gen_ai.system` / `gen_ai.provider.name` value for a model.
968+
969+ The name has to follow the model actually being served, otherwise every
970+ provider is reported as Gemini. A LiteLLM-style `<provider>/<model>` id
971+ carries the provider in its prefix, which semantic conventions allow as a
972+ lowercased name outside their well-known set. The prefix is read off the bare
973+ model name so that a resource path, whose leading segments describe where the
974+ model lives rather than who serves it, is not mistaken for one. When no model
975+ id is available, or the id names no provider, the deployment-derived
976+ Gemini/Vertex name is used, since Gemini is the backend ADK talks to
977+ natively.
978+
979+ Args:
980+ model: The model id the request is being served by, if known.
981+ """
982+ if not model or is_gemini_model (model ):
983+ return _guess_gemini_system_name ()
984+
985+ model_name = extract_model_name (model )
986+ if _ANTHROPIC_MODEL_PATTERN .match (model_name ):
987+ return GenAiSystemValues .ANTHROPIC .name .lower ()
988+
989+ provider , separator , _ = model_name .partition ("/" )
990+ provider = provider .lower ()
991+ if separator and provider and provider not in _RESOURCE_COLLECTION_SEGMENTS :
992+ return provider
993+
994+ return _guess_gemini_system_name ()
995+
996+
997+ def _agent_model_name (agent : BaseAgent | BaseNode ) -> str | None :
998+ """Returns the model id configured on an agent, if it has one."""
999+ from ..agents .llm_agent import LlmAgent
1000+
1001+ if not isinstance (agent , LlmAgent ):
1002+ return None
1003+
1004+ model = agent .model if agent .model != "" else agent ._default_model
1005+ return model if isinstance (model , str ) else model .model
1006+
1007+
1008+ def _inference_system_name (
1009+ invocation_context : InvocationContext | None ,
1010+ llm_response : LlmResponse ,
1011+ ) -> str :
1012+ """Returns the system name of the model that produced an inference result."""
1013+ model = llm_response .model_version
1014+ if not model and invocation_context is not None :
1015+ agent = invocation_context .agent
1016+ if agent is not None :
1017+ model = _agent_model_name (agent )
1018+ return _resolve_gen_ai_system_name (model )
0 commit comments