Repository navigation
fix(openai): don't consume iterator arguments before calling OpenAI - #2075
mosafariuk wants to merge 1 commit into
Conversation
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.
|
@hassiebp could you approve the workflow runs for this one when you have a moment? CI shows |
chrikrah
left a comment
There was a problem hiding this comment.
Nothing here blocks a merge from my side. OpenAI gets the messages the span records, and the async client matches your manual run.
$ cp <base 043321f>/langfuse/openai.py langfuse/openai.py && .venv/bin/python -m pytest tests/unit/test_openai.py -q -p no:cacheprovider # head 462ff24, fix reverted
1 failed, 38 passed in 2.15s
$ .venv/bin/python probe_async.py # AsyncOpenAI, generator messages, httpx.MockTransport
async sent: [] # base 043321f
async sent: [{'role': 'user', 'content': 'hello'}] # head 462ff24
$ .venv/bin/python resp.py # responses.create(input=<generator>), openai 2.29.0, httpx.MockTransport
plain TypeError: Object of type generator is not JSON serializable # bare openai.OpenAI
lf TypeError: Object of type generator is not JSON serializable # langfuse.openai, base 043321f
lf sent input: [{'role': 'user', 'content': 'hello'}] # langfuse.openai, head 462ff24
non-blocking: the drop-in now accepts a generator input on responses.create that plain openai rejects. Fine by me if that's intended; a test would pin it. Want the snippet?
|
@chrikrah thanks for checking the Responses path too. Yes, that's intended: the extractor now never consumes a one-shot iterator before the call, so anything it reads for tracing is still intact for OpenAI. For |
What does this PR do?
With the
langfuse.openaidrop-in, passingmessagesas a generator breaks the call. The OpenAI SDK typesmessagesasIterable[...], and plainopenaiaccepts a generator. ButOpenAiArgsExtractorreadskwargs["messages"]to record the prompt (_extract_chat_prompt), which uses the generator up before the originalcreateruns. OpenAI then receivesmessages: []and rejects the request:The async client is affected the same way, because it goes through the same extractor.
This PR turns one-shot iterator arguments into lists in
OpenAiArgsExtractor.__init__, so tracing and the OpenAI call both see the same values. Lists, tuples, dicts and other re-iterable values are left as they are. Only the chat, completions, responses and embeddings methods are wrapped, and none of them take file arguments, so no file handle is ever read here.Fixes # (no existing issue)
Type of change
Verification
The new
test_chat_completion_does_not_consume_messages_iteratorpatches the client's_post, returns a realChatCompletion, and checks both the request body sent to OpenAI and the recorded span input. Onmainit fails withassert [] == [{...}].I also ran the async client by hand against an
httpx.MockTransport. Onmainit sendsmessages: [], and with this change it sends the real messages.Checklist
code_review.md..env.templateif needed.

Confidence Score: 5/5
The PR appears safe to merge.Summary
The PR converts iterator-valued keyword arguments to lists before tracing reads them. OpenAI then receives the same values instead of an exhausted iterator.
Reviews (1) · Last reviewed commit: "fix(openai): don't consume iterator argu..." · Reviewed by Greptile