Skip to content

Commit 6659125

Browse files
authored
fix(openai): handle null choices in chat completion responses (#1745)
1 parent 8c8b120 commit 6659125

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

langfuse/openai.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any:
755755
if chunk_usage is not None:
756756
usage = chunk_usage
757757

758-
choices = chunk.get("choices", [])
758+
choices = chunk.get("choices") or []
759759

760760
for choice in choices:
761761
if _is_openai_v1():
@@ -898,7 +898,7 @@ def _get_langfuse_data_from_default_response(
898898
completion = None
899899

900900
if resource.type == "completion":
901-
choices = response.get("choices", [])
901+
choices = response.get("choices") or []
902902
if len(choices) > 0:
903903
choice = choices[-1]
904904

@@ -908,7 +908,7 @@ def _get_langfuse_data_from_default_response(
908908
completion = _extract_response_api_completion(response.get("output", {}))
909909

910910
elif resource.type == "chat":
911-
choices = response.get("choices", [])
911+
choices = response.get("choices") or []
912912
if len(choices) > 0:
913913
# If multiple choices were generated, we'll show all of them in the UI as a list.
914914
if len(choices) > 1:
@@ -927,7 +927,7 @@ def _get_langfuse_data_from_default_response(
927927
)
928928

929929
elif resource.type == "embedding":
930-
data = response.get("data", [])
930+
data = response.get("data") or []
931931
if len(data) > 0:
932932
first_embedding = data[0]
933933
embedding_vector = (

tests/unit/test_openai.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,69 @@ def test_chat_completion_exports_generation_span(
311311
}
312312

313313

314+
def test_chat_completion_with_none_choices_does_not_crash(
315+
langfuse_memory_client, get_span, json_attr
316+
):
317+
openai_client = lf_openai.OpenAI(api_key="test")
318+
response = SimpleNamespace(
319+
model="gpt-4o-mini",
320+
choices=None,
321+
usage=SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4),
322+
)
323+
324+
with patch.object(openai_client.chat.completions, "_post", return_value=response):
325+
result = openai_client.chat.completions.create(
326+
name="unit-openai-chat-none-choices",
327+
model="gpt-4o-mini",
328+
messages=[{"role": "user", "content": "1 + 1 = ?"}],
329+
)
330+
331+
assert result is response
332+
333+
langfuse_memory_client.flush()
334+
span = get_span("unit-openai-chat-none-choices")
335+
336+
assert LangfuseOtelSpanAttributes.OBSERVATION_LEVEL not in span.attributes
337+
assert (
338+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o-mini"
339+
)
340+
assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == {
341+
"prompt_tokens": 3,
342+
"completion_tokens": 1,
343+
"total_tokens": 4,
344+
}
345+
346+
347+
def test_openai_stream_with_none_choices_chunk_does_not_crash(
348+
langfuse_memory_client, get_span
349+
):
350+
openai_client = lf_openai.OpenAI(api_key="test")
351+
chunks_with_none_choices = [
352+
SimpleNamespace(model="gpt-4o-mini", choices=None, usage=None),
353+
*_make_chat_stream_chunks(),
354+
]
355+
raw_stream = DummyOpenAIStream(chunks_with_none_choices, DummySyncResponse())
356+
357+
with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream):
358+
stream = openai_client.chat.completions.create(
359+
name="unit-openai-stream-none-choices",
360+
model="gpt-4o-mini",
361+
messages=[{"role": "user", "content": "1 + 1 = ?"}],
362+
stream=True,
363+
)
364+
365+
chunks = list(stream)
366+
stream.close()
367+
368+
assert len(chunks) == 3
369+
370+
langfuse_memory_client.flush()
371+
span = get_span("unit-openai-stream-none-choices")
372+
373+
assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2"
374+
assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop"
375+
376+
314377
def test_streaming_chat_completion_preserves_tool_calls_after_content():
315378
model, completion, usage, metadata = (
316379
lf_openai_module._extract_streamed_openai_response(

0 commit comments

Comments
 (0)