[Bugfix][Frontend][gpt-oss] Return raw output when Harmony parser ends non-terminal - #47062
Conversation
yzong-rh
left a comment
There was a problem hiding this comment.
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.
| self._parser = None | ||
| self._num_processed_messages = 0 | ||
|
|
||
| if eos_error is not None: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| 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), | ||
| ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
71835c8 to
1a3e910
Compare
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. |
|
@yzong-rh thank you for the support, I would appreciate it if you assign me to any other interesting issues too |
yzong-rh
left a comment
There was a problem hiding this comment.
cc @sfeng33 or @bbrowning could you add ready status so CI could run?
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com> Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
…s non-terminal (vllm-project#47062) Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
…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>
Purpose
Fixes #45736. When GPT-OSS Harmony output ends in a non-terminal parser
state — e.g. a malformed
finalchannel that omits the<|message|>delimiter, trapping the body in the header — vLLM silently returned
content: nullwithfinish_reason="stop"andcompletion_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 theHarmonyError, return the raw unparsedoutput as content, keep a standard
finish_reason, and log a warning forobservability. No 4xx/5xx and no out-of-spec
finish_reason.Root cause
flush()ranprocess_eos()insidecontextlib.suppress(HarmonyError),swallowing the library's own non-terminal signal (the
# TODO: Consider reraisingleft by #46437).parse()then returnedcontent=Noneand theserving layer emitted a clean
stop.Changes
vllm/parser/harmony.py:flush()re-raisesHarmonyErrorafter resetting parser state for the nextturn.
parse()catches it and returns the rawmodel_outputas content.parse_delta()catches it, returnsDeltaMessage(content=delta_text), andresets the tool-call index.
logger.warningis emitted at each fallback site.tests/parser/test_harmony.py:finaltests for bothparse()andparse_delta().get_model_output_tokensto render withrender_conversationinsteadof
render_conversation_for_completion, dropping the trailing next-turn<|start|>assistantso synthetic streams match realoutput.token_ids. Thattrailing generation prompt left the parser non-terminal for every turn
(previously masked by
contextlib.suppress), which would otherwise makewell-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, andmypy(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 areERROR at teardown, not test failures, and do not occur on CI.)Notes
reasoningfield is dropped; the reasoningtext still appears inside the recovered raw content. This matches the agreed
"return what we can of the unparsed output" approach.
deltas, so the final delta surfaces the raw delta text plus the warning.
process_eos()to flush Harmony Parser outputs. #46437 and supersedes the closed warn-only PR [Bugfix][Frontend] Warn on silent GPT-OSS Harmony non-terminal parse drops #45796 —not a duplicate.
cc @yzong-rh @sfeng33