fix(server): flush reasoning parser at stream end (supersedes #653) - #663
Closed
janhilgard wants to merge 1 commit into
Closed
janhilgard wants to merge 1 commit into
janhilgard wants to merge 1 commit into
Conversation
Flush parser-buffered terminal text before emitting a streamed completion's terminal chunk. Gemma 4's reasoning parser already implements finalize_stream() for a truncated channel marker, but stream_chat_completion() did not invoke it. Guard the invocation so third-party or test-only reasoning parsers that duck-type the interface without inheriting from ReasoningParser still stream correctly (default no-op behavior). ## Reproduction With Gemma 4 reasoning enabled, a stream that ends at finish_reason=length after the parser has buffered a partial channel marker could omit that buffered tail from the client stream. The added regression simulates a Gemma thought-to-response transition that ends at a length boundary. It verifies the thought text, the buffered final content, and the terminal length reason all reach the emitted stream. ## Scope - Preserves template selection, sampling, request limits, and scheduler behavior. - Does not change Jobs integration, model configuration, or constrained decoding. - Does not claim that every length-bound Gemma generation produces final content; it preserves parser-buffered terminal content when it exists. ## Validation - Full test suite passes on apple-silicon; the four previously failing tests (test_reasoning_stream_emits_structured_tool_calls, test_reasoning_stream_redirects_gemma4_tool_marker, test_reasoning_stream_skips_tool_parser_until_markup_appears, test_response_format_stream_promotes_reasoning_json_to_content) now pass because the getattr guard tolerates duck-typed test fixtures without finalize_stream. - The new test (test_gemma_stream_flushes_pending_content_before_length_finish) exercises the intended Gemma 4 buffered-tail flush. Closes waybarrios#653. Co-Authored-By: janhilgard <jan.hilgard@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #653 with the CI failure resolved.
Background
@Thump604 opened #653 on 2026-07-28 to flush parser-buffered terminal text (e.g. Gemma 4's truncated
<|channel>markers) before emitting the terminal chunk of a stream. The fix is correct and needed — Gemma 4'sfinalize_stream()already implements the fallback,stream_chat_completionjust wasn't calling it.CI on that PR broke four existing streaming tests:
All four failed with
AttributeError: 'FakeReasoningParser'/'ReasoningOnlyParser' object has no attribute 'finalize_stream'.Root cause
Those tests declare inline test fixtures (
FakeReasoningParser,ReasoningOnlyParser) that duck-typeextract_reasoning_streamingandreset_statebut don't inherit fromReasoningParser. The base class providesfinalize_stream()as a documented no-op, so any parser that does inherit gets the default automatically. #653 calledreasoning_parser.finalize_stream()unconditionally, which broke the duck-typed fixtures.The alternatives are:
hasattr/getattrguard in_resolve_reasoning_stream_delta.finalize_stream = lambda self: Nonestubs to every fake fixture.I picked (1). The base class's own docstring establishes
finalize_streamas an optional extension point ("Default implementation is a no-op"), so the calling code should tolerate parsers that don't implement it — that keeps the interface contract loose for third-party parsers as well, not only tests.Change
One-line change in
_resolve_reasoning_stream_delta(vllm_mlx/server.py):Everything else is @Thump604's original diff, unchanged. Authorship on this commit is preserved as
Thump604 <...>; I'm listed asCo-Authored-Byfor the guard change.Validation
Ran the 5 relevant tests locally (Python 3.14, macOS ARM64, uv-installed dev extras against fresh clone):
The four previously-failing tests now pass without modification — the guard tolerates duck-typed fixtures. The new test (@Thump604's Gemma flush regression) still passes as designed.
Not claimed
Same non-claims as #653:
Followup
Please close #653 after merging this. The intent is identical; this is just a rebased single-commit variant with the CI regression fixed and the base-class no-op contract respected.
Closes #653.