From fbe00c651c75503d1609a5b4489eb1f10b90d4cf Mon Sep 17 00:00:00 2001 From: Gal Kleinman Date: Tue, 25 Nov 2025 18:27:18 +0200 Subject: [PATCH 1/2] fix(openai): report request attributes in responses API instrumentation --- .../openai/v1/responses_wrappers.py | 42 +++++++++++++++++-- .../tests/traces/test_responses.py | 25 +++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py index 1c3d78d0bb..8971cd1e81 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py @@ -51,6 +51,8 @@ from typing_extensions import NotRequired from opentelemetry.instrumentation.openai.shared import ( + _extract_model_name_from_provider_format, + _set_request_attributes, _set_span_attribute, model_as_dict, ) @@ -189,12 +191,28 @@ def process_content_block( @dont_throw +def prepare_kwargs_for_shared_attributes(kwargs): + """ + Prepare kwargs for the shared _set_request_attributes function. + Maps responses API specific parameters to the common format. + """ + prepared_kwargs = kwargs.copy() + + # Map max_output_tokens to max_tokens for the shared function + if "max_output_tokens" in kwargs: + prepared_kwargs["max_tokens"] = kwargs["max_output_tokens"] + + return prepared_kwargs + + def set_data_attributes(traced_response: TracedData, span: Span): - _set_span_attribute(span, GenAIAttributes.GEN_AI_SYSTEM, "openai") - _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, traced_response.request_model) + # Response-specific attributes (request attributes are set by _set_request_attributes) _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, traced_response.response_id) - _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, traced_response.response_model) - _set_span_attribute(span, OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER, traced_response.request_service_tier) + + # Extract model name from provider format (e.g., 'openai/gpt-4' -> 'gpt-4') + response_model = _extract_model_name_from_provider_format(traced_response.response_model) + _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model) + _set_span_attribute(span, OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER, traced_response.response_service_tier) if usage := traced_response.usage: _set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) @@ -445,6 +463,8 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa kind=SpanKind.CLIENT, start_time=start_time, ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) return ResponseStream( span=span, @@ -503,6 +523,8 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa start_time if traced_data is None else int(traced_data.start_time) ), ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) span.set_status(StatusCode.ERROR, str(e)) @@ -568,6 +590,8 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa kind=SpanKind.CLIENT, start_time=int(traced_data.start_time), ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) set_data_attributes(traced_data, span) span.end() @@ -591,6 +615,8 @@ async def async_responses_get_or_create_wrapper( kind=SpanKind.CLIENT, start_time=start_time, ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) return ResponseStream( span=span, @@ -645,6 +671,8 @@ async def async_responses_get_or_create_wrapper( start_time if traced_data is None else int(traced_data.start_time) ), ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) span.set_status(StatusCode.ERROR, str(e)) @@ -711,6 +739,8 @@ async def async_responses_get_or_create_wrapper( kind=SpanKind.CLIENT, start_time=int(traced_data.start_time), ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) set_data_attributes(traced_data, span) span.end() @@ -735,6 +765,8 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs): start_time=existing_data.start_time, record_exception=True, ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.record_exception(Exception("Response cancelled")) set_data_attributes(existing_data, span) span.end() @@ -761,6 +793,8 @@ async def async_responses_cancel_wrapper( start_time=existing_data.start_time, record_exception=True, ) + # Set request attributes using shared logic + _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.record_exception(Exception("Response cancelled")) set_data_attributes(existing_data, span) span.end() diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py index 043ecccf68..85a0dd5028 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py @@ -29,6 +29,31 @@ def test_responses( # assert span.attributes["gen_ai.prompt.0.role"] == "user" +@pytest.mark.vcr +def test_responses_with_request_params( + instrument_legacy, span_exporter: InMemorySpanExporter, openai_client: OpenAI +): + """Test that request parameters like temperature, max_tokens, top_p are captured""" + _ = openai_client.responses.create( + model="gpt-4.1-nano", + input="What is the capital of France?", + temperature=0.7, + max_output_tokens=100, + top_p=0.9, + ) + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "openai.response" + assert span.attributes["gen_ai.system"] == "openai" + assert span.attributes["gen_ai.request.model"] == "gpt-4.1-nano" + + # Check that request parameters are captured + assert span.attributes["gen_ai.request.temperature"] == 0.7 + assert span.attributes["gen_ai.request.max_tokens"] == 100 + assert span.attributes["gen_ai.request.top_p"] == 0.9 + + @pytest.mark.vcr def test_responses_with_service_tier( instrument_legacy, span_exporter: InMemorySpanExporter, openai_client: OpenAI From c739dad2b31b6dd89ff545c7b0841d5d9c9043c4 Mon Sep 17 00:00:00 2001 From: Gal Kleinman Date: Tue, 25 Nov 2025 18:31:52 +0200 Subject: [PATCH 2/2] remove comments + add missing file --- .../openai/v1/responses_wrappers.py | 10 -- .../test_responses_with_request_params.yaml | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_responses/test_responses_with_request_params.yaml diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py index 8971cd1e81..f14572439b 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py @@ -206,10 +206,8 @@ def prepare_kwargs_for_shared_attributes(kwargs): def set_data_attributes(traced_response: TracedData, span: Span): - # Response-specific attributes (request attributes are set by _set_request_attributes) _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, traced_response.response_id) - # Extract model name from provider format (e.g., 'openai/gpt-4' -> 'gpt-4') response_model = _extract_model_name_from_provider_format(traced_response.response_model) _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model) @@ -463,7 +461,6 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa kind=SpanKind.CLIENT, start_time=start_time, ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) return ResponseStream( @@ -523,7 +520,6 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa start_time if traced_data is None else int(traced_data.start_time) ), ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) @@ -590,7 +586,6 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa kind=SpanKind.CLIENT, start_time=int(traced_data.start_time), ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) set_data_attributes(traced_data, span) span.end() @@ -615,7 +610,6 @@ async def async_responses_get_or_create_wrapper( kind=SpanKind.CLIENT, start_time=start_time, ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) return ResponseStream( @@ -671,7 +665,6 @@ async def async_responses_get_or_create_wrapper( start_time if traced_data is None else int(traced_data.start_time) ), ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.set_attribute(ERROR_TYPE, e.__class__.__name__) span.record_exception(e) @@ -739,7 +732,6 @@ async def async_responses_get_or_create_wrapper( kind=SpanKind.CLIENT, start_time=int(traced_data.start_time), ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) set_data_attributes(traced_data, span) span.end() @@ -765,7 +757,6 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs): start_time=existing_data.start_time, record_exception=True, ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.record_exception(Exception("Response cancelled")) set_data_attributes(existing_data, span) @@ -793,7 +784,6 @@ async def async_responses_cancel_wrapper( start_time=existing_data.start_time, record_exception=True, ) - # Set request attributes using shared logic _set_request_attributes(span, prepare_kwargs_for_shared_attributes(kwargs), instance) span.record_exception(Exception("Response cancelled")) set_data_attributes(existing_data, span) diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_responses/test_responses_with_request_params.yaml b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_responses/test_responses_with_request_params.yaml new file mode 100644 index 0000000000..ae44543b75 --- /dev/null +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_responses/test_responses_with_request_params.yaml @@ -0,0 +1,111 @@ +interactions: +- request: + body: '{"input": "What is the capital of France?", "max_output_tokens": 100, "model": + "gpt-4.1-nano", "temperature": 0.7, "top_p": 0.9}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.99.7 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.99.7 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.16 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA3RUwW7bMAy95ysEnZtCdp3YyQfsvMNuxWDQNp1qlUVBooIGRf59sBw78dbeLD7y + mXyP0udGCKk7eRTSY3C12pVlh6qBBiqV56jU/pDvurLEw65VVXbYVf2havt90eUNFi/7Tj6NFNT8 + wZZnGrIBp3jrERi7GkYsK/eFqsp9WSUsMHAMY01LgzPIeCNroH0/eYp27KsHE3AKa2O0Pcmj+NwI + IYR0cEE/1nd4RkMOvdwIcU3J6D2NmI3GpIC281/qDhm0CWs0sI8ta7Kr+AAfNUV2kWumd0xgptSC + MZGpWzBrtoE6NGNjJ8fb4jnbWrC0zVW+26pimxU3zRKvPIrXNM401GLHEE7fulGpbJ9VyQ1V7opc + ZX3evFSwqxJzYuGLw8SDIcAJ78B3siewJcto7009NrainUXBD16qUwJYSwyzkK+/V6Chk/PUfIEk + oqOQv95QtOA0gxHUix8ebItCB/ETvA7Pcqm53r4WGunJpNYgBB0YLE/JY2JKkg48GINm7Rr7OO2X + 83jWFEM9r3CdnFhcdZ4Gx3UL7RvW73j5FvM4aqjJPmZ4hEB2tb/Y9+T5IWl0Jw4D+Jl7WecAPfKl + 1t1I3GtcrXZAf9Yt1qzn69BDNJMvMjB5fByTcXDogWMKq+fyFk363zrryQ9wPz/4nvImXW8dn9E3 + FDRfpm3rdBzu13BS+o10O1kTmeQC3NdAMrn6YTnUEnSpx8N09tG2cBNWdjpAY+Y3I6YlXwbQdn1l + i6f/4w/vwDJmMrC7F6rVqP++BNVX8a9oF/O/Y2ZiMHcwzxcFY1ibPSBDBwwj/XVz/QsAAP//AwDZ + CKVDwQUAAA== + headers: + CF-RAY: + - 9a427a739fc97d95-TLV + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Nov 2025 16:21:20 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=972AABf9T_oEadbDKJ76kKF9Af2tTgOaTdaX4cxL8kg-1764087680-1.0.1.1-3xsObn4FVX3Nmp7VNdniyOmOOdjB3JnvdcHUEpHKZqNX2Q6j1k9MbrU0lGYlUVLrVb2Ls0gopzNAe6FA5AiAIvLVTtFzLT2780nDD_KBqxM; + path=/; expires=Tue, 25-Nov-25 16:51:20 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=w6zJyOCR9tAsFojGun.7hYM9_l7GfWdALH5Vd04TmeQ-1764087680812-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - traceloop + openai-processing-ms: + - '2149' + openai-project: + - proj_tzz1TbPPOXaf6j9tEkVUBIAa + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '2151' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999967' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_500c9ae7e13b4845be386000962eb7fd + status: + code: 200 + message: OK +version: 1