[Bugfix][Parser] kimi_k2: route streaming through arg_converter so schema type coercion applies - #49318
Open
muhammadfawaz1 wants to merge 1 commit into
Open
[Bugfix][Parser] kimi_k2: route streaming through arg_converter so schema type coercion applies#49318muhammadfawaz1 wants to merge 1 commit into
muhammadfawaz1 wants to merge 1 commit into
Conversation
…chema type coercion Streaming previously skipped schema type coercion for kimi_k2 since it has no arg_converter; non-streaming always coerced via _fix_arg_types. Same model output, different typed values depending on client mode. Fixes silent type mismatch by giving kimi_k2 a converter that normalizes to canonical JSON, routing it through the same converter-present machinery PR vllm-project#48706 already hardened. Co-authored-by: Mahad Rehman <mahadrehmann@users.noreply.github.com> Signed-off-by: muhammadfawaz1 <135441198+muhammadfawaz1@users.noreply.github.com>
muhammadfawaz1
marked this pull request as ready for review
July 23, 2026 07:24
muhammadfawaz1
requested review from
aarnphm,
bbrowning and
sfeng33
as code owners
July 23, 2026 07:24
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
This branch has not been deployed
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.
Summary
kimi_k2 was the only Streaming Parser Engine backend with
tool_args_json=Trueand noarg_converter. Because_compute_arg_deltaand_flush_arg_converterboth return early whenarg_converter is None(before ever reaching_fix_arg_types), streaming emitted the model's raw JSON value types unchanged, while non-streaming always coerces to the tool schema via_build_extracted_result. Same model output, different typed values depending on whether the client streams or not — silent, since the output is still valid JSON.Full root cause and reproduction: #49316
Fix
Entirely in
vllm/parser/kimi_k2.py. Zero changes toparser_engine.py._kimi_k2_arg_converter(raw_args, partial): normalizes kimi's raw JSON to canonicaljson.dumpsoutput, usingpartial_json_parser(already a project dependency — seerequirements/common.txt, used by 7 other tool parsers) to complete incomplete objects whenjson.loadsfails on partial input. Unparsable text is returned unchanged so no argument bytes are lost.arg_converter=_kimi_k2_arg_converterinkimi_k2_config._handle_arg_chunkoverride, which emitted the first arg chunk as rawevent.value, bypassing any converter. Once a converter exists this duplicated output.Why this approach
Once kimi has a converter, it flows through the same converter-present machinery PR #48706 already hardened:
_fix_arg_typescoercion,_safe_arg_prefix's withholding of unstable trailing values, and thestartswithprefix invariant. Because the converter re-emits canonicaljson.dumpsoutput on every tick (both partial and flush), and non-streaming routes through the same converter, both paths produce byte-identical concatenated output by construction, rather than needing to be kept in sync by hand.Alternative considered and rejected: raw-text withholding
Adapting PR #48706's
non_string_clipidea directly to the no-converter path (withhold non-string field values from the raw stream, emit them corrected at flush) was the first approach tried. It doesn't work: once a field coerces,_fix_arg_typesre-serializes the entire object withjson.dumpsspacing ({"a":"x",...}→{"a": "x", ...}), but partial raw JSON can't be run through_fix_arg_typesmid-stream (it isn't valid JSON yet), so the raw no-space prefix already streamed is never a valid prefix of the eventual coerced-and-spaced output. Thestartswithcheck then fails and silently truncates — reproducing the exact #48702 failure mode in a new place. The only prefix that's safe under both outcomes turns out to be the opening{"firstkey":, which defeats progressive streaming entirely. This is unavoidable specifically because of middle (not just trailing) non-string fields — verified by direct construction, not just argued.Alternative considered and rejected: minimal in-place coercion
Coercing values in place while preserving the model's original raw formatting (no re-spacing) would require either changing
_fix_arg_typesitself (shared with the converter-present path — out of scope and risks regressing #48706's behavior) or accepting that kimi's non-streaming output diverges from the canonical format everywhere else in the engine.Known behavior change (intentional, flagged for review)
Non-streaming output for untyped/already-correct fields is now whitespace-normalized (
{"city":"Tokyo"}→{"city": "Tokyo"}) — a direct consequence of routing both paths through the same canonicaljson.dumpsconverter. Already-coerced field output is unchanged ({"count": 3}stays{"count": 3}). No existing test asserts on the old compact non-streaming format for kimi_k2 (checked:tests/parser/,tests/reasoning/test_kimi_k2_reasoning_parser.py,tests/entrypoints/unit_tests/test_chat_utils.pyall pass unchanged). Flagging this explicitly in case a maintainer considers exact non-streaming byte format a compatibility concern.Testing
tests/parser/engine/test_kimi_k2_streaming.py, 27 deterministic, byte-level tests at chunk sizes 1/3/1000, covering: single non-string field (int/bool/number), middle (non-trailing) non-string field, nested non-string field, parallel tool calls, string field with a numeric-looking literal, already-correct-types control, and truncated/never-closed JSON.git stash+ run → 21/27 fail on unpatchedmain(every coercion scenario);git stash pop+ run → 27/27 pass.tests/parser/engine/— 3699 passed, 0 regressions (includes all converter-present deepseek/qwen/gemma streaming tests, confirming this change doesn't touch that path's behavior).tests/parser/test_parse.py,tests/parser/test_streaming.py,tests/reasoning/test_kimi_k2_reasoning_parser.py: pass, unchanged.tests/entrypoints/unit_tests/test_chat_utils.py: 12 failed / 7 errored, identically with and without this change applied (confirmed via stash-diff) — pre-existing environment gap (missing Torchvision), not a regression.ruff check+ruff format: clean.Not yet verified
Real-tokenizer reasoning/tool-parser suites requiring
moonshotai/*tokenizer download couldn't be run in this environment (no network access to HuggingFace from the dev machine). Logic exercised is the same code path covered by the mock-tokenizer tests above, but a maintainer with the model cached should run these before merge.Not a duplicate of
arg_converter-present path (_flush_arg_converter'sstartswith(prev)invariant breaking when coercion shrinks JSON). This bug is in thearg_converter is Nonepath, which never calls_fix_arg_typesat all, regardless of any prefix invariant. Confirmed no code overlap:git diffof [Bugfix][Parser] Fix streaming tool-call args truncated by schema coercion #48706 contains no reference to thearg_converter is Nonebranch this PR modifies.Nonecoercion).