Skip to content

Commit d27975d

Browse files
lievanhappynancee
authored andcommitted
fix(llmobs): fix parsing stream when response.completed chunk is missing (#13850)
The openai responses api stream returns any of the following chunk types that have `response` field. - `response.completed`, `response.incomplete`, `response.failed`, `response.created`, `response.in_progress` https://platform.openai.com/docs/api-reference/responses-streaming/response The `response` field is updated when each chunk arrives, so we always want to grab the _latest_ one to read data for setting attributes on our llm obs span. However, currently, we are only accounting for `response.completed`. This actually results in an `IndexError` when the `response.completed` chunk is not present. This pr updates the logic always use the latest `response` field of the streamed chunks so we support all the chunk types. ### Note We bump the test agent version so that it supports our new vcr proxy logic introduced [here](DataDog/dd-apm-test-agent@9349315) ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent c0a01de commit d27975d

File tree

5 files changed

+276
-4
lines changed

5 files changed

+276
-4
lines changed

ddtrace/contrib/internal/openai/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ def _loop_handler(span, chunk, streamed_chunks):
277277
span.set_tag_str("openai.response.model", model)
278278

279279
response = getattr(chunk, "response", None)
280-
if getattr(chunk, "type", "") == "response.completed":
281-
streamed_chunks[0].append(response)
280+
if response is not None:
281+
streamed_chunks[0].insert(0, response)
282282

283283
# Completions/chat completions are returned as `choices`
284284
for choice in getattr(chunk, "choices", []):
@@ -292,7 +292,7 @@ def _process_finished_stream(integration, span, kwargs, streamed_chunks, operati
292292
request_messages = kwargs.get("messages", None)
293293
try:
294294
if operation_type == "response":
295-
formatted_completions = streamed_chunks[0][0]
295+
formatted_completions = streamed_chunks[0][0] if streamed_chunks and streamed_chunks[0] else None
296296
elif operation_type == "completion":
297297
formatted_completions = [
298298
openai_construct_completion_from_streamed_chunks(choice) for choice in streamed_chunks

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,4 @@ services:
228228
- "127.0.0.1:7005:7005"
229229

230230
volumes:
231-
ddagent:
231+
ddagent:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
LLM Observability: This fix resolves an issue where incomplete streamed responses returned from OpenAI responses API caused an index error with LLM Observability tracing.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
interactions:
2+
- request:
3+
body: '{"input":"Give me a multi paragraph narrative on the life of a car","max_output_tokens":16,"model":"gpt-4o","stream":true,"temperature":0.1}'
4+
headers:
5+
accept:
6+
- application/json
7+
accept-encoding:
8+
- gzip, deflate
9+
connection:
10+
- keep-alive
11+
content-length:
12+
- '140'
13+
content-type:
14+
- application/json
15+
host:
16+
- api.openai.com
17+
user-agent:
18+
- OpenAI/Python 1.91.0
19+
x-stainless-arch:
20+
- arm64
21+
x-stainless-async:
22+
- 'false'
23+
x-stainless-lang:
24+
- python
25+
x-stainless-os:
26+
- MacOS
27+
x-stainless-package-version:
28+
- 1.91.0
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.13
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_6866d2dec420819c92534dcf75e475120847a84b87aad89c","object":"response","created_at":1751569118,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"service_tier":"auto","store":false,"temperature":0.1,"text":{"format":{"type":"text"}},"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_6866d2dec420819c92534dcf75e475120847a84b87aad89c","object":"response","created_at":1751569118,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"service_tier":"auto","store":false,"temperature":0.1,"text":{"format":{"type":"text"}},"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_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","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_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","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_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"In","logprobs":[]}
64+
65+
66+
event: response.output_text.delta
67+
68+
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
69+
the","logprobs":[]}
70+
71+
72+
event: response.output_text.delta
73+
74+
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
75+
bustling","logprobs":[]}
76+
77+
78+
event: response.output_text.delta
79+
80+
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
81+
city","logprobs":[]}
82+
83+
84+
event: response.output_text.delta
85+
86+
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
87+
of","logprobs":[]}
88+
89+
90+
event: response.output_text.delta
91+
92+
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
93+
Detroit","logprobs":[]}
94+
95+
96+
event: response.output_text.delta
97+
98+
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":",","logprobs":[]}
99+
100+
101+
event: response.output_text.delta
102+
103+
data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
104+
a","logprobs":[]}
105+
106+
107+
event: response.output_text.delta
108+
109+
data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
110+
sleek","logprobs":[]}
111+
112+
113+
event: response.output_text.delta
114+
115+
data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":",","logprobs":[]}
116+
117+
118+
event: response.output_text.delta
119+
120+
data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
121+
metallic","logprobs":[]}
122+
123+
124+
event: response.output_text.delta
125+
126+
data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
127+
blue","logprobs":[]}
128+
129+
130+
event: response.output_text.delta
131+
132+
data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
133+
sedan","logprobs":[]}
134+
135+
136+
event: response.output_text.delta
137+
138+
data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
139+
rolled","logprobs":[]}
140+
141+
142+
event: response.output_text.delta
143+
144+
data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
145+
off","logprobs":[]}
146+
147+
148+
event: response.output_text.delta
149+
150+
data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"delta":"
151+
the","logprobs":[]}
152+
153+
154+
event: response.output_text.done
155+
156+
data: {"type":"response.output_text.done","sequence_number":20,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"text":"In
157+
the bustling city of Detroit, a sleek, metallic blue sedan rolled off the","logprobs":[]}
158+
159+
160+
event: response.content_part.done
161+
162+
data: {"type":"response.content_part.done","sequence_number":21,"item_id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"In
163+
the bustling city of Detroit, a sleek, metallic blue sedan rolled off the"}}
164+
165+
166+
event: response.output_item.done
167+
168+
data: {"type":"response.output_item.done","sequence_number":22,"output_index":0,"item":{"id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In
169+
the bustling city of Detroit, a sleek, metallic blue sedan rolled off the"}],"role":"assistant"}}
170+
171+
172+
event: response.incomplete
173+
174+
data: {"type":"response.incomplete","sequence_number":23,"response":{"id":"resp_6866d2dec420819c92534dcf75e475120847a84b87aad89c","object":"response","created_at":1751569118,"status":"incomplete","background":false,"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[{"id":"msg_6866d2df1c0c819c885ca369ba853e7f0847a84b87aad89c","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In
175+
the bustling city of Detroit, a sleek, metallic blue sedan rolled off the"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"service_tier":"default","store":false,"temperature":0.1,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0},"user":null,"metadata":{}}}
176+
177+
178+
'
179+
headers:
180+
CF-RAY:
181+
- 95989d900ebc15c7-EWR
182+
Connection:
183+
- keep-alive
184+
Content-Type:
185+
- text/event-stream; charset=utf-8
186+
Date:
187+
- Thu, 03 Jul 2025 18:58:38 GMT
188+
Server:
189+
- cloudflare
190+
Set-Cookie:
191+
- __cf_bm=KpYG1oRKkQLyyjVvJMwoM7ql79VaamURrWrTFo.aTE0-1751569118-1.0.1.1-KGbIEiERemwjKrv8ycZmhv0cRJOMMKhFVJv.o0u1rPJqtnugj.3FF_6iz78OARuG5mZZ0ohKG5geXbCQ3uC9eZ0wEn0sBFq3X2qgV5BUl44;
192+
path=/; expires=Thu, 03-Jul-25 19:28:38 GMT; domain=.api.openai.com; HttpOnly;
193+
Secure; SameSite=None
194+
- _cfuvid=r8PrX5IPe5I9EgCb9rpkzUITgLbT65.Q573NUBXW_iY-1751569118795-0.0.1.1-604800000;
195+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
196+
Transfer-Encoding:
197+
- chunked
198+
X-Content-Type-Options:
199+
- nosniff
200+
alt-svc:
201+
- h3=":443"; ma=86400
202+
cf-cache-status:
203+
- DYNAMIC
204+
openai-organization:
205+
- datadog-staging
206+
openai-processing-ms:
207+
- '27'
208+
openai-version:
209+
- '2020-10-01'
210+
strict-transport-security:
211+
- max-age=31536000; includeSubDomains; preload
212+
x-request-id:
213+
- req_e5f20714325d29dc7b7e40b8a587ff3d
214+
status:
215+
code: 200
216+
message: OK
217+
version: 1

tests/contrib/openai/test_openai_llmobs.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,57 @@ def test_response_stream_tokens(self, openai, mock_llmobs_writer, mock_tracer):
11201120
)
11211121
)
11221122

1123+
@pytest.mark.skipif(
1124+
parse_version(openai_module.version.VERSION) < (1, 66), reason="Response options only available openai >= 1.66"
1125+
)
1126+
def test_response_stream_incomplete(self, openai, mock_llmobs_writer, mock_tracer):
1127+
client = openai.OpenAI()
1128+
request_args = {
1129+
"model": "gpt-4o",
1130+
"max_output_tokens": 16,
1131+
"temperature": 0.1,
1132+
"stream": True,
1133+
}
1134+
with get_openai_vcr(subdirectory_name="v1").use_cassette("response_stream_incomplete.yaml"):
1135+
resp1 = client.responses.create(
1136+
input="Give me a multi paragraph narrative on the life of a car",
1137+
**request_args,
1138+
)
1139+
for chunk in resp1:
1140+
if hasattr(chunk, "response") and hasattr(chunk.response, "model"):
1141+
resp_model = chunk.response.model
1142+
span = mock_tracer.pop_traces()[0][0]
1143+
assert mock_llmobs_writer.enqueue.call_count == 1
1144+
mock_llmobs_writer.enqueue.assert_called_with(
1145+
_expected_llmobs_llm_span_event(
1146+
span,
1147+
model_name=resp_model,
1148+
model_provider="openai",
1149+
input_messages=[
1150+
{"content": "Give me a multi paragraph narrative on the life of a car", "role": "user"}
1151+
],
1152+
output_messages=[
1153+
{
1154+
"role": "assistant",
1155+
"content": "In the bustling city of Detroit, a sleek, metallic blue sedan rolled off the",
1156+
}
1157+
],
1158+
metadata={
1159+
"max_output_tokens": 16,
1160+
"temperature": 0.1,
1161+
"stream": True,
1162+
"top_p": 1.0,
1163+
"tools": [],
1164+
"tool_choice": "auto",
1165+
"truncation": "disabled",
1166+
"text": {"format": {"type": "text"}},
1167+
"reasoning_tokens": 0,
1168+
},
1169+
token_metrics={"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
1170+
tags={"ml_app": "<ml-app-name>", "service": "tests.contrib.openai"},
1171+
)
1172+
)
1173+
11231174
@pytest.mark.skipif(
11241175
parse_version(openai_module.version.VERSION) < (1, 66), reason="Response options only available openai >= 1.66"
11251176
)

0 commit comments

Comments
 (0)