fix(responses_bridge): map incomplete responses to finish_reason length instead of 500 - #37710
Conversation
…th instead of 500
Greptile SummaryThe PR converts incomplete Responses bridge results into valid chat completions instead of errors and preserves truncation metadata and usage.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/completion_extras/litellm_responses_transformation/transformation.py | Adds incomplete-response finish-reason mapping and terminal handling for both non-streaming and streaming bridge responses. |
| tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py | Adds typed helpers and focused regression coverage; the previously reported missing annotations are now present. |
Reviews (4): Last reviewed commit: "require an incomplete reason before over..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
The three helpers added for the incomplete-response tests took untyped parameters, which the repo's typing rule does not allow. Annotate them through a TYPE_CHECKING block so the runtime imports stay inside the function bodies like the rest of this file.
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7b25ee1. Configure here.
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 16bba15. Configure here.
tin-berri
left a comment
There was a problem hiding this comment.
Solid fix — converts the 500-on-truncated-reasoning bug into a proper finish_reason (length/content_filter) for both streaming and non-streaming paths, and cleans up duplicated reasoning-item-extraction logic into one shared helper used by both. The response_is_incomplete check (status=="incomplete" OR incomplete_details.reason set) correctly preserves the old raise-on-genuinely-empty-and-not-incomplete behavior — good regression guard in test_transform_response_zero_choices_not_incomplete_still_raises. Boundary case (completed status + null-reason incomplete_details staying "stop") is explicitly tested too. Good coverage across reasoning-only, content-filter, partial-text-override, and all three streaming response.incomplete event shapes. CI green. Approved.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a developer whose app calls a bridged reasoning model with a small completion budget gets a 500 whenever reasoning uses up the whole budget
"model": "bridge/gpt-5.6-sol"(aopenai/responses/...deployment),"reasoning_effort": "high","max_completion_tokens": 16"litellm.APIConnectionError: ... gpt-5.6-sol unable to complete request: max_output_tokens", so their app surfaces a gateway error to the end user"stream": truereturns 200, but the only meaningful chunk carries"finish_reason": "stop"and no usage, so the app believes the model finished normally and records zero spend"max_output_tokens": 16returns 200 with"status": "incomplete", so only the chat completions surface is brokenAfter: the same request behaves like OpenAI's native chat completions on truncation
"reasoning_effort": "high","max_completion_tokens": 16"content": "","finish_reason": "length", and usage showing prompt 37 / completion 16 / reasoning 16 tokens"stream": truereturns a terminal chunk with"finish_reason": "length", and with"stream_options": {"include_usage": true}the final chunk carries the real usage"finish_reason": "content_filter"insteadRelevant issues
Linear ticket
Resolves LIT-5886
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Two proxies, same config, same prompt, real OpenAI calls with no mocks. Before is the merge base cb4eb82, after is 5c89490 on this branch. All four cases were re-run at the current tip 16bba15 and came back identical, so the proof stands at the head, not just at the commit it was first captured on
Shared config:
Prompt used in every case: "Determine the last ten decimal digits of 7 raised to the power 123456789. Think through modular arithmetic carefully, then return only those ten digits." Every request caps output at 16 tokens, which is what makes the upstream response come back incomplete
Before (cb4eb82, port 52361)
Non-streaming /v1/chat/completions
curl -sS -i http://localhost:52361/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"bridge/gpt-5.6-sol","messages":[{"role":"user","content":"<prompt>"}],"reasoning_effort":"high","max_completion_tokens":16}'HTTP/1.1 500 Internal Server Errorwith body{"error":{"message":"litellm.APIConnectionError: APIConnectionError: OpenAIException - gpt-5.6-sol unable to complete request: max_output_tokens. ...","code":"500"}}, no usage anywhere, andx-litellm-response-cost: 0ValueError: gpt-5.6-sol unable to complete request: max_output_tokens, raised at transformation.py:768Streaming /v1/chat/completions
"stream": trueand"stream_options":{"include_usage":true}{"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}, then a second empty-delta chunk, then[DONE]Non-streaming /v1/messages
curl -sS -i http://localhost:52361/v1/messages -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"bridge/gpt-5.6-sol","max_tokens":16,"messages":[{"role":"user","content":"<prompt>"}]}'"content":[],"stop_reason":"max_tokens"and"usage":{"input_tokens":37,"output_tokens":16}, billed atx-litellm-response-cost: 0.000665Streaming /v1/messages
"stream": truemessage_start, thenmessage_deltacarrying{"stop_reason": "max_tokens"}with usage 37 in / 16 out, thenmessage_stopAfter (5c89490, port 35416)
Non-streaming /v1/chat/completions
HTTP/1.1 200 OKwith"choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_items":[{"type":"reasoning","id":"rs_0785b87a...","encrypted_content":"gAAAAABqh4bd...","summary":[]}]}}]and"usage":{"completion_tokens":16,"prompt_tokens":37,"total_tokens":53,"completion_tokens_details":{"reasoning_tokens":16},...}x-litellm-response-cost: 0.000665, where the 500 billed nothingStreaming /v1/chat/completions
"stream": trueand"stream_options":{"include_usage":true}{"choices":[{"index":0,"delta":{},"finish_reason":"length"}]}{"completion_tokens":16,"prompt_tokens":37,"total_tokens":53,"completion_tokens_details":{"reasoning_tokens":16},...}, then[DONE]Non-streaming /v1/messages
"content":[],"stop_reason":"max_tokens", usage 37 in / 16 out, cost 0.000665, unchanged from beforeStreaming /v1/messages
"stream": truemessage_start/message_deltawith"stop_reason": "max_tokens"and usage 37 / 16 /message_stop, unchangedThe Anthropic shape already reported the truncation honestly at the merge base, so the fix lands only on the OpenAI chat completions shape and /v1/messages stays where it was
Re-run at the tip (16bba15, port 55643)
The same four cases, driven again against a proxy booted from the current head with
PYTHONPATHpinned to that worktree and the loadedlitellm.__file__asserted to sit inside it:HTTP/1.1 200 OK,"finish_reason":"length",reasoning_itemspresent,"usage":{"completion_tokens":16,"prompt_tokens":37,"total_tokens":53,"completion_tokens_details":{"reasoning_tokens":16}}, billedx-litellm-response-cost: 0.000665{"choices":[{"index":0,"delta":{},"finish_reason":"length"}]}followed by the real usage chunk and[DONE]"content":[],"stop_reason":"max_tokens", usage 37 in / 16 out, cost 0.000665message_start,message_deltacarrying{"stop_reason": "max_tokens"}with usage 37 / 16,message_stopSurprises
Type
🐛 Bug Fix
Caveats (if any)
Final Attestation
Note
Medium Risk
Changes how truncated/incomplete Responses API results are mapped into chat completions, including finish_reason and usage/billing. Wrong mapping would mis-signal truncation or still 500, but the change is isolated to the bridge transformer.
Overview
Chat completions through the Responses bridge no longer 500 when a reasoning model burns the whole token budget. Incomplete Responses API results now look like native OpenAI truncation: HTTP 200,
finish_reasonlength(orcontent_filter), and real usage.If there is no message (reasoning-only output), the bridge emits one empty assistant choice and still round-trips
reasoning_items. Partial text is kept and only the finish reason is overridden. Streaming treatsresponse.incompletelikeresponse.completed, so the terminal chunk carries the mapped finish reason and usage instead ofstopwith no spend. Unknown empty completed responses still raise.Reviewed by Cursor Bugbot for commit 16bba15. Bugbot is set up for automated code reviews on this repo. Configure here.