Skip to content

Commit 3e7904f

Browse files
authored
openai-v2: handle with_raw_response streaming (#4033)
* openai-v2: handle with_raw_response streaming * Add changelog * Add missing vcr recording
1 parent d0d895d commit 3e7904f

File tree

6 files changed

+261
-1
lines changed

6 files changed

+261
-1
lines changed

instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
([#4017](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4017))
1212
- Add support for chat completions choice count and stop sequences span attributes
1313
([#4028](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4028))
14+
- Fix crash with streaming `with_raw_response`
15+
([#4033](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4033))
1416

1517
## Version 2.2b0 (2025-11-25)
1618

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,3 +701,7 @@ def process_chunk(self, chunk):
701701
self.set_response_service_tier(chunk)
702702
self.build_streaming_response(chunk)
703703
self.set_usage(chunk)
704+
705+
def parse(self):
706+
"""Called when using with_raw_response with stream=True"""
707+
return self

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_chat_completion_with_raw_repsonse.yaml renamed to instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_chat_completion_with_raw_response.yaml

File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"messages": [
6+
{
7+
"role": "user",
8+
"content": "Say this is a test"
9+
}
10+
],
11+
"model": "gpt-4o-mini",
12+
"stream": true,
13+
"stream_options": {
14+
"include_usage": true
15+
}
16+
}
17+
headers:
18+
Accept:
19+
- application/json
20+
Accept-Encoding:
21+
- gzip, deflate
22+
Connection:
23+
- keep-alive
24+
Content-Length:
25+
- '148'
26+
Content-Type:
27+
- application/json
28+
Host:
29+
- api.openai.com
30+
User-Agent:
31+
- AsyncOpenAI/Python 1.109.1
32+
X-Stainless-Arch:
33+
- x64
34+
X-Stainless-Async:
35+
- async:asyncio
36+
X-Stainless-Lang:
37+
- python
38+
X-Stainless-OS:
39+
- Linux
40+
X-Stainless-Package-Version:
41+
- 1.109.1
42+
X-Stainless-Raw-Response:
43+
- 'true'
44+
X-Stainless-Runtime:
45+
- CPython
46+
X-Stainless-Runtime-Version:
47+
- 3.12.12
48+
authorization:
49+
- Bearer test_openai_api_key
50+
x-stainless-read-timeout:
51+
- '600'
52+
x-stainless-retry-count:
53+
- '0'
54+
method: POST
55+
uri: https://api.openai.com/v1/chat/completions
56+
response:
57+
body:
58+
string: |+
59+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v3JkrR4kf"}
60+
61+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"c2Yj6Tq"}
62+
63+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vYP94Gjb"}
64+
65+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"axQhTg4rR"}
66+
67+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Sd4wYC"}
68+
69+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZufRh78gtk"}
70+
71+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Si8PiPK"}
72+
73+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DtALgOW"}
74+
75+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xYwawpnRk"}
76+
77+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Swpx"}
78+
79+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"L6Pd0pV"}
80+
81+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Viytd"}
82+
83+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LqmsdvgjP8"}
84+
85+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"vbpp9"}
86+
87+
data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":12,"total_tokens":24,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"xEbQODa0Ga"}
88+
89+
data: [DONE]
90+
91+
headers:
92+
CF-RAY:
93+
- 9aed6932dfb8ed9e-MXP
94+
Connection:
95+
- keep-alive
96+
Content-Type:
97+
- text/event-stream; charset=utf-8
98+
Date:
99+
- Tue, 16 Dec 2025 10:13:56 GMT
100+
Server:
101+
- cloudflare
102+
Set-Cookie: test_set_cookie
103+
Strict-Transport-Security:
104+
- max-age=31536000; includeSubDomains; preload
105+
Transfer-Encoding:
106+
- chunked
107+
X-Content-Type-Options:
108+
- nosniff
109+
access-control-expose-headers:
110+
- X-Request-ID
111+
alt-svc:
112+
- h3=":443"; ma=86400
113+
cf-cache-status:
114+
- DYNAMIC
115+
openai-organization: test_openai_org_id
116+
openai-processing-ms:
117+
- '228'
118+
openai-project:
119+
- proj_Pf1eM5R55Z35wBy4rt8PxAGq
120+
openai-version:
121+
- '2020-10-01'
122+
x-envoy-upstream-service-time:
123+
- '241'
124+
x-openai-proxy-wasm:
125+
- v0.1
126+
x-ratelimit-limit-requests:
127+
- '10000'
128+
x-ratelimit-limit-tokens:
129+
- '10000000'
130+
x-ratelimit-remaining-requests:
131+
- '9999'
132+
x-ratelimit-remaining-tokens:
133+
- '9999993'
134+
x-ratelimit-reset-requests:
135+
- 6ms
136+
x-ratelimit-reset-tokens:
137+
- 0s
138+
x-request-id:
139+
- req_279d1848f0cf450dbffc9d7776f157f7
140+
status:
141+
code: 200
142+
message: OK
143+
version: 1

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,63 @@ async def test_async_chat_completion_with_raw_repsonse(
299299
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0])
300300

301301

302+
@pytest.mark.vcr()
303+
@pytest.mark.asyncio()
304+
async def test_chat_completion_with_raw_response_streaming(
305+
span_exporter, log_exporter, async_openai_client, instrument_with_content
306+
):
307+
llm_model_value = "gpt-4o-mini"
308+
messages_value = [{"role": "user", "content": "Say this is a test"}]
309+
raw_response = (
310+
await async_openai_client.chat.completions.with_raw_response.create(
311+
messages=messages_value,
312+
model=llm_model_value,
313+
stream=True,
314+
stream_options={"include_usage": True},
315+
)
316+
)
317+
response = raw_response.parse()
318+
319+
message_content = ""
320+
async for chunk in response:
321+
if chunk.choices:
322+
message_content += chunk.choices[0].delta.content or ""
323+
# get the last chunk
324+
if getattr(chunk, "usage", None):
325+
response_stream_usage = chunk.usage
326+
response_stream_model = chunk.model
327+
response_stream_id = chunk.id
328+
329+
spans = span_exporter.get_finished_spans()
330+
assert_all_attributes(
331+
spans[0],
332+
llm_model_value,
333+
response_stream_id,
334+
response_stream_model,
335+
response_stream_usage.prompt_tokens,
336+
response_stream_usage.completion_tokens,
337+
response_service_tier="default",
338+
)
339+
340+
logs = log_exporter.get_finished_logs()
341+
assert len(logs) == 2
342+
343+
user_message = {"content": messages_value[0]["content"]}
344+
assert_message_in_logs(
345+
logs[0], "gen_ai.user.message", user_message, spans[0]
346+
)
347+
348+
choice_event = {
349+
"index": 0,
350+
"finish_reason": "stop",
351+
"message": {
352+
"role": "assistant",
353+
"content": message_content,
354+
},
355+
}
356+
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0])
357+
358+
302359
@pytest.mark.vcr()
303360
@pytest.mark.asyncio()
304361
async def test_async_chat_completion_tool_calls_with_content(

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ def test_chat_completion_multiple_choices(
412412

413413

414414
@pytest.mark.vcr()
415-
def test_chat_completion_with_raw_repsonse(
415+
def test_chat_completion_with_raw_response(
416416
span_exporter, log_exporter, openai_client, instrument_with_content
417417
):
418418
llm_model_value = "gpt-4o-mini"
@@ -451,6 +451,60 @@ def test_chat_completion_with_raw_repsonse(
451451
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0])
452452

453453

454+
@pytest.mark.vcr()
455+
def test_chat_completion_with_raw_response_streaming(
456+
span_exporter, log_exporter, openai_client, instrument_with_content
457+
):
458+
llm_model_value = "gpt-4o-mini"
459+
messages_value = [{"role": "user", "content": "Say this is a test"}]
460+
raw_response = openai_client.chat.completions.with_raw_response.create(
461+
messages=messages_value,
462+
model=llm_model_value,
463+
stream=True,
464+
stream_options={"include_usage": True},
465+
)
466+
response = raw_response.parse()
467+
468+
message_content = ""
469+
for chunk in response:
470+
if chunk.choices:
471+
message_content += chunk.choices[0].delta.content or ""
472+
# get the last chunk
473+
if getattr(chunk, "usage", None):
474+
response_stream_usage = chunk.usage
475+
response_stream_model = chunk.model
476+
response_stream_id = chunk.id
477+
478+
spans = span_exporter.get_finished_spans()
479+
assert_all_attributes(
480+
spans[0],
481+
llm_model_value,
482+
response_stream_id,
483+
response_stream_model,
484+
response_stream_usage.prompt_tokens,
485+
response_stream_usage.completion_tokens,
486+
response_service_tier="default",
487+
)
488+
489+
logs = log_exporter.get_finished_logs()
490+
assert len(logs) == 2
491+
492+
user_message = {"content": messages_value[0]["content"]}
493+
assert_message_in_logs(
494+
logs[0], "gen_ai.user.message", user_message, spans[0]
495+
)
496+
497+
choice_event = {
498+
"index": 0,
499+
"finish_reason": "stop",
500+
"message": {
501+
"role": "assistant",
502+
"content": message_content,
503+
},
504+
}
505+
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0])
506+
507+
454508
@pytest.mark.vcr()
455509
def test_chat_completion_tool_calls_with_content(
456510
span_exporter, log_exporter, openai_client, instrument_with_content

0 commit comments

Comments
 (0)