Skip to content

[Bugfix][Frontend][gpt-oss] Return raw output when Harmony parser ends non-terminal - #47062

Merged
sfeng33 merged 2 commits into
vllm-project:mainfrom
Achyuthan-S:fix/harmony-nonterminal-content-fallback
Jul 1, 2026
Merged

[Bugfix][Frontend][gpt-oss] Return raw output when Harmony parser ends non-terminal#47062
sfeng33 merged 2 commits into
vllm-project:mainfrom
Achyuthan-S:fix/harmony-nonterminal-content-fallback

Conversation

@Achyuthan-S

@Achyuthan-S Achyuthan-S commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Purpose

Fixes #45736. When GPT-OSS Harmony output ends in a non-terminal parser
state
— e.g. a malformed final channel that omits the <|message|>
delimiter, trapping the body in the header — vLLM silently returned
content: null with finish_reason="stop" and completion_tokens > 0,
dropping the generated answer with no exception, log, or recovery.

This implements the consensus from #45736 (after #46437 added
flush()/process_eos()): catch the HarmonyError, return the raw unparsed
output as content, keep a standard finish_reason, and log a warning for
observability. No 4xx/5xx and no out-of-spec finish_reason.

Root cause

flush() ran process_eos() inside contextlib.suppress(HarmonyError),
swallowing the library's own non-terminal signal (the # TODO: Consider reraising left by #46437). parse() then returned content=None and the
serving layer emitted a clean stop.

Changes

vllm/parser/harmony.py:

  • flush() re-raises HarmonyError after resetting parser state for the next
    turn.
  • parse() catches it and returns the raw model_output as content.
  • parse_delta() catches it, returns DeltaMessage(content=delta_text), and
    resets the tool-call index.
  • A logger.warning is emitted at each fallback site.

tests/parser/test_harmony.py:

  • New malformed-final tests for both parse() and parse_delta().
  • Updated the flush EOS test to assert the new raise-and-reset contract.
  • Fixed get_model_output_tokens to render with render_conversation instead
    of render_conversation_for_completion, dropping the trailing next-turn
    <|start|>assistant so synthetic streams match real output.token_ids. That
    trailing generation prompt left the parser non-terminal for every turn
    (previously masked by contextlib.suppress), which would otherwise make
    well-formed turns look malformed.

Test plan

.venv/bin/python -m pytest tests/parser/test_harmony.py -v
.venv/bin/pre-commit run --files vllm/parser/harmony.py tests/parser/test_harmony.py
.venv/bin/pre-commit run mypy-3.12 --files vllm/parser/harmony.py --hook-stage manual

Result: 39 passed; ruff, typos, and mypy (3.10 + 3.12) all pass.
(Local-only teardown errors come from an unrelated Apple-MPS
torch.accelerator.empty_cache() call in the shared conftest cleanup — they are
ERROR at teardown, not test failures, and do not occur on CI.)

Notes

cc @yzong-rh @sfeng33

@Achyuthan-S
Achyuthan-S requested a review from aarnphm as a code owner June 29, 2026 17:50
Copilot AI review requested due to automatic review settings June 29, 2026 17:50

@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.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sfeng33

sfeng33 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

cc @bbrowning @yzong-rh

@yzong-rh yzong-rh 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.

Thanks for the quick turnaround.

Looks good. Could dedup separate logger.warning calls and remove extra comments (the content of the warning and the test names document the fix quite well already).

tests/parser/test_harmony.py passes for me.

Comment thread tests/parser/test_harmony.py Outdated
Comment thread tests/parser/test_harmony.py Outdated
Comment thread tests/parser/test_harmony.py Outdated
Comment thread tests/parser/test_harmony.py Outdated
Comment thread vllm/parser/harmony.py Outdated
self._parser = None
self._num_processed_messages = 0

if eos_error is not None:

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.

I think try ... finally would be preferable over storing the eos_error then re-raising it? Is there a reason you did it this way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No good reason — yours is cleaner. Switched to try/finally: process_eos() + poll in the try, the warning + re-raise in except HarmonyError, and the parser reset in finally so it always runs. Dropped the stored eos_error.

Comment thread vllm/parser/harmony.py Outdated
Comment on lines +155 to +161
logger.warning(
"Harmony parser ended in a non-terminal state; returning the "
"raw model output as content (%d token(s)). This usually "
"indicates a malformed assistant turn, e.g. a 'final' channel "
"missing the <|message|> delimiter.",
len(model_output_token_ids),
)

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.

You could consolidate logger.warning in flush() itself. No need for two separate warnings for parse() and parse_delta imo when the cause is the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — there's now a single logger.warning in flush() (same cause for both paths), and parse()/parse_delta() just catch and fall back.

…s non-terminal

When GPT-OSS Harmony output ends in a non-terminal parser state (e.g. a
malformed `final` channel that omits the <|message|> delimiter, trapping the
body in the header), vLLM silently returned content=None with
finish_reason="stop" and billed tokens, dropping the answer with no error,
log, or recovery.

flush() called process_eos() inside contextlib.suppress(HarmonyError),
swallowing the library's non-terminal signal. flush() now re-raises it after
resetting parser state; parse() falls back to the raw model output as content,
and parse_delta() falls back to the raw delta text. A warning is logged for
observability. finish_reason is unchanged.

Also fixes the get_model_output_tokens test helper to render without a trailing
next-turn <|start|>assistant generation prompt, so synthetic streams match real
output.token_ids instead of always ending non-terminal.

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
@Achyuthan-S
Achyuthan-S force-pushed the fix/harmony-nonterminal-content-fallback branch from 71835c8 to 1a3e910 Compare June 30, 2026 06:34
@Achyuthan-S

Copy link
Copy Markdown
Contributor Author

Thanks for the quick turnaround.

Looks good. Could dedup separate logger.warning calls and remove extra comments (the content of the warning and the test names document the fix quite well already).

tests/parser/test_harmony.py passes for me.

Thanks @yzong-rh! Addressed everything: consolidated the warning into flush(), switched to try/finally, and removed the extra comments. tests/parser/test_harmony.py still passes (39) and ruff + mypy are clean.

@Achyuthan-S

Copy link
Copy Markdown
Contributor Author

@yzong-rh thank you for the support, I would appreciate it if you assign me to any other interesting issues too
I would love to contribute more , thanks.

@yzong-rh yzong-rh 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.

cc @sfeng33 or @bbrowning could you add ready status so CI could run?

@sfeng33 sfeng33 added the ready ONLY add when PR is ready to merge/full CI is needed label Jun 30, 2026
@github-project-automation github-project-automation Bot moved this from To Triage to Ready in gpt-oss Issues & Enhancements Jun 30, 2026
@sfeng33
sfeng33 enabled auto-merge (squash) June 30, 2026 19:42
@sfeng33
sfeng33 merged commit 3406e8f into vllm-project:main Jul 1, 2026
54 checks passed
rjrock pushed a commit to rjrock/vllm that referenced this pull request Jul 1, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
jakki-amd pushed a commit to jakki-amd/vllm that referenced this pull request Jul 6, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
lkk12014402 pushed a commit to lkk12014402/vllm that referenced this pull request Jul 8, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
noooop pushed a commit to noooop/vllm that referenced this pull request Jul 9, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
philippesic pushed a commit to philippesic/vllm-semantic-cache that referenced this pull request Jul 19, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
plasticchris pushed a commit to plasticchris/vllm that referenced this pull request Jul 20, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
aditi-amd pushed a commit to aditi-amd/vllm that referenced this pull request Aug 4, 2026
…s non-terminal (vllm-project#47062)

Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
Signed-off-by: root <root@smci355-ccs-aus-m02-09.cs-aus.dcgpu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working gpt-oss Related to GPT-OSS models ready ONLY add when PR is ready to merge/full CI is needed tool-calling

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: GPT-OSS Harmony: vLLM silently returns content: null with finish_reason="stop" when its parser ends in a non-terminal state

5 participants