From ea1836ab7c03f4ebcaa2b77eff9adf80c591cc91 Mon Sep 17 00:00:00 2001 From: Teja Date: Mon, 17 Nov 2025 17:55:45 -0500 Subject: [PATCH 1/6] fix: Fix service tier attribute names in openai-v2 instrumentation --- .../src/opentelemetry/instrumentation/openai_v2/patch.py | 2 +- .../src/opentelemetry/instrumentation/openai_v2/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py index 4d4b4871a4..b51a8aa83c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py @@ -370,7 +370,7 @@ def _set_response_attributes( if getattr(result, "service_tier", None): set_span_attribute( span, - GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER, + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, result.service_tier, ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index 739e6e9207..d321f5110c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -231,7 +231,7 @@ def get_llm_request_attributes( ] = response_format service_tier = kwargs.get("service_tier") - attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] = ( + attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] = ( service_tier if service_tier != "auto" else None ) From 97ea1f3cc5b98e789e07744ed86be59a47589b9a Mon Sep 17 00:00:00 2001 From: Teja Date: Tue, 18 Nov 2025 17:31:13 -0500 Subject: [PATCH 2/6] polish: extracting service_tier from extra_body. --- .../src/opentelemetry/instrumentation/openai_v2/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index d321f5110c..a96649b796 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -230,7 +230,12 @@ def get_llm_request_attributes( GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT ] = response_format + # service_tier can be passed directly or in extra_body (in SDK 1.26.0 it's via extra_body) service_tier = kwargs.get("service_tier") + if service_tier is None: + extra_body = kwargs.get("extra_body") + if isinstance(extra_body, Mapping): + service_tier = extra_body.get("service_tier") attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] = ( service_tier if service_tier != "auto" else None ) From cb7b692243199dcb460853b3e569664b3f2db3fb Mon Sep 17 00:00:00 2001 From: Teja Date: Tue, 18 Nov 2025 17:31:13 -0500 Subject: [PATCH 3/6] Extract service_tier from extra_body when not in kwargs In OpenAI SDK 1.26.0, service_tier is passed via extra_body. Update get_llm_request_attributes to check both kwargs and extra_body for service_tier to support both ways of passing it. --- .../src/opentelemetry/instrumentation/openai_v2/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index d321f5110c..a96649b796 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -230,7 +230,12 @@ def get_llm_request_attributes( GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT ] = response_format + # service_tier can be passed directly or in extra_body (in SDK 1.26.0 it's via extra_body) service_tier = kwargs.get("service_tier") + if service_tier is None: + extra_body = kwargs.get("extra_body") + if isinstance(extra_body, Mapping): + service_tier = extra_body.get("service_tier") attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] = ( service_tier if service_tier != "auto" else None ) From 1a3e87e30406fc94370819bd6566c63b9ca803a0 Mon Sep 17 00:00:00 2001 From: Teja Date: Tue, 18 Nov 2025 17:36:07 -0500 Subject: [PATCH 4/6] Add changelog entry for service tier attribute fix Fix #3920: Add changelog entry documenting the fix for service tier attribute names. --- .../opentelemetry-instrumentation-openai-v2/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md index 9da6770271..0a7aebb9b2 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix service tier attribute names: use `GEN_AI_OPENAI_REQUEST_SERVICE_TIER` for request + attributes and `GEN_AI_OPENAI_RESPONSE_SERVICE_TIER` for response attributes. + ([#3920](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3920)) - Added support for OpenAI embeddings instrumentation ([#3461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3461)) - Record prompt and completion events regardless of span sampling decision. From 80e36f15bbafeefc3d09545bf21f58ea0cba311c Mon Sep 17 00:00:00 2001 From: Teja Date: Wed, 19 Nov 2025 23:24:55 -0500 Subject: [PATCH 5/6] wip: adding checks for service_tier. --- .../tests/test_async_chat_completions.py | 2 ++ .../tests/test_chat_completions.py | 2 ++ .../tests/test_utils.py | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py index f618267e6b..59594c028b 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py @@ -183,6 +183,8 @@ async def test_async_chat_completion_extra_params( response.model, response.usage.prompt_tokens, response.usage.completion_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), ) assert ( spans[0].attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED] == 42 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py index 471d70fcdb..00a3c7c88c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py @@ -221,6 +221,8 @@ def test_chat_completion_extra_params( response.model, response.usage.prompt_tokens, response.usage.completion_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), ) assert ( spans[0].attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED] == 42 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index eb66eb7c7a..c08ce4fdae 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -35,6 +35,8 @@ def assert_all_attributes( operation_name: str = "chat", server_address: str = "api.openai.com", server_port: int = 443, + request_service_tier: Optional[str] = None, + response_service_tier: Optional[str] = None, ): assert span.name == f"{operation_name} {request_model}" assert ( @@ -87,6 +89,28 @@ def assert_all_attributes( if server_port != 443 and server_port > 0: assert server_port == span.attributes[ServerAttributes.SERVER_PORT] + if request_service_tier is not None: + assert ( + request_service_tier + == span.attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] + ) + else: + assert ( + GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER + not in span.attributes + ) + + if response_service_tier is not None: + assert ( + response_service_tier + == span.attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] + ) + else: + assert ( + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER + not in span.attributes + ) + def assert_log_parent(log, span): """Assert that the log record has the correct parent span context""" From e2481fd4e4b118c64bca774e6b3ced5e34898a95 Mon Sep 17 00:00:00 2001 From: Teja Date: Thu, 20 Nov 2025 07:24:36 -0500 Subject: [PATCH 6/6] wip: fix linting errors. --- .../tests/test_utils.py | 83 +++++++------------ 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index c08ce4fdae..d4d64c4027 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -25,6 +25,14 @@ ) +def _assert_optional_attribute(span, attribute_name, expected_value): + """Helper to assert optional span attributes.""" + if expected_value is not None: + assert expected_value == span.attributes[attribute_name] + else: + assert attribute_name not in span.attributes + + def assert_all_attributes( span: ReadableSpan, request_model: str, @@ -51,65 +59,34 @@ def assert_all_attributes( request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] ) - if response_model: - assert ( - response_model - == span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] - ) - else: - assert GenAIAttributes.GEN_AI_RESPONSE_MODEL not in span.attributes - - if response_id: - assert ( - response_id == span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] - ) - else: - assert GenAIAttributes.GEN_AI_RESPONSE_ID not in span.attributes - - if input_tokens: - assert ( - input_tokens - == span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] - ) - else: - assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS not in span.attributes - - if output_tokens: - assert ( - output_tokens - == span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] - ) - else: - assert ( - GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS not in span.attributes - ) + _assert_optional_attribute( + span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model + ) + _assert_optional_attribute( + span, GenAIAttributes.GEN_AI_RESPONSE_ID, response_id + ) + _assert_optional_attribute( + span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, input_tokens + ) + _assert_optional_attribute( + span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + ) assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS] if server_port != 443 and server_port > 0: assert server_port == span.attributes[ServerAttributes.SERVER_PORT] - if request_service_tier is not None: - assert ( - request_service_tier - == span.attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] - ) - else: - assert ( - GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER - not in span.attributes - ) - - if response_service_tier is not None: - assert ( - response_service_tier - == span.attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] - ) - else: - assert ( - GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER - not in span.attributes - ) + _assert_optional_attribute( + span, + GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER, + request_service_tier, + ) + _assert_optional_attribute( + span, + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, + response_service_tier, + ) def assert_log_parent(log, span):