Skip to content

[Bugfix][Frontend] Apply model-default reasoning parser in GPU-less render server - #54835

Open
shimib wants to merge 3 commits into
vllm-project:mainfrom
shimib:fix/render-server-reasoning-parser-default
Open

shimib wants to merge 3 commits into
vllm-project:mainfrom
shimib:fix/render-server-reasoning-parser-default

Conversation

@shimib

@shimib shimib commented Sep 1, 2026

Copy link
Copy Markdown

Purpose

Fix the GPU-less render server (vllm launch render) ignoring model-default reasoning parsers, which left gpt-oss (harmony) output completely unparsed by the derender endpoints. The same mis-resolution affected the render leg too: OnlineRenderer received the raw (empty) flag as well, so both the render and derender handlers now get the resolved value.

init_render_app_state passed the raw --reasoning-parser CLI flag to OnlineRenderer/OnlineDerenderer, missing model-specific defaults applied by verify_and_update_config (e.g. "openai_gptoss" for gpt_oss, set in vllm/model_executor/models/config.py). The main API server uses the config-resolved value (vllm/entrypoints/generate/api_router.py), so the two frontends diverged.

The render entrypoint builds VllmConfig(model_config=...) directly, bypassing create_engine_config — so the CLI flag was never merged into structured_outputs_config there. The fix extracts that merge into EngineArgs.create_structured_outputs_config() (called from both create_engine_config and the render entrypoint), and app_state reads the config-resolved value with args.reasoning_parser kept as the first source, so callers that construct a VllmConfig without the merge still honor an explicit flag.

Note: create_structured_outputs_config also merges --reasoning-parser-plugin for create_engine_config parity, but that part is inert for the render server (plugin import happens from args in the launcher and already worked); no behavior change is claimed for it.

Observable impact before this fix, with vllm launch render openai/gpt-oss-20b (no flags):

  • ParserManager.get_parser received no parser names and returned None before its is_harmony check.
  • Non-streaming /v1/chat/completions/derender returned raw harmony markup as content (e.g. "analysisNeed the weather for Paris.assistantfinalIt is sunny.") — no reasoning, no tool_calls.
  • Streaming derender leaked the raw markup into delta.content instead of failing closed (whether channel markers appear as literal special-token text additionally depends on the request's skip_special_tokens and the tokenizer's special-token flags).

After the fix, the same launch parses harmony like the main server (reasoning/content split correctly), and streaming fails closed with the documented 501 until parser-aware streaming derender (#50550) lands — the two changes compose (verified on that PR's branch).

Not a duplicate: searched open/closed PRs and issues for render/derender + harmony/gpt-oss/reasoning-parser combinations; nothing addresses this. #50550 (streaming parse path) is orthogonal and does not touch parser-name resolution.

No model-output changes: this affects only frontend prompt/response processing parity, so no model evals are applicable.

Test Plan

Regression unit tests (real gpt-oss VllmConfig, heavy constructors monkeypatched) and flag-precedence coverage:

pytest tests/entrypoints/launchers/render/ -q

E2E regression on a no-flag gpt-oss render server (new), plus existing suites guarding the flagged paths (the DeepSeek-R1 e2e tests launch a real server with --reasoning-parser and assert parsed reasoning):

pytest tests/entrypoints/scale_out/derender/test_derender.py -q
pytest tests/entrypoints/scale_out/render/test_render.py -q

Lint:

pre-commit run --files vllm/engine/arg_utils.py vllm/entrypoints/launchers/render/entry.py vllm/entrypoints/launchers/render/app_state.py
pre-commit run mypy-3.12 --files vllm/entrypoints/launchers/render/app_state.py --hook-stage manual

Test Result

  • Unit regression fails on unfixed code with AssertionError: assert '' == 'openai_gptoss'; passes with the fix. Flag-precedence test confirms an explicit --reasoning-parser survives VllmConfig construction over the model default.
  • New test_e2e_harmony_reasoning_default_parser (bare gpt-oss server, no parser flags) passes: reasoning parsed, no <|channel|> markup in content.
  • Full render/derender/launcher suites pass; all pre-commit hooks incl. CI-style mypy-3.12 pass.
  • Manual e2e: non-streaming derender returns reasoning: 'Need the weather for Paris.' / content: 'It is sunny.' (previously raw markup); streaming returns HTTP 501 fail-closed.

AI-assisted contribution

This change was developed with AI assistance (Claude Code). Every changed line was reviewed by the submitter, who ran the tests above.

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

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added frontend bug Something isn't working labels Sep 1, 2026

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

Verified locally on head c21d812 (base 55178f2):

Bug real on base. init_render_app_state passed the raw CLI flag args.reasoning_parser (empty string when the flag is omitted) straight to OnlineRenderer/OnlineDerenderer. The render entrypoint builds VllmConfig directly from the model config, so unlike create_engine_config it never merges the CLI flag into structured_outputs_config — and model-specific defaults applied during config verification (e.g. openai_gptoss set at model_executor/models/config.py:407 for gpt-oss) were dropped, leaving harmony markup unparsed in derender output.

Regression proven. Head test file applied to base worktree → FAILS with exactly the bug: _Renderer.captured[0]["reasoning_parser"] == '' vs expected 'openai_gptoss' (assertion at test_app_state.py:52). PASSES on head.

Verification on head:

  • Test 1/1 passes: VllmConfig(model_config=ModelConfig("openai/gpt-oss-20b")) resolves structured_outputs_config.reasoning_parser == "openai_gptoss" (confirming the model default survives this construction path), and both OnlineRenderer and OnlineDerenderer receive the resolved value.
  • Precedence correct: args.reasoning_parser or vllm_config.structured_outputs_config.reasoning_parser — an explicit --reasoning-parser still wins; only the empty-flag case falls back to the config-resolved default.
  • StructuredOutputsConfig.reasoning_parser is a dataclass field with str = "" default (structured_outputs.py:35), always present on VllmConfig — no None-risk in the fallback.
  • Test is hermetic (monkeypatched renderer/derenderer/tokenization; the only real object is the ModelConfig/VllmConfig construction, which is CPU-only). ruff check clean.

No issues found — approving.

Comment thread vllm/entrypoints/launchers/render/app_state.py
@shimib
shimib requested a review from hmellor as a code owner September 2, 2026 04:14

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

Nice work @shimib, thanks. Good to merge, once the or (Point 1) is restored. Nothing else blocks.

  1. Put back args.reasoning_parser or ... which was dropped in 9aa30b5. PR description still claims it and it matters for the two exported entry points
  2. PR fixes the render leg too, not just derender. Might be worth claiming?
  3. The PR description claims the following but it doesn't hold: (a) the plugin merge is inert, (b) the special-token leak is conditional
  4. Nit: create_structured_outputs_config() mutates and returns self
  5. test_derender.py:1029 already launches a real gpt-oss render server, just with the flags set. A no-flag variant is the e2e regression

@shimib

shimib commented Sep 2, 2026

Copy link
Copy Markdown
Author

Thanks @hickeyma — all addressed in fc07848: (1) restored the args.reasoning_parser or ... precedence in app_state (the helper refactor stays; this guards callers that build VllmConfig without the entrypoint merge); (4) docstring now states the mutate-and-return-self behavior; (5) added test_e2e_harmony_reasoning_default_parser on a no-flag gpt-oss server as the e2e regression (passes; asserts parsed reasoning and no <|channel|> markup in content). On (2)/(3) you're right on both counts — description updated: the render leg (OnlineRenderer) is fixed too, the --reasoning-parser-plugin merge is noted as inert for the render server (plugin import happens from args in the launcher — retracting that claim from my earlier comment), and the special-token wording is now qualified.

@hickeyma hickeyma 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 @shimib. LGTM.

@mergify

mergify Bot commented Sep 7, 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, @shimib.

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 7, 2026
@shimib
shimib force-pushed the fix/render-server-reasoning-parser-default branch from fc07848 to 65ba1bf Compare September 8, 2026 04:50
@shimib

shimib commented Sep 8, 2026

Copy link
Copy Markdown
Author

Rebased onto main (65ba1bf). The only conflict was in init_render_app_state, where #55012's resolve_default_chat_template_kwargs(args) landed on the lines adjacent to the reasoning_parser fallback — resolved by keeping the new helper's default_chat_template_kwargs local alongside the resolved reasoning_parser for both OnlineRenderer and OnlineDerenderer. No behavioural change from the reviewed version.

Re-verified on the rebased head: tests/entrypoints/launchers/render/test_app_state.py 2 passed, and pre-commit run --files over the six touched files is clean (ruff check/format, mypy, forbidden imports).

Approvals from @Manny7717, @sagearc and @hickeyma still stand — this is ready for the ready label / /ci run whenever a maintainer can kick it off.

@mergify mergify Bot removed the needs-rebase label Sep 8, 2026
@mergify

mergify Bot commented Sep 10, 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, @shimib.

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 10, 2026
shimib and others added 3 commits September 10, 2026 13:37
The GPU-less render server passed the raw --reasoning-parser CLI flag to
OnlineRenderer/OnlineDerenderer, missing model-specific defaults applied
by verify_and_update_config (e.g. openai_gptoss for gpt_oss). The render
entrypoint also builds VllmConfig directly from the model config, so the
CLI flag never reaches structured_outputs_config as it does through
create_engine_config; the flag must therefore take precedence with the
config-resolved value as fallback.

Without this, 'vllm launch render openai/gpt-oss-20b' derendered raw
harmony markup into content (with special tokens on the streaming path)
instead of parsed reasoning and tool calls, diverging from the main API
server.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Shimi Bandiel <shimib@google.com>
…helper

Extract the reasoning-parser flag merge from create_engine_config into
EngineArgs.create_structured_outputs_config() and call it from the render
entrypoint when constructing VllmConfig, per review feedback. app_state now
reads only the config-resolved value, matching the main API server, and
--reasoning-parser-plugin is no longer dropped by the render server.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Shimi Bandiel <shimib@google.com>
Restore 'args.reasoning_parser or ...' in init_render_app_state so callers
that build a VllmConfig without the entrypoint merge still honor an
explicit flag. Document that create_structured_outputs_config mutates and
returns self.structured_outputs_config. Add an e2e regression launching
gpt-oss with no parser flags and asserting parsed reasoning.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Shimi Bandiel <shimib@google.com>
@shimib
shimib force-pushed the fix/render-server-reasoning-parser-default branch from 65ba1bf to f00e90a Compare September 10, 2026 20:40
@mergify mergify Bot removed the needs-rebase label Sep 10, 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.

4 participants