Skip to content

[Bugfix] Apply structured output on /v1/completions when the prompt never opens reasoning - #57754

Open
RichardoMrMu wants to merge 5 commits into
vllm-project:mainfrom
RichardoMrMu:fix-structured-output-completions-reasoning-gate
Open

RichardoMrMu wants to merge 5 commits into
vllm-project:mainfrom
RichardoMrMu:fix-structured-output-completions-reasoning-gate

Conversation

@RichardoMrMu

Copy link
Copy Markdown
Contributor

Purpose

Fixes #57726.

With a reasoning parser configured, a /v1/completions request carrying a
structured-output constraint comes back unconstrained, with a 200 and no
warning. The same request on the same model without --reasoning-parser is
constrained. /v1/chat/completions is unaffected.

Root cause

StructuredOutputManager._get_constraint_start() infers the initial reasoning
state from the prompt with a single question:

if reasoner.is_reasoning_end(request.prompt_token_ids or []):
    return 0
structured_req.reasoning_ended = False

BaseThinkingReasoningParser.is_reasoning_end() scans backwards and returns
False on the start token, True on the end token, and falls through to
return False when it finds neither. That final line conflates two opposite
states:

prompt is_reasoning_end meaning current behaviour
ends with <think> False inside reasoning wait for </think> — correct
contains <think></think> True reasoning closed constrain now — correct
contains neither False never entered reasoning wait forever — bug

A raw completions prompt renders no chat template, so it is the third row. No
reasoning-end token will ever arrive, so the bitmask is never applied and the
model is unconstrained for the whole completion.

The same holds for the engine parser family: ParserEngineReasoningAdapter
inherits ReasoningParser and proxies the same properties, so both families
reach the gate through this one inference.

The change

Ask the second question at the same place: does the prompt contain a reasoning
start marker at all? ReasoningParser.reasoning_start_str already exists
for this and the manager never consulted it.

A parser that exposes no start marker, or whose marker is absent from the
vocabulary, keeps today's behaviour — the helper returns False for "cannot
tell", so only a provable never-entered-reasoning prompt changes.

Verification

Behaviour of the gate before and after, driven through the repo's own
is_reasoning_end source:

prompt shape main this PR
chat, thinking on (ends <think>) waits waits
chat, thinking off (<think></think>) constrained constrained
chat mid-reasoning, long prompt waits waits
reasoning closed then reopened waits waits
reasoning closed, trailing content constrained constrained
raw completions, no reasoning tokens waits constrained
raw completions, empty prompt waits constrained

Exactly the two raw-completions rows change; the five chat rows are untouched.
Parsers without a start marker are identical on all seven.

Tests added to tests/v1/structured_output/test_structured_output_manager.py,
parametrized over both backends:

  • test_constraint_applies_when_prompt_never_opens_reasoning — a prompt that
    opens reasoning still waits, one that closes it is constrained, and one with
    no reasoning tokens is now constrained rather than free.
  • test_parser_without_start_marker_keeps_waiting — the fallback stays on the
    previous behaviour.

Both fail on main for the no-reasoning-tokens case and pass with this change.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added structured-output bug Something isn't working labels Sep 20, 2026
@RichardoMrMu
RichardoMrMu force-pushed the fix-structured-output-completions-reasoning-gate branch from ec3b7e9 to 97cc77d Compare September 20, 2026 06:05
RichardoMu added 2 commits September 20, 2026 06:12
`is_reasoning_end()` returns False both for a prompt sitting inside an open
reasoning block and for one that never entered reasoning at all. A
`/v1/completions` prompt renders no chat template, so it contains neither a
reasoning-start nor a reasoning-end token, and no reasoning-end token will
ever arrive -- the grammar bitmask is never applied and the completion comes
back unconstrained with a 200 and no warning.

Distinguish the two cases with the parser's reasoning-start marker, which the
`ReasoningParser` interface already exposes and the structured-output manager
never consulted. Parsers without a start marker keep the previous behaviour.

Assisted-by: Doubao
Signed-off-by: RichardoMu <tianbowen.richardo@bytedance.com>
…reasoning

Assisted-by: Doubao
Signed-off-by: RichardoMu <tianbowen.richardo@bytedance.com>
@RichardoMrMu
RichardoMrMu force-pushed the fix-structured-output-completions-reasoning-gate branch from 97cc77d to f75abb0 Compare September 20, 2026 06:12
@mergify

mergify Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @RichardoMrMu.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 25, 2026
@mergify mergify Bot removed the needs-rebase label Sep 25, 2026
@RichardoMrMu
RichardoMrMu force-pushed the fix-structured-output-completions-reasoning-gate branch from 77932f0 to 38c942b Compare September 25, 2026 09:21
…gate

Resolve vllm/v1/structured_output/__init__.py: keep the prompt-skips-reasoning gate alongside main's failed_index bitmask refactor. (vllm-project#57754)

Signed-off-by: RichardoMu <44485717+RichardoMrMu@users.noreply.github.com>
@RichardoMrMu
RichardoMrMu force-pushed the fix-structured-output-completions-reasoning-gate branch from 38c942b to ae251a6 Compare September 25, 2026 09:24
@mergify

mergify Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @RichardoMrMu.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

…oning

A /v1/completions prompt renders no chat template, so it contains neither
a reasoning-start nor a reasoning-end token. `is_reasoning_end()` returns
False for it, but no reasoning-end token will ever arrive: the previous
logic left the request unconstrained for its whole completion.

Detect prompts that provably never open a reasoning block via the parser's
reasoning-start marker, and start the constraint immediately instead of
waiting for a reasoning-end token that can never come.

Fixes vllm-project#57726

Signed-off-by: RichardoMu <tianbowen.richardo@bytedance.com>

@arpera arpera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all thank you for your PR!

I have some concerns about approach that you have chosen to resolve initial issue #57726.
There is a problem in case user sent request with token to /v1/completions. In that case we would falsely detect that reasoning has already started and we would skip constraint for structured output -- the same problem we are trying to resolve.

Would it be a better idea simply to put warning in case we have /v1/completions + --reasoning-parser + structured output? I think this should not be hot path as well as /v1/completions API is now considered as superseded by /v1/chat/completions? In that case patch would be simple and small enough. What do you think about this?

@arpera arpera moved this from Unsorted queue to In review in Structured Output (arpera) Oct 1, 2026
@mergify

mergify Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @RichardoMrMu.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 8, 2026
@RichardoMrMu

Copy link
Copy Markdown
Contributor Author

Thanks for the review.

On the /v1/completions + <think> case: with this patch the constraint is not skipped there, the request still waits. _prompt_skips_reasoning() returns True only when the parser exposes a reasoning-start marker and the start token id is absent from prompt_token_ids:

start_id = reasoner.vocab.get(start_str)
if start_id is None:
    return False
return start_id not in prompt_token_ids

A completions prompt that contains <think> has the start token in prompt_token_ids, so the helper returns False, _get_constraint_start() does not return 0, and it falls through to structured_req.reasoning_ended = False, the same path as main. Only the prompt with no reasoning token at all changes (the unconstrained row in #57726).

The case you are pointing at is real, but narrower than a skip: a raw completions prompt that contains <think> and never emits </think> still waits, so the constraint never applies. I treated "start marker present" as "reasoning may still close", which is the assumption main already makes for chat prompts. If you want that handled too, the warning you suggest is the honest way to cover it.

On the warning alternative: it is smaller and touches no chat path, and I am fine with it. The tradeoff is that the request stays unconstrained, so the warning makes #57726 visible rather than fixed. If you prefer warning-only, I will rework it that way, or I can add the warning on top of this predicate so both cases are covered.

Tests covering the two behaviours are test_constraint_applies_when_prompt_never_opens_reasoning and test_parser_without_start_marker_keeps_waiting in tests/v1/structured_output/test_structured_output_manager.py, parametrized over both backends; both fail on main for the no-reasoning-token prompt.

# Conflicts:
#	tests/v1/structured_output/test_structured_output_manager.py
#	vllm/v1/structured_output/__init__.py
@mergify mergify Bot removed the needs-rebase label Oct 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working structured-output

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Bug]: with a --reasoning-parser configured, structured outputs on /v1/completions are not enforced, with no warnings

2 participants