From 462ff24185d46c33e50f94e94f439d721ff92671 Mon Sep 17 00:00:00 2001 From: mo Date: Tue, 27 Oct 2026 11:53:11 +0000 Subject: [PATCH 1/2] 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 ): From 633d10479889adbb0ce383345ccd6b179cd60036 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 9 Nov 2026 10:30:58 +0000 Subject: [PATCH 2/2] test(openai): pin generator input on responses.create Co-authored-by: chrikrah <48338417+chrikrah@users.noreply.github.com> --- tests/unit/test_openai.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index f531ea8c4..d6b87805d 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -1,8 +1,10 @@ import asyncio import base64 +import json from types import SimpleNamespace from unittest.mock import patch +import httpx import pytest from openai.types.chat import ChatCompletion from openai.types.responses import ParsedResponseOutputMessage, ParsedResponseOutputText @@ -1568,3 +1570,44 @@ def test_with_raw_response_streaming_passes_through_untraced( span.name != "OpenAI-generation" for span in memory_exporter.get_finished_spans() ) + + +def test_responses_create_does_not_consume_input_iterator( + langfuse_memory_client, get_span, json_attr +): + sent = [] + + def handler(request: httpx.Request) -> httpx.Response: + sent.append(json.loads(request.content)["input"]) + return httpx.Response( + 200, + json={ + "id": "resp_1", + "object": "response", + "created_at": 1, + "model": "gpt-4o-mini", + "status": "completed", + "output": [], + "parallel_tool_calls": False, + "tool_choice": "auto", + "tools": [], + }, + ) + + openai_client = lf_openai.OpenAI( + api_key="test", + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + messages = [{"role": "user", "content": "hello"}] + + openai_client.responses.create( + name="unit-responses-iterator", + model="gpt-4o-mini", + input=(message for message in messages), + ) + + assert sent == [messages] + + langfuse_memory_client.flush() + span = get_span("unit-responses-iterator") + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == messages