Skip to content

[Bugfix][Frontend] Emit response.incomplete for truncated Responses streams - #45386

Closed
Sunt-ing wants to merge 2 commits into
vllm-project:mainfrom
Sunt-ing:fix/dig046-responses-stream-incomplete-event
Closed

Sunt-ing wants to merge 2 commits into
vllm-project:mainfrom
Sunt-ing:fix/dig046-responses-stream-incomplete-event

Conversation

@Sunt-ing

@Sunt-ing Sunt-ing commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

OpenAI's Responses streaming reference defines response.completed as the event emitted when the model response is complete, and response.incomplete as the event emitted when a response finishes as incomplete. vLLM already builds the final Responses payload with response.status == "incomplete" and incomplete_details.reason == "max_output_tokens" when generation stops on the output-token limit, but the streaming path still wraps that payload in a response.completed terminal event.

This PR makes the terminal event match the final response status: incomplete emits response.incomplete, and other successful responses keep emitting response.completed. It also adds the vLLM ResponseIncompleteEvent wrapper, updates the streaming event-stack helper so both completed and incomplete are valid terminal response events, and treats response.incomplete as terminal when replaying stored background stream events.

Scope: this only handles max-token truncation after the final payload is already status="incomplete". It does not change generation-error streaming; #40418 and #38190 cover response.failed paths.

Test Plan

  • Run a real Responses streaming truncation E2E with Qwen3-8B and max_output_tokens=30.
  • Run the same truncation path with background=True, then replay it with retrieve(..., stream=True).
  • Revert only the source change while keeping the new E2E assertion.
  • Run the full Responses simple E2E file.
  • Run related Responses protocol, serving, and error tests.
  • Run changed-file pre-commit.

Test Result

Check Result
Real truncation E2E with fix Terminal event is response.incomplete; payload status is incomplete; reason is max_output_tokens
Background truncation replay E2E with fix Direct stream and stored replay both end with response.incomplete
Same E2E after source-only revert Failed with AssertionError: assert 'response.completed' == 'response.incomplete'
Full tests/entrypoints/openai/responses/test_simple.py 12 passed
Related Responses protocol, serving, and error tests 26 passed
Changed-file pre-commit Passed
Real server E2E details

Environment:

  • GPU: NVIDIA A800-SXM4-80GB, single GPU
  • Model: Qwen/Qwen3-8B
  • Server harness: RemoteOpenAIServer
  • Server args: --reasoning-parser qwen3 --max_model_len 5000
  • Server env: VLLM_SYSTEM_START_DATE=2023-09-12, VLLM_ENABLE_RESPONSES_API_STORE=1
  • Hardware workaround for this A800 setup: VLLM_USE_FLASHINFER_SAMPLER=0

The committed E2E starts a real OpenAI-compatible server and sends a streaming Responses request that is intentionally truncated:

stream = await client.responses.create(
    model="Qwen/Qwen3-8B",
    input="What is the first paragraph of Moby Dick?",
    reasoning={"effort": "low"},
    max_output_tokens=30,
    temperature=0.0,
    stream=True,
)
events = [event async for event in stream]

terminal_event = events[-1]
assert terminal_event.type == "response.incomplete"
assert terminal_event.response.status == "incomplete"
assert terminal_event.response.incomplete_details.reason == "max_output_tokens"

The background replay E2E uses the same truncation trigger with background=True, then replays the stored stream:

stream = await client.responses.create(
    model="Qwen/Qwen3-8B",
    input="What is the first paragraph of Moby Dick?",
    reasoning={"effort": "low"},
    max_output_tokens=30,
    temperature=0.0,
    stream=True,
    background=True,
)
events = [event async for event in stream]

async with await client.responses.retrieve(
    response_id=events[0].response.id,
    stream=True,
) as replay_stream:
    replay_events = [event async for event in replay_stream]

assert replay_events == events
assert replay_events[-1].type == "response.incomplete"

With the fix:

tests/entrypoints/openai/responses/test_simple.py::test_streaming_max_tokens_terminal_event
tests/entrypoints/openai/responses/test_simple.py::test_background_streaming_max_tokens_terminal_event
2 passed, 16 warnings in 73.61s

After reverting only the source change and keeping the same test:

AssertionError: assert 'response.completed' == 'response.incomplete'
Additional checks

Commands:

PYTHONPATH=$PWD VLLM_USE_FLASHINFER_SAMPLER=0 python -m pytest tests/entrypoints/openai/responses/test_simple.py::test_streaming_max_tokens_terminal_event tests/entrypoints/openai/responses/test_simple.py::test_background_streaming_max_tokens_terminal_event -q --tb=short --disable-warnings
PYTHONPATH=$PWD VLLM_USE_FLASHINFER_SAMPLER=0 python -m pytest tests/entrypoints/openai/responses/test_simple.py -q --tb=short --disable-warnings
CUDA_VISIBLE_DEVICES="" PYTHONPATH=$PWD python -m pytest tests/entrypoints/openai/responses/test_protocol.py tests/entrypoints/openai/responses/test_serving_responses.py tests/entrypoints/openai/responses/test_errors.py -q --tb=short --disable-warnings
pre-commit run --files tests/entrypoints/openai/responses/conftest.py tests/entrypoints/openai/responses/test_simple.py vllm/entrypoints/openai/responses/protocol.py vllm/entrypoints/openai/responses/serving.py

Results:

tests/entrypoints/openai/responses/test_simple.py
12 passed, 16 warnings in 169.64s

tests/entrypoints/openai/responses/test_protocol.py
tests/entrypoints/openai/responses/test_serving_responses.py
tests/entrypoints/openai/responses/test_errors.py
26 passed, 16 warnings in 4.27s

pre-commit run --files <changed files>
Passed

AI assistance was used to prepare this PR.

cc @sfeng33 @chaunceyjiang

…treams

Responses streaming currently emits response.completed even when max_output_tokens truncates the response and the final payload status is incomplete.

Add a vLLM ResponseIncompleteEvent wrapper and select the terminal event from the final response status. Treat response.incomplete as a terminal stored event during background stream replay. Update streaming event helpers and Responses E2E coverage for direct and background truncated streams.

Signed-off-by: Ting Sun <suntcrick@gmail.com>
Signed-off-by: Ting Sun <suntcrick@gmail.com>
@Sunt-ing

Sunt-ing commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of #59324, which covers this fix, including the response.incomplete terminal event and background replay handling.

@Sunt-ing Sunt-ing closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant