Repository navigation
[None][fix] Responses: fail over-length streamed requests in-stream with context_length_exceeded - #21060
[None][fix] Responses: fail over-length streamed requests in-stream with context_length_exceeded#21060JunyiXu-nv wants to merge 5 commits into
Conversation
…am with context_length_exceeded A streamed /v1/responses request whose prompt exceeds the context window now gets the stream OpenAI sends for it: response.created, response.in_progress, error, then response.failed with error.code context_length_exceeded, after which the stream ends normally. Clients act on that event: the Codex CLI compacts its history on it, while it treats an HTTP 400 as an invalid request and an aborted stream as a transport error that it retries. Before, the answer depended on where the executor rejected the prompt: - in a worker process (raised while the stream runs): response.failed carried server_error, and the re-raise then aborted the connection; - in this process (raised by generate_async): HTTP 400; - disaggregated serving (the context worker's 400): HTTP 400. Non-streamed requests keep HTTP 400 with code context_length_exceeded. Every other stream failure keeps server_error and the re-raise; its response.failed frame is byte-identical to before. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. WalkthroughStreamed Responses requests with context-length errors now receive an in-stream ChangesResponses stream rejection handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Suggested reviewers: Merge Risk:
|
|
/bot run --disable-fail-fast |
|
PR_Github #80799 [ run ] triggered by Bot. Commit: |
|
PR_Github #80799 [ run ] completed with state
|
…ses-context-length-in-stream Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com> # Conflicts: # tensorrt_llm/serve/openai_server.py # tensorrt_llm/serve/responses_utils.py # tests/unittest/llmapi/apps/test_responses_streaming_events.py
|
/bot run --disable-fail-fast |
|
PR_Github #80939 [ run ] triggered by Bot. Commit: |
|
PR_Github #80939 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #80999 [ run ] triggered by Bot. Commit: |
|
PR_Github #80999 [ run ] completed with state
|
…ext-length-in-stream Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #82259 [ run ] triggered by Bot. Commit: |
|
PR_Github #82259 [ run ] completed with state
|
…ext-length-in-stream Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #82299 [ run ] triggered by Bot. Commit: |
|
PR_Github #82299 [ run ] completed with state
|
…xception handler for subclasses OpenAIDisaggServer._handle_exception(self, exception) keeps the signature it has on main. Subclasses override it with that signature (NeMo-RL's OpenAIDisaggServerAdaptor), so the extra request argument would have made every failed request raise TypeError there. The in-stream answer for a streamed Responses request that the context worker rejected for its length moves to the caller, _wrap_entry_point. The handler, base or override, rules on the failure first, as it does for every failure; only when it returns the worker's context_length_exceeded envelope is that envelope re-shaped into the stream. An override that answers the failure itself keeps its answer. Tests drive the wrapped route instead of the handler, and a subclass with a one-argument handler is checked for an ordinary failure and for a streamed over-length Responses request. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…h a verdict A run of the skill on a clean machine, from the skill alone, could not reach a passing cell. This adds what it missed: - The Codex default profile's echo is measured (exact: codex-cli 0.155.1, 12 sessions against the fake server). Conformance was BLOCKED on it. The contract template now asks for echo. - The capture changes are named by PR (NVIDIA#21605, NVIDIA#21602, NVIDIA#20936), with where to get a wheel that matches the base and which CPU suites to run. - The wiring snippets use absolute paths, create their task, pass --build-id and --matrix, and state which claims a wiring run must pass. - The real run has a full serve command with the recorder in front, and profiles/model/serving/ holds complete configs for GLM-5.3-NVFP4 and DeepSeek-V4.1-Flash as their cells were served. - The matrix template proposes starting thresholds, lists the units each threshold needs, drops keys the verdict never reads, and names cell directories. - templates/full-tier-report.md: the full tier's steps and the five files it writes. - Smaller additions: restricting egress, a harness's own sandbox inside a container, Codex over-length on main (PR NVIDIA#21060), where the GLM goldens came from, test and gate hygiene, and the controls receipt command. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
/bot run |
|
PR_Github #82476 [ run ] triggered by Bot. Commit: |
|
PR_Github #82476 [ run ] completed with state
|
Description
A streamed
/v1/responsesrequest whose prompt is longer than the context window is not answered the way OpenAI answers it. OpenAI opens the stream and ends it withresponse.failed, whoseresponse.error.codeiscontext_length_exceeded. Clients act on that event. The Codex CLI reads it as a full context window: it ends the turn and compacts its history before the next one. It reads an HTTP 400 as an invalid request that it does not retry or recover from.On main the server answers such a request in one of three ways, depending on where the prompt is rejected. None of them reaches the client as a full context window.
guard_responses_streamsendserrorandresponse.failed, but witherror.code: "server_error", and then re-raises. The re-raise aborts the connection mid-body (an incomplete chunked read). The Codex CLI keeps the code ofresponse.failedand acts on it only when the stream ends cleanly. On a transport error it reports a stream error and retries the same request.generate_asyncraises at once, and the server answers HTTP 400 withcode: "context_length_exceeded".This change answers all three with the same stream:
response.created,response.in_progress,error, thenresponse.failed. The failed snapshot carrieserror.code: "context_length_exceeded"and the rejection text aserror.message. Sequence numbers run from 0 to 3, and the stream ends normally.classify_stream_terminationrecognizes an over-lengthRequestErrorby its text (the text is what crosses the executor IPC boundary) and returns the new causecontext_length_exceeded.describe_stream_terminationreports such a rejection by its message alone, the text the non-streamed 400 carries.ResponsesStreamingProcessor.get_stream_failed_eventssetserror.codetocontext_length_exceededfor that cause and keepsserver_errorfor every other cause. The SDK'sResponseError.codeliteral does not listcontext_length_exceeded(checked with openai 2.48 and 3.39). So the event is validated withserver_errorin that slot, and the real code is written into the serialized event by the same serializer as every other frame. For every other cause the frame is byte-identical to before.guard_responses_streamends the stream normally after the terminal events of an over-length rejection, instead of re-raising. Every other failure is re-raised as before.rejected_responses_stream(new) streams a request that was rejected before generation: the opening pair, then the same failure events.OpenAIServer.openai_responsesuses it whengenerate_asyncitself raises the rejection for a streamed request.OpenAIDisaggServer._handle_exception(self, exception)keeps its one-argument signature: subclasses such as NeMo-RL'sOpenAIDisaggServerAdaptoroverride it with that signature and delegate tosuper(), so it is still called, unchanged, for every failure of a wrapped request. The conversion happens in the caller,_wrap_entry_point: when the handler returns the context worker's 4xxcontext_length_exceededenvelope for a streamed Responses request, the route answers with the failure stream instead of that 400. An override that answers the failure itself (raises, or returns anything else) keeps its answer. The snapshot names the model the client asked for, because the frontend does not know the served name.Unchanged:
code: "context_length_exceeded", which is how OpenAI rejects a non-streamed over-length request.max_num_tokenswith chunked prefill disabled is a different rejection ("The prompt length ... should not exceed max_num_tokens"). It keeps its current form, as on the 400 path.This was found while serving DeepSeek-V4.1-Flash to the Codex CLI. The change does not depend on the model.
Test Coverage
CPU-only tests, in files that
l0_cpu.ymlalready lists:tests/unittest/llmapi/apps/test_context_length_error.pytest_a_streamed_responses_request_fails_in_stream[in_worker|in_process]drivesOpenAIServer.openai_responseswith an executor that rejects the prompt in a worker (the rejection travels as anErrorResponsethrough the realGenerationResultand error handler) or in-process (generate_asyncraises). It checks the four events, their numbering, the response id, the code and message, and that the body ends without raising.test_a_non_streamed_responses_request_keeps_the_400[...]andtest_other_rejections_of_a_streamed_request_are_unchanged[...].test_disagg_route_fails_a_streamed_responses_request_in_streamandtest_disagg_route_keeps_the_400_for_other_requests[...](both drive_wrap_entry_point).test_a_subclass_handler_is_called_for_an_ordinary_failure[worker_400|internal_error],test_a_subclass_handler_is_called_for_a_streamed_over_length_request,test_a_subclass_that_answers_the_over_length_failure_itself_keeps_its_answerandtest_a_subclass_that_returns_its_own_answer_keeps_it[...]: a subclass overriding_handle_exception(self, exception)with one argument, as NeMo-RL does, is called for every failure, the streamed over-length request still fails in-stream, and an override's own answer is kept. With the earlier two-argument call these fail withTypeError.tests/unittest/llmapi/apps/test_responses_streaming_events.pyRequestError, and of aValueErrorwith the same text, which staysinternal_error;On a build of current main, these two files and the two disaggregated Responses test files (
tests/unittest/disaggregated/test_openai_server_stream_errors.py,tests/unittest/disaggregated/test_disagg_responses_api.py) pass: 147 tests.Also checked outside the test suite: generated-input properties over the same functions (any chunking and number of frames already sent, wrapped rejection texts, request fields with floats and non-ASCII text), with planted bugs to confirm the checks can fail. An HTTP run under uvicorn with a reader that follows the Codex CLI's stream handling reports a full context window for both in-worker and in-process rejections. On main the same reader reports a stream error (incomplete chunked read) and an HTTP 400.
PR Checklist
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.🤖 Generated with Claude Code
Dev Engineer Review
Streamed
/v1/responsesrequests rejected for context length now produce aresponse.failedevent witherror.code: "context_length_exceeded"and end normally. The change covers rejection before generation, during streaming, and in disaggregated serving. Other streamed failures retain their existing handling. Non-streamed over-length requests retain HTTP 400.No current review findings or severity counts were supplied. The author reports that 113 CPU-only tests passed across the specified Responses test files, compared with 91 on main. The author also reports generated-input checks and an HTTP run; these were outside the test suite and were not independently verified. The supplied CI comments report mixed pipeline outcomes, including failures and unstable runs.
QA Engineer Review
Two test files were modified.
test_context_length_error.pycovers streamed and non-streamed requests, in-process and worker rejection paths, and disaggregated routing.test_responses_streaming_events.pycovers error classification, event serialization, and stream termination. Both files are listed intests/integration/test_lists/test-db/l0_cpu.yml. Neither appears in the searched manual-QA lists. Coverage verdict: sufficient.Per-File QA Perspective
tensorrt_llm/serve/openai_disagg_server.py— Verify that a streamed Responses request rejected by the disaggregated context worker receives an in-stream failure. Verify that other failures retain their existing handling.tensorrt_llm/serve/openai_server.py— Verify that a pre-generation context-length rejection returns a failure stream for streamed Responses requests. Verify that non-streamed requests and other errors retain their existing handling.tensorrt_llm/serve/responses_utils.py— Verify thecontext_length_exceededcode, terminal event sequence, and normal stream termination. Verify that other stream failures retain their existing classification and propagation.tests/unittest/llmapi/apps/test_context_length_error.py— Covers in-process, worker, and disaggregated rejection paths, non-streamed behavior, and other rejection causes. Listed intests/integration/test_lists/test-db/l0_cpu.yml; no entry appeared in the searched manual-QA lists.tests/unittest/llmapi/apps/test_responses_streaming_events.py— Covers error classification, event serialization, stream termination, and propagation of other failures. Listed intests/integration/test_lists/test-db/l0_cpu.yml; no entry appeared in the searched manual-QA lists.