diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py index e46713e513..e1c77e9f63 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py @@ -627,7 +627,9 @@ def _wrap( ) elif response: try: - metric_attributes = shared_metrics_attributes(response) + metric_attributes = shared_metrics_attributes( + response, request_model=kwargs.get("model") + ) if duration_histogram: duration = time.time() - start_time @@ -755,7 +757,9 @@ async def _awrap( ashared_metrics_attributes, ) - metric_attributes = await ashared_metrics_attributes(response) + metric_attributes = await ashared_metrics_attributes( + response, request_model=kwargs.get("model") + ) if duration_histogram: duration = time.time() - start_time diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py index 45762dfc99..045e589b0f 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py @@ -262,7 +262,10 @@ def __next__(self): # FR: rewritten for pending-item safety buffer # the duration_histogram conditional, causing metrics to be recorded twice. def _handle_completion(self): """Handle completion logic""" - metric_attributes = shared_metrics_attributes(self._complete_response) + metric_attributes = shared_metrics_attributes( + self._complete_response, + request_model=self._kwargs.get("model"), + ) set_span_attribute( self._span, GenAIAttributes.GEN_AI_RESPONSE_ID, @@ -451,7 +454,10 @@ def _complete_instrumentation(self): return # This mirrors the logic from abuild_from_streaming_response - metric_attributes = shared_metrics_attributes(self._complete_response) + metric_attributes = shared_metrics_attributes( + self._complete_response, + request_model=self._kwargs.get("model"), + ) set_span_attribute(self._span, GenAIAttributes.GEN_AI_RESPONSE_ID, self._complete_response.get("id")) if self._duration_histogram: diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py index 8344b3da4d..81b1cb3f8c 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py @@ -136,7 +136,7 @@ def _extract_response_data(response): @dont_throw -async def ashared_metrics_attributes(response): +async def ashared_metrics_attributes(response, request_model=None): import inspect # If we get a coroutine, await it @@ -168,15 +168,19 @@ async def ashared_metrics_attributes(response): common_attributes = Config.get_common_metrics_attributes() - return { + metric_attributes = { **common_attributes, GenAIAttributes.GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC, GenAIAttributes.GEN_AI_RESPONSE_MODEL: model, } + if request_model: + metric_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = request_model + + return metric_attributes @dont_throw -def shared_metrics_attributes(response): +def shared_metrics_attributes(response, request_model=None): import inspect # If we get a coroutine, we cannot process it in sync context @@ -205,11 +209,15 @@ def shared_metrics_attributes(response): common_attributes = Config.get_common_metrics_attributes() - return { + metric_attributes = { **common_attributes, GenAIAttributes.GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC, GenAIAttributes.GEN_AI_RESPONSE_MODEL: model, } + if request_model: + metric_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = request_model + + return metric_attributes @dont_throw diff --git a/packages/opentelemetry-instrumentation-anthropic/tests/utils.py b/packages/opentelemetry-instrumentation-anthropic/tests/utils.py index fd3ad80c5a..1d53496fe4 100644 --- a/packages/opentelemetry-instrumentation-anthropic/tests/utils.py +++ b/packages/opentelemetry-instrumentation-anthropic/tests/utils.py @@ -27,6 +27,10 @@ def verify_metrics( data_point.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] == model_name ) + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == model_name + ) if not ignore_zero_input_tokens: assert data_point.sum > 0 @@ -38,6 +42,10 @@ def verify_metrics( data_point.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] == model_name ) + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == model_name + ) if metric.name == Meters.LLM_OPERATION_DURATION: found_duration_metric = True @@ -53,6 +61,12 @@ def verify_metrics( or data_point.attributes.get("error.type") == "TypeError" for data_point in metric.data.data_points ) + assert all( + data_point.attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) + == model_name + or data_point.attributes.get("error.type") == "TypeError" + for data_point in metric.data.data_points + ) if metric.name == Meters.LLM_ANTHROPIC_COMPLETION_EXCEPTIONS: found_exception_metric = True diff --git a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/span_utils.py b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/span_utils.py index 0edc750b4b..fd029941be 100644 --- a/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/span_utils.py +++ b/packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/span_utils.py @@ -646,6 +646,7 @@ def _metric_shared_attributes( ): return { "vendor": response_vendor, + GenAIAttributes.GEN_AI_REQUEST_MODEL: response_model, GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_model, GenAIAttributes.GEN_AI_SYSTEM: "bedrock", "stream": is_streaming, diff --git a/packages/opentelemetry-instrumentation-bedrock/tests/metrics/test_bedrock_metrics.py b/packages/opentelemetry-instrumentation-bedrock/tests/metrics/test_bedrock_metrics.py index f3b7e25feb..5cec4f25c7 100644 --- a/packages/opentelemetry-instrumentation-bedrock/tests/metrics/test_bedrock_metrics.py +++ b/packages/opentelemetry-instrumentation-bedrock/tests/metrics/test_bedrock_metrics.py @@ -39,6 +39,10 @@ def test_invoke_model_metrics(test_context, brt): found_token_metric = False found_duration_metric = False + expected_request_models = { + "amazon.titan-text-express-v1", + "titan-text-express-v1", + } for rm in resource_metrics: for sm in rm.scope_metrics: @@ -51,6 +55,10 @@ def test_invoke_model_metrics(test_context, brt): "output", "input", ] + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + in expected_request_models + ) assert data_point.sum > 0 if metric.name == Meters.LLM_OPERATION_DURATION: @@ -61,6 +69,18 @@ def test_invoke_model_metrics(test_context, brt): assert any( data_point.sum > 0 for data_point in metric.data.data_points ) + model_points = [ + data_point + for data_point in metric.data.data_points + if GenAIAttributes.GEN_AI_REQUEST_MODEL + in data_point.attributes + ] + assert model_points + assert all( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + in expected_request_models + for data_point in model_points + ) if metric.name in (Meters.LLM_TOKEN_USAGE, Meters.LLM_OPERATION_DURATION): assert ( diff --git a/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py b/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py index eb7a38ae9a..21bff41a23 100644 --- a/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py +++ b/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py @@ -239,6 +239,7 @@ async def _awrap( duration, attributes={ GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google", + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model, }, ) @@ -318,6 +319,7 @@ def _wrap( duration, attributes={ GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google", + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model, }, ) diff --git a/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py b/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py index 0c92bc6840..e904035eaf 100644 --- a/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py +++ b/packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py @@ -475,6 +475,7 @@ def set_model_response_attributes(span, response, llm_model, token_histogram): attributes={ GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google", GenAIAttributes.GEN_AI_TOKEN_TYPE: "input", + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model, } ) @@ -483,6 +484,7 @@ def set_model_response_attributes(span, response, llm_model, token_histogram): attributes={ GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google", GenAIAttributes.GEN_AI_TOKEN_TYPE: "output", + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model, }, ) diff --git a/packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py b/packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py index 82d3a7383f..4801c29d56 100644 --- a/packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py +++ b/packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py @@ -97,6 +97,10 @@ def test_generate_metrics(metrics_test_context, genai_client): # Required attributes (values are intentionally not hard-coded) assert GenAIAttributes.GEN_AI_PROVIDER_NAME in duration_dp.attributes assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in duration_dp.attributes + assert ( + duration_dp.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gemini-2.5-flash" + ) # ---- Token metric (only emitted when response includes usage_metadata) ---- if Meters.LLM_TOKEN_USAGE in metrics: @@ -120,3 +124,7 @@ def test_generate_metrics(metrics_test_context, genai_client): # Required semantic attributes assert GenAIAttributes.GEN_AI_PROVIDER_NAME in dp.attributes assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in dp.attributes + assert ( + dp.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gemini-2.5-flash" + ) diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py index cd5c17100d..880394b3fc 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py @@ -668,6 +668,7 @@ def on_llm_end( attributes={ GenAIAttributes.GEN_AI_SYSTEM: vendor, GenAIAttributes.GEN_AI_TOKEN_TYPE: "input", + GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name or "unknown", GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name or "unknown", }, ) @@ -678,6 +679,7 @@ def on_llm_end( attributes={ GenAIAttributes.GEN_AI_SYSTEM: vendor, GenAIAttributes.GEN_AI_TOKEN_TYPE: "output", + GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name or "unknown", GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name or "unknown", }, ) @@ -698,6 +700,7 @@ def on_llm_end( duration, attributes={ GenAIAttributes.GEN_AI_SYSTEM: vendor, + GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name or "unknown", GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name or "unknown", }, ) diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py index 49dab634ad..edfd7643fd 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py @@ -373,6 +373,7 @@ def set_chat_response_usage( attributes={ GenAIAttributes.GEN_AI_SYSTEM: vendor, GenAIAttributes.GEN_AI_TOKEN_TYPE: "input", + GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name, GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name, }, ) @@ -383,6 +384,7 @@ def set_chat_response_usage( attributes={ GenAIAttributes.GEN_AI_SYSTEM: vendor, GenAIAttributes.GEN_AI_TOKEN_TYPE: "output", + GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name, GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name, }, ) diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py index 99c416cdb2..bfccd8b35f 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py @@ -373,12 +373,16 @@ def _token_type(token_type: str): def metric_shared_attributes( - response_model: str, operation: str, server_address: str, is_streaming: bool = False + response_model: str, + operation: str, + server_address: str, + is_streaming: bool = False, + request_model: str = None, ): attributes = Config.get_common_metrics_attributes() vendor = _get_vendor_from_url(server_address) - return { + metric_attributes = { **attributes, GenAIAttributes.GEN_AI_SYSTEM: vendor, GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_model, @@ -386,6 +390,10 @@ def metric_shared_attributes( "server.address": server_address, "stream": is_streaming, } + if request_model: + metric_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = request_model + + return metric_attributes def propagate_trace_context(span, kwargs): diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py index a9938afe0d..468d1b31a3 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py @@ -188,6 +188,7 @@ def chat_wrapper( choice_counter, duration_histogram, duration, + request_kwargs=kwargs, ) span.end() @@ -303,6 +304,7 @@ async def achat_wrapper( choice_counter, duration_histogram, duration, + request_kwargs=kwargs, ) span.end() @@ -353,6 +355,7 @@ def _handle_response( duration_histogram=None, duration=None, is_streaming: bool = False, + request_kwargs=None, ): if is_openai_v1(): response_dict = model_as_dict(response) @@ -368,6 +371,7 @@ def _handle_response( response_dict, duration, is_streaming, + request_kwargs=request_kwargs, ) # span attributes @@ -414,12 +418,14 @@ def _set_chat_metrics( response_dict, duration, is_streaming: bool = False, + request_kwargs=None, ): shared_attributes = metric_shared_attributes( response_model=response_dict.get("model") or None, operation="chat", server_address=_get_openai_base_url(instance), is_streaming=is_streaming, + request_model=(request_kwargs or {}).get("model"), ) # token metrics @@ -782,6 +788,7 @@ def _shared_attributes(self): operation="chat", server_address=_get_openai_base_url(self._instance), is_streaming=True, + request_model=self._request_kwargs.get("model"), ) @dont_throw @@ -927,6 +934,7 @@ def _build_from_streaming_response( start_time=None, request_kwargs=None, ): + request_kwargs = request_kwargs or {} complete_response = {"choices": [], "model": "", "id": ""} streaming_safety = OpenAIChatStreamingSafety(span, SPAN_NAME) @@ -948,11 +956,15 @@ def _build_from_streaming_response( yield item_to_yield - shared_attributes = { - GenAIAttributes.GEN_AI_RESPONSE_MODEL: complete_response.get("model") or None, - "server.address": _get_openai_base_url(instance), - "stream": True, - } + shared_attributes = metric_shared_attributes( + response_model=complete_response.get("model") + or request_kwargs.get("model") + or None, + operation="chat", + server_address=_get_openai_base_url(instance), + is_streaming=True, + request_model=request_kwargs.get("model"), + ) _set_streaming_token_metrics( request_kwargs, complete_response, span, token_counter, shared_attributes @@ -999,6 +1011,7 @@ async def _abuild_from_streaming_response( start_time=None, request_kwargs=None, ): + request_kwargs = request_kwargs or {} complete_response = {"choices": [], "model": "", "id": ""} streaming_safety = OpenAIChatStreamingSafety(span, SPAN_NAME) @@ -1020,11 +1033,15 @@ async def _abuild_from_streaming_response( yield item_to_yield - shared_attributes = { - GenAIAttributes.GEN_AI_RESPONSE_MODEL: complete_response.get("model") or None, - "server.address": _get_openai_base_url(instance), - "stream": True, - } + shared_attributes = metric_shared_attributes( + response_model=complete_response.get("model") + or request_kwargs.get("model") + or None, + operation="chat", + server_address=_get_openai_base_url(instance), + is_streaming=True, + request_model=request_kwargs.get("model"), + ) _set_streaming_token_metrics( request_kwargs, complete_response, span, token_counter, shared_attributes diff --git a/packages/opentelemetry-instrumentation-openai/tests/metrics/test_openai_metrics.py b/packages/opentelemetry-instrumentation-openai/tests/metrics/test_openai_metrics.py index e5462442ae..1e8c4b4ec5 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/metrics/test_openai_metrics.py +++ b/packages/opentelemetry-instrumentation-openai/tests/metrics/test_openai_metrics.py @@ -50,6 +50,10 @@ def test_chat_completion_metrics(instrument_legacy, reader, openai_client): "output", "input", ] + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gpt-3.5-turbo" + ) assert len(data_point.attributes["server.address"]) > 0 assert data_point.sum > 0 @@ -71,6 +75,11 @@ def test_chat_completion_metrics(instrument_legacy, reader, openai_client): len(data_point.attributes["server.address"]) > 0 for data_point in metric.data.data_points ) + assert all( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gpt-3.5-turbo" + for data_point in metric.data.data_points + ) assert found_token_metric is True assert found_choice_metric is True @@ -116,6 +125,10 @@ class StructuredAnswer(BaseModel): metric.name == Meters.LLM_TOKEN_USAGE and model == "gpt-4o-2024-08-06" ): + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gpt-4o" + ) found_token_metric = True elif ( metric.name == Meters.LLM_GENERATION_CHOICES @@ -126,6 +139,10 @@ class StructuredAnswer(BaseModel): metric.name == Meters.LLM_OPERATION_DURATION and model == "gpt-4o-2024-08-06" ): + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "gpt-4o" + ) found_duration_metric = True assert found_token_metric @@ -176,6 +193,10 @@ def test_chat_streaming_metrics(instrument_legacy, reader, deepseek_client): "output", "input", ] + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "deepseek-chat" + ) assert data_point.sum > 0 if metric.name == Meters.LLM_GENERATION_CHOICES: @@ -220,6 +241,16 @@ def test_chat_streaming_metrics(instrument_legacy, reader, deepseek_client): ) in ("gpt-3.5-turbo", "gpt-3.5-turbo-0125", "gpt-4o-2024-08-06", "deepseek-chat") assert data_point.attributes["gen_ai.operation.name"] == "chat" assert data_point.attributes["server.address"] != "" + if metric.name in ( + Meters.LLM_TOKEN_USAGE, + Meters.LLM_OPERATION_DURATION, + GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN, + Meters.LLM_STREAMING_TIME_TO_GENERATE, + ): + assert ( + data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "deepseek-chat" + ) assert found_token_metric is True assert found_choice_metric is True