Skip to content

Commit a005fe0

Browse files
authored
fix(openai): responses instrumentation broken traces for async streaming (#3475)
1 parent 083c327 commit a005fe0

File tree

6 files changed

+798
-1
lines changed

6 files changed

+798
-1
lines changed

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ class TracedData(pydantic.BaseModel):
179179
request_service_tier: Optional[str] = pydantic.Field(default=None)
180180
response_service_tier: Optional[str] = pydantic.Field(default=None)
181181

182+
# Trace context - to maintain trace continuity across async operations
183+
trace_context: Any = pydantic.Field(default=None)
184+
185+
class Config:
186+
arbitrary_types_allowed = True
187+
182188

183189
responses: dict[str, TracedData] = {}
184190

@@ -499,10 +505,13 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
499505
try:
500506
response = wrapped(*args, **kwargs)
501507
if isinstance(response, Stream):
508+
# Capture current trace context to maintain trace continuity
509+
ctx = context_api.get_current()
502510
span = tracer.start_span(
503511
SPAN_NAME,
504512
kind=SpanKind.CLIENT,
505513
start_time=start_time,
514+
context=ctx,
506515
)
507516
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
508517

@@ -552,16 +561,22 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
552561
response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
553562
request_service_tier=non_sentinel_kwargs.get("service_tier"),
554563
response_service_tier=existing_data.get("response_service_tier"),
564+
# Capture trace context to maintain continuity
565+
trace_context=existing_data.get("trace_context", context_api.get_current()),
555566
)
556567
except Exception:
557568
traced_data = None
558569

570+
# Restore the original trace context to maintain trace continuity
571+
ctx = (traced_data.trace_context if traced_data and traced_data.trace_context
572+
else context_api.get_current())
559573
span = tracer.start_span(
560574
SPAN_NAME,
561575
kind=SpanKind.CLIENT,
562576
start_time=(
563577
start_time if traced_data is None else int(traced_data.start_time)
564578
),
579+
context=ctx,
565580
)
566581
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
567582
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
@@ -618,16 +633,21 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
618633
response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
619634
request_service_tier=existing_data.get("request_service_tier", non_sentinel_kwargs.get("service_tier")),
620635
response_service_tier=existing_data.get("response_service_tier", parsed_response.service_tier),
636+
# Capture trace context to maintain continuity across async operations
637+
trace_context=existing_data.get("trace_context", context_api.get_current()),
621638
)
622639
responses[parsed_response.id] = traced_data
623640
except Exception:
624641
return response
625642

626643
if parsed_response.status == "completed":
644+
# Restore the original trace context to maintain trace continuity
645+
ctx = traced_data.trace_context if traced_data.trace_context else context_api.get_current()
627646
span = tracer.start_span(
628647
SPAN_NAME,
629648
kind=SpanKind.CLIENT,
630649
start_time=int(traced_data.start_time),
650+
context=ctx,
631651
)
632652
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
633653
set_data_attributes(traced_data, span)
@@ -651,10 +671,13 @@ async def async_responses_get_or_create_wrapper(
651671
try:
652672
response = await wrapped(*args, **kwargs)
653673
if isinstance(response, (Stream, AsyncStream)):
674+
# Capture current trace context to maintain trace continuity
675+
ctx = context_api.get_current()
654676
span = tracer.start_span(
655677
SPAN_NAME,
656678
kind=SpanKind.CLIENT,
657679
start_time=start_time,
680+
context=ctx,
658681
)
659682
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
660683

@@ -700,16 +723,22 @@ async def async_responses_get_or_create_wrapper(
700723
response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
701724
request_service_tier=non_sentinel_kwargs.get("service_tier"),
702725
response_service_tier=existing_data.get("response_service_tier"),
726+
# Capture trace context to maintain continuity
727+
trace_context=existing_data.get("trace_context", context_api.get_current()),
703728
)
704729
except Exception:
705730
traced_data = None
706731

732+
# Restore the original trace context to maintain trace continuity
733+
ctx = (traced_data.trace_context if traced_data and traced_data.trace_context
734+
else context_api.get_current())
707735
span = tracer.start_span(
708736
SPAN_NAME,
709737
kind=SpanKind.CLIENT,
710738
start_time=(
711739
start_time if traced_data is None else int(traced_data.start_time)
712740
),
741+
context=ctx,
713742
)
714743
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
715744
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
@@ -767,16 +796,21 @@ async def async_responses_get_or_create_wrapper(
767796
response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
768797
request_service_tier=existing_data.get("request_service_tier", non_sentinel_kwargs.get("service_tier")),
769798
response_service_tier=existing_data.get("response_service_tier", parsed_response.service_tier),
799+
# Capture trace context to maintain continuity across async operations
800+
trace_context=existing_data.get("trace_context", context_api.get_current()),
770801
)
771802
responses[parsed_response.id] = traced_data
772803
except Exception:
773804
return response
774805

775806
if parsed_response.status == "completed":
807+
# Restore the original trace context to maintain trace continuity
808+
ctx = traced_data.trace_context if traced_data.trace_context else context_api.get_current()
776809
span = tracer.start_span(
777810
SPAN_NAME,
778811
kind=SpanKind.CLIENT,
779812
start_time=int(traced_data.start_time),
813+
context=ctx,
780814
)
781815
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
782816
set_data_attributes(traced_data, span)
@@ -799,11 +833,14 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs):
799833
parsed_response = parse_response(response)
800834
existing_data = responses.pop(parsed_response.id, None)
801835
if existing_data is not None:
836+
# Restore the original trace context to maintain trace continuity
837+
ctx = existing_data.trace_context if existing_data.trace_context else context_api.get_current()
802838
span = tracer.start_span(
803839
SPAN_NAME,
804840
kind=SpanKind.CLIENT,
805841
start_time=existing_data.start_time,
806842
record_exception=True,
843+
context=ctx,
807844
)
808845
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
809846
span.record_exception(Exception("Response cancelled"))
@@ -828,11 +865,14 @@ async def async_responses_cancel_wrapper(
828865
parsed_response = parse_response(response)
829866
existing_data = responses.pop(parsed_response.id, None)
830867
if existing_data is not None:
868+
# Restore the original trace context to maintain trace continuity
869+
ctx = existing_data.trace_context if existing_data.trace_context else context_api.get_current()
831870
span = tracer.start_span(
832871
SPAN_NAME,
833872
kind=SpanKind.CLIENT,
834873
start_time=existing_data.start_time,
835874
record_exception=True,
875+
context=ctx,
836876
)
837877
_set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
838878
span.record_exception(Exception("Response cancelled"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
interactions:
2+
- request:
3+
body: '{"input": "Count to 3", "model": "gpt-4o", "stream": true}'
4+
headers:
5+
accept:
6+
- application/json
7+
accept-encoding:
8+
- gzip, deflate
9+
connection:
10+
- keep-alive
11+
content-length:
12+
- '58'
13+
content-type:
14+
- application/json
15+
host:
16+
- api.openai.com
17+
user-agent:
18+
- AsyncOpenAI/Python 1.99.7
19+
x-stainless-arch:
20+
- arm64
21+
x-stainless-async:
22+
- async:asyncio
23+
x-stainless-lang:
24+
- python
25+
x-stainless-os:
26+
- MacOS
27+
x-stainless-package-version:
28+
- 1.99.7
29+
x-stainless-read-timeout:
30+
- '600'
31+
x-stainless-retry-count:
32+
- '0'
33+
x-stainless-runtime:
34+
- CPython
35+
x-stainless-runtime-version:
36+
- 3.10.16
37+
method: POST
38+
uri: https://api.openai.com/v1/responses
39+
response:
40+
body:
41+
string: 'event: response.created
42+
43+
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0833439a21e2e07f00692744f7343c8194a974c6475b934753","object":"response","created_at":1764181239,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
44+
45+
46+
event: response.in_progress
47+
48+
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0833439a21e2e07f00692744f7343c8194a974c6475b934753","object":"response","created_at":1764181239,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
49+
50+
51+
event: response.output_item.added
52+
53+
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","type":"message","status":"in_progress","content":[],"role":"assistant"}}
54+
55+
56+
event: response.content_part.added
57+
58+
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
59+
60+
61+
event: response.output_text.delta
62+
63+
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":"1","logprobs":[],"obfuscation":"XA0B7nq36y3I0p0"}
64+
65+
66+
event: response.output_text.delta
67+
68+
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Xr3zhqYKv3CQfSO"}
69+
70+
71+
event: response.output_text.delta
72+
73+
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":"
74+
","logprobs":[],"obfuscation":"UVLmTFr7FkA87lB"}
75+
76+
77+
event: response.output_text.delta
78+
79+
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":"2","logprobs":[],"obfuscation":"wPVtjCN3qKdLhJk"}
80+
81+
82+
event: response.output_text.delta
83+
84+
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"7ajjiAJKL5KDRn7"}
85+
86+
87+
event: response.output_text.delta
88+
89+
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":"
90+
","logprobs":[],"obfuscation":"mJIUyOlMT0CuNgP"}
91+
92+
93+
event: response.output_text.delta
94+
95+
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"delta":"3","logprobs":[],"obfuscation":"tkvYO2ofKRqiMo2"}
96+
97+
98+
event: response.output_text.done
99+
100+
data: {"type":"response.output_text.done","sequence_number":11,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"text":"1,
101+
2, 3","logprobs":[]}
102+
103+
104+
event: response.content_part.done
105+
106+
data: {"type":"response.content_part.done","sequence_number":12,"item_id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"1,
107+
2, 3"}}
108+
109+
110+
event: response.output_item.done
111+
112+
data: {"type":"response.output_item.done","sequence_number":13,"output_index":0,"item":{"id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"1,
113+
2, 3"}],"role":"assistant"}}
114+
115+
116+
event: response.completed
117+
118+
data: {"type":"response.completed","sequence_number":14,"response":{"id":"resp_0833439a21e2e07f00692744f7343c8194a974c6475b934753","object":"response","created_at":1764181239,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[{"id":"msg_0833439a21e2e07f00692744f7989c81949dceeaed14d8e652","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"1,
119+
2, 3"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cached_tokens":0},"output_tokens":8,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":19},"user":null,"metadata":{}}}
120+
121+
122+
'
123+
headers:
124+
CF-RAY:
125+
- 9a4b66a84caf0dd4-TLV
126+
Connection:
127+
- keep-alive
128+
Content-Type:
129+
- text/event-stream; charset=utf-8
130+
Date:
131+
- Wed, 26 Nov 2025 18:20:39 GMT
132+
Server:
133+
- cloudflare
134+
Set-Cookie:
135+
- __cf_bm=V.B2dWw7Yqqxcb1uimxGCTcSgcx.eZynXeAnI8MdqrY-1764181239-1.0.1.1-fLTkIx0eOASBJaTgLguVlUcPeXfGdMzkMTJLHFk_x1Cp16J1_CVnZQv28zh4n2kXaZhhKmaa.rU.jVIHjsuftLTTbR8d2NsEO_GRuM9Oe3k;
136+
path=/; expires=Wed, 26-Nov-25 18:50:39 GMT; domain=.api.openai.com; HttpOnly;
137+
Secure; SameSite=None
138+
- _cfuvid=OlLe323e8JzelzLOlibVBzWl3.rFDoa53lmaaBwwA4Y-1764181239310-0.0.1.1-604800000;
139+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
140+
Strict-Transport-Security:
141+
- max-age=31536000; includeSubDomains; preload
142+
Transfer-Encoding:
143+
- chunked
144+
X-Content-Type-Options:
145+
- nosniff
146+
alt-svc:
147+
- h3=":443"; ma=86400
148+
cf-cache-status:
149+
- DYNAMIC
150+
openai-organization:
151+
- traceloop
152+
openai-processing-ms:
153+
- '31'
154+
openai-project:
155+
- proj_tzz1TbPPOXaf6j9tEkVUBIAa
156+
openai-version:
157+
- '2020-10-01'
158+
x-envoy-upstream-service-time:
159+
- '36'
160+
x-request-id:
161+
- req_e541f460fc054ca5b778bb69c39095e9
162+
status:
163+
code: 200
164+
message: OK
165+
version: 1

0 commit comments

Comments
 (0)