Skip to content

Commit a764fb0

Browse files
committed
fix(openai): handle Azure stream chunks without delta
1 parent fdcecb7 commit a764fb0

2 files changed

Lines changed: 103 additions & 3 deletions

File tree

langfuse/openai.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,9 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any:
640640
chunk = chunk.__dict__
641641

642642
model = model or chunk.get("model", None) or None
643-
usage = chunk.get("usage", None)
643+
chunk_usage = chunk.get("usage", None)
644+
if chunk_usage is not None:
645+
usage = chunk_usage
644646

645647
choices = chunk.get("choices", [])
646648

@@ -649,11 +651,16 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any:
649651
choice = choice.__dict__
650652
if resource.type == "chat":
651653
delta = choice.get("delta", None)
652-
finish_reason = choice.get("finish_reason", None)
654+
choice_finish_reason = choice.get("finish_reason", None)
655+
if choice_finish_reason is not None:
656+
finish_reason = choice_finish_reason
653657

654-
if _is_openai_v1():
658+
if _is_openai_v1() and delta is not None:
655659
delta = delta.__dict__
656660

661+
if delta is None:
662+
delta = {}
663+
657664
if delta.get("role", None) is not None:
658665
completion["role"] = delta["role"]
659666

tests/unit/test_openai.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,64 @@ def _make_chat_stream_chunks():
101101
]
102102

103103

104+
def _make_chat_stream_chunks_with_trailing_content_filter_chunk():
105+
usage = SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4)
106+
107+
return [
108+
SimpleNamespace(
109+
model="gpt-4o-mini",
110+
choices=[
111+
SimpleNamespace(
112+
delta=SimpleNamespace(
113+
role="assistant",
114+
content="2",
115+
function_call=None,
116+
tool_calls=None,
117+
),
118+
finish_reason=None,
119+
)
120+
],
121+
usage=None,
122+
),
123+
SimpleNamespace(
124+
model="gpt-4o-mini",
125+
choices=[
126+
SimpleNamespace(
127+
delta=SimpleNamespace(
128+
role=None,
129+
content=None,
130+
function_call=None,
131+
tool_calls=None,
132+
),
133+
finish_reason="stop",
134+
)
135+
],
136+
usage=usage,
137+
),
138+
SimpleNamespace(
139+
model="",
140+
choices=[
141+
SimpleNamespace(
142+
delta=None,
143+
finish_reason=None,
144+
content_filter_offsets={
145+
"check_offset": 44,
146+
"start_offset": 44,
147+
"end_offset": 121,
148+
},
149+
content_filter_results={
150+
"hate": {"filtered": False, "severity": "safe"},
151+
"self_harm": {"filtered": False, "severity": "safe"},
152+
"sexual": {"filtered": False, "severity": "safe"},
153+
"violence": {"filtered": False, "severity": "safe"},
154+
},
155+
)
156+
],
157+
usage=None,
158+
),
159+
]
160+
161+
104162
def _make_single_chunk_stream():
105163
return SimpleNamespace(
106164
model="gpt-4o-mini",
@@ -315,6 +373,41 @@ def test_openai_stream_preserves_original_stream_contract(
315373
}
316374

317375

376+
def test_openai_stream_handles_trailing_azure_content_filter_chunk(
377+
langfuse_memory_client, get_span, json_attr
378+
):
379+
openai_client = lf_openai.OpenAI(api_key="test")
380+
raw_stream = DummyOpenAIStream(
381+
_make_chat_stream_chunks_with_trailing_content_filter_chunk(),
382+
DummySyncResponse(),
383+
)
384+
385+
with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream):
386+
stream = openai_client.chat.completions.create(
387+
name="unit-openai-native-stream-azure-filter",
388+
model="gpt-4o-mini",
389+
messages=[{"role": "user", "content": "1 + 1 = ?"}],
390+
temperature=0,
391+
stream=True,
392+
)
393+
394+
chunks = list(stream)
395+
stream.close()
396+
397+
assert len(chunks) == 3
398+
399+
langfuse_memory_client.flush()
400+
span = get_span("unit-openai-native-stream-azure-filter")
401+
402+
assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2"
403+
assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop"
404+
assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == {
405+
"prompt_tokens": 3,
406+
"completion_tokens": 1,
407+
"total_tokens": 4,
408+
}
409+
410+
318411
def test_openai_stream_break_still_finalizes_generation(
319412
langfuse_memory_client, get_span
320413
):

0 commit comments

Comments
 (0)