From 462ff24185d46c33e50f94e94f439d721ff92671 Mon Sep 17 00:00:00 2001 From: mo Date: Tue, 27 Oct 2026 11:53:11 +0000 Subject: [PATCH] fix(openai): don't consume iterator arguments before calling OpenAI OpenAI accepts any iterable for messages. Langfuse read a generator while extracting the prompt and then passed the exhausted generator to OpenAI, so the request went out with messages=[] and the API returned a 400. One-shot iterators in the call kwargs are now turned into lists before they are read. --- langfuse/openai.py | 8 +++++++- tests/unit/test_openai.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/langfuse/openai.py b/langfuse/openai.py index 32637569b..789afe87b 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -27,6 +27,7 @@ import os import types from collections import defaultdict +from collections.abc import Iterator from dataclasses import dataclass from datetime import datetime from inspect import isawaitable, isclass @@ -311,7 +312,12 @@ def __init__( self.args["trace_id"] = trace_id self.args["parent_observation_id"] = parent_observation_id - self.kwargs = kwargs + # Materialize one-shot iterators (e.g. a generator of messages) so that + # reading them for tracing does not leave OpenAI with an empty argument. + self.kwargs = { + key: list(value) if isinstance(value, Iterator) else value + for key, value in kwargs.items() + } def get_langfuse_args(self) -> Any: return {**self.args, **self.kwargs} diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index a2a4f7f4f..f531ea8c4 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -372,6 +372,43 @@ def test_chat_completion_exports_generation_span( } +def test_chat_completion_does_not_consume_messages_iterator( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + response = ChatCompletion.model_validate( + { + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 1, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "hi"}, + "finish_reason": "stop", + } + ], + } + ) + messages = [{"role": "user", "content": "hello"}] + + with patch.object( + openai_client.chat.completions, "_post", return_value=response + ) as post: + openai_client.chat.completions.create( + name="unit-openai-iterator", + model="gpt-4o-mini", + messages=(message for message in messages), + ) + + assert post.call_args.kwargs["body"]["messages"] == messages + + langfuse_memory_client.flush() + span = get_span("unit-openai-iterator") + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == messages + + def test_chat_completion_with_none_choices_does_not_crash( langfuse_memory_client, get_span, json_attr ):