feat(ENG-MM-INPUT-PIPELINE): the multimodal limits reach a live request, so the flags stop being decoration (#607, #686) - #749
Merged
Conversation
…st, so the flags stop being decoration (#607, #686) Wave L2 of #607. L1 ported vLLM's per-modality input limits and the refusal they produce, but nothing constructed a config on a live request, so the numbers were unreachable and the HTTP 400 arm was unproven. This wave adds the two serve flags, the C-ABI fields, and — the part that makes the other two mean anything — the CALL SITE. Three surfaces, each mirroring vLLM at the pin (5559679229bc): 1. `--[no-]language-model-only` and `--limit-mm-per-prompt '<json>'` (arg_utils.py:555-556,1276-1279,1691-1692). Both spellings of the boolean, because _compute_kwargs gives a bool field argparse.BooleanOptionalAction (arg_utils.py:346-348) and a recipe that turns the flag off explicitly must not die on an unknown argument. The dict value is a JSON object (type=parse_type(json.loads), arg_utils.py:375-377); ParseLimitMmPerPromptJson ports _validate_limit_per_prompt (multimodal.py:212-236) and the DummyOptions dataclasses behind it (:17-43), so the legacy count-only, the configurable {"count": N, ...} and the mixed spellings all parse, while a non-object document, a negative count (Field(999, ge=0)), an unknown per-modality option (extra="forbid") or a non-positive one (Field(None, gt=0)) is REFUSED before the model load. A mistyped limit that silently became 999 is a limit that is not there. The profiling options are validated, dropped (only .count feeds get_limit_per_prompt, :335) and the drop is announced per key. 2. vllm_model_params.language_model_only + .limit_mm_per_prompt, appended, so VLLM_ABI_VERSION goes 18 -> 19 and a zero-initialised v18 struct is byte-identical. The JSON-string shape follows the v9 kv_transfer_config precedent: a dict-valued vLLM flag crosses the ABI as its own JSON rather than as a fixed struct of modalities the ABI would then owe forever. Both land on EngineParams::multimodal -> LoadedEngine::mm_config(), so the flags and the ABI resolve ONE config per engine and cannot drift. 3. chat_mm.cpp calls ValidateChatMmLimits — the port of the chat_utils.py:648-662 tracker — as step 0 of MakeQwen3VLImageChatFn, over a BaseProcessingInfo folding the engine's config with the seam's own declared ceiling, Qwen3VLChatSupportedMmLimits() == {"image": 1}. That declaration is the answer to the question #686 left open ("the model's own supported limit; today nothing declares one"): this seam locates a single image part and routes no video or audio, so its honest ceiling is one image and every other modality is absent, which context.py:414-415 reads as limit 0. Closes #686. A three-image chat request is now 400 BadRequestError "At most 1 image(s) may be provided in one prompt." One correction to that issue, found by the RED run and recorded rather than quietly fixed: through the PRODUCTION seam the pre-L2 behaviour was not the truncated 200 the issue describes but an HTTP 500 — the seam injects one placeholder marker per image part while routing only the first image, so ExpandImagePlaceholders raised "more image placeholders than grids" and the client saw a server fault carrying an internal message. The truncated 200 is real for the validate-then-build shape and is pinned as its own test leg. Both are wrong the same way, so the issue's diagnosis stands and only its consequence was understated. NO MEMORY CLAIM. Nothing gates vision-tower construction on the limits, so --language-model-only changes what the server accepts, not what it allocates. That is wave L3 and it is owed with a measured RSS reduction; the docs say so explicitly. Also still owed and named rather than assumed: the second call site, process_inputs_mm (upstream's context.py:461). It needs the per-model get_supported_mm_limits() hook L1 already recorded as absent (the M2 towers own it); wiring it first would mean inventing a supported-limits source inside the engine, which the mirror rule forbids. The chat call site is wired because it HAS a concrete source, and every path the OpenAI server reaches today goes through it. Verified on aarch64 (the build-test-cpu-arm64 CI lane), CPU-only: test_serve_mm_limits 10/10 (101 assertions) -- new test_chat_mm 11/11 (126 assertions) test_openai_api_server 56/56 (638 assertions) test_capi <see PR> -- ABI v19 + the config hop RED first, behaviourally: CHECK_THROWS_AS threw "ExpandImagePlaceholders: more image placeholders than grids" instead of InputValidationError; the HTTP legs failed CHECK(500 == 400), CHECK("InternalServerError" == BadRequestError) and CHECK(200 == 400) -- the last being the truncated 200 itself. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…head (#607, #686) Anchors are gate-unvalidated (#632) and rot on rebase, and this branch rebased twice while the gate ran. Every line reference the wave records was re-read at the final head rather than trusted: * api_server.cpp:185,252 -- both ARE `catch (const vllm::v1::InputValidationError& e)`, which is the claim the whole 400-not-500 argument rests on. * input_processor.cpp:321,352 -- the `mm_features` parameter and the `request.mm_features = std::move(mm_features);` assignment, i.e. the second call site this wave deliberately leaves unwired. * chat_mm.cpp -- the first-image loop MOVED. It was :256-268 before this wave and is :313-326 now, preceded by the new check at :311. The test comment and mm-serving.md said only the old numbers, which after L2 point at neither the code they describe nor the code that replaced it; both now say which is which. The engine-matrix row also gains the anchors the L2 code actually introduces (config/multimodal.cpp:49, chat_mm.cpp:295,311, vllm.h:197,403), so the record names the new surface rather than only the pre-existing hasher anchor. Records only. No behaviour, no test assertion, and no build input changes: the diff is two record files and one comment block. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…view repairs Nothing in the six merged commits touches the multimodal-limits surfaces (MODEL-MM-indextts2 wave, the LTX-2.5 VAE mmap fix, and the Windows real-process test); the merge is taken so the review-repair commits gate against current main rather than against the branch base. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ABI cannot reach, and refused a document vLLM accepts (#607, #686, #758) Review repairs for #749. The L2 deliverable itself is untouched — three surfaces wired, the e2e 400 proven, seven mutations caught. What changed is one false claim on a PERMANENT contract, one real mirror divergence, and three citations. ## 1. include/vllm.h said the v19 fields are "ENFORCED, not recorded" They are not, on the surface that header describes. Verified three ways at this head: `grep -c set_multimodal_chat_fn src/capi/vllm_c.cpp` is 0; the seam is installed exactly once, at `server_main.cpp:1117`; and `serving_chat.cpp:677` gates the whole multimodal branch on `if (mm_chat_fn_)`. So on a C-ABI engine `mm_inputs` stays empty, `ValidateChatMmLimits` never runs, an `image_url` content part is dropped, and `language_model_only=1` changes nothing a `vllm_chat` caller can observe. `test_capi`'s existing case only asserted the config LANDED. Remediation (a) of the two offered: correct the wording. (b) — adding ABI-level enforcement — means building a C-ABI multimodal REQUEST path, which is a new capability and not L2's scope; the header now says that in as many words. The version log and the field block state where enforcement lives (the OpenAI-server path, the one caller that installs the seam), that this ABI has no multimodal request path yet, and exactly what a C-ABI caller gets today: the part parses, is dropped, and the answer is text. So the claim cannot silently become false again, it is PINNED behaviourally, not just reworded: a new `test_capi` case builds a `language_model_only` engine, sends a real `image_url` body through `vllm_chat`, and asserts `VLLM_OK` + a `chat.completion` + no `error` — the opposite of the HTTP 400 the same config produces on the server. Wire the seam into the ABI and it goes red. This is the failure #685's review named — inheriting an unproven end-to-end claim — restated on a contract that cannot be revised later. ## 2. `IsKnownOption` refused unknown option keys for EVERY modality Upstream's `BaseDummyOptions` (`multimodal.py:17-21`), which `_validate_limit_per_prompt`'s `else` at `:233` builds for any modality outside image/video/audio, is the ONE dummy-options dataclass declared WITHOUT `config=ConfigDict(extra="forbid")` — unlike `:24,33,41`. Re-derived rather than taken on trust: I fetched `multimodal.py` at the pinned `5559679229bc`, read the four declarations, and ran them under the same pydantic (2.12.5) the oracle has. BaseDummyOptions(**{"count": 2, "foo": 3}) -> BaseDummyOptions(count=2) ImageDummyOptions(**{"count": 2, "foo": 3}) -> ValidationError BaseDummyOptions(**{"count": -1}) -> ValidationError So `'{"pointcloud": {"count": 2, "foo": 3}}'` is accepted upstream and was refused here, and the code comment asserted the opposite reasoning. Now mirrored: a non-builtin modality's unlisted key is dropped (and ANNOUNCED, since we announce every drop) with its value unvalidated, while the three builtin modalities keep `extra="forbid"` and `Field(None, gt=0)`, and `count` — declared on BaseDummyOptions itself — stays validated for all of them. Pinned by a new `test_serve_mm_limits` case covering both halves, so the repair cannot decay into "accept everything". The `{"tactile": {"count": 1, "width": 2}}` row of the refusal list, which encoded the divergence, is gone. No ABI version bump: v19 is unreleased, landing in this same PR. ## 3-5. Citations and one rendering - `arg_utils.py:375-377` was the `union_dict_and_str` branch. `limit_per_prompt` takes the plain-`dict` branch, `parse_type(json.loads)`, at `:379-381` — `dict[str, BaseDummyOptions]` has no `str` arm and `dict[...]` reports `__module__ == "builtins"`, so `is_not_builtin` is False and `:374-378` is not taken. Fixed in all five places it appeared (the reviewer found four; the fifth is `include/vllm/config/multimodal.h:97`), plus the PR body. - `get_supported_mm_limits` is not on `Qwen3VLProcessingInfo`. It is inherited from `Qwen2VLProcessingInfo` at `qwen2_vl.py:851-852` (`{"image": None, "video": None}`); `qwen3_vl.py:848` subclasses it. - The `chat_mm.h` block claimed the `{"image": 1}` ceiling satisfies AGENTS.md's "an unimplemented arm is refused with a message naming the missing piece". It does not: the message is upstream's generic "At most 0 video(s) may be provided in one prompt.", indistinguishable from a configured limit. The claim is withdrawn and the behaviour is owed to #758, filed for it — naming the arm diverges from a verbatim-ported message three suites assert byte-for-byte, so it takes its own spec rather than a review repair. - While re-deriving, the dataclass anchors themselves were off by one to four lines (`:20` for `count`, `:23,32,39` for the forbid decorators, `:26-28,...` for the option fields, `:17-43` for the block). Corrected everywhere against the lines actually read. - `docs/USAGE.md`'s flag table rendered the hint without the backticks `context.py:426` carries; the console example below it had them. Both now match the real string. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
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.
Wave L2 of #607, and it closes #686.
L1 ported vLLM's per-modality input limits and the refusal they produce, but
nothing constructed a config on a live request — so the numbers were
unreachable, and #685's reviewer correctly recorded that the HTTP 400 arm was
unproven end to end and that "L2's reviewer should treat that as an owed gate,
not an inherited pass." This wave pays that.
Spec:
.agents/specs/multimodal-track.md§1.5(the L2 block now carries the outcome). Issues: #607, #686.
The three surfaces
1. Serve flags —
src/vllm/entrypoints/openai/server_main.cpp5559679229bc--language-model-only/--no-language-model-onlyarg_utils.py:555,1276,1691--limit-mm-per-prompt '<json>'arg_utils.py:556,1279,1692ParseLimitMmPerPromptJson(src/vllm/config/multimodal.cpp)multimodal.py:212-236+ the DummyOptions dataclasses:17-43Both spellings of the boolean, because
_compute_kwargsgives a bool fieldargparse.BooleanOptionalAction(arg_utils.py:346-348) — a recipe that turnsthe flag off explicitly must not die on an unknown argument. The dict value is a
JSON object (
type=parse_type(json.loads),arg_utils.py:379-381— the plain-dictbranch, corrected from:375-377in the review round), and allthree of upstream's object spellings parse: legacy count-only, configurable
{"count": N, …}, and the two mixed.Malformed input is refused before the model load, never defaulted — a
mistyped limit that silently became 999 is a limit that is not there. Every
refusal is upstream's own: a non-object document, a negative count
(
count: int = Field(999, ge=0)), an unknown per-modality option onimage/video/audio (
extra="forbid"; corrected in the review round — every OTHERmodality falls to
BaseDummyOptions, which has noextra="forbid", so itsunknown keys are DROPPED, not refused), a non-positive option (
Field(None, gt=0)). The profilingoptions are validated, dropped (only
.countfeedsget_limit_per_prompt,:335) and the drop is announced per key.kAcceptedInertArgswas checked first and is untouched: #606 deliberatelyexcluded
--language-model-onlyas "a real capability gap", and L2 resolves thatby implementing it rather than by adding an entry, so that table still holds only
genuine no-ops.
.agents/specs/serve-recipe-args.mdis reconciled to say so.NAMED RESIDUAL: upstream's dotted spelling (
--limit-mm-per-prompt.image 2)is a
FlexibleArgumentParserfeature (argparse_utils.py:389-425) that appliesequally to
--kv-transfer-configand--speculative-config, which this serveralso takes as JSON only. Adding it for one flag would be the bespoke path; it
belongs to a parser brick covering all three.
2. C-ABI field —
include/vllm.h,VLLM_ABI_VERSION18 → 19vllm_model_params.language_model_only(int32_t) and.limit_mm_per_prompt(
const char*, the same JSON object the flag takes). Appended at the end of thestruct, following the convention v14/v16/v18 set: a versioned
/* ── … (ABI v19) ── */block on the field, av19 — …paragraph in the header's version logstating what a zero-initialised v18 struct gets, and the JSON-string shape
following the v9
kv_transfer_configprecedent — a dict-valued vLLM flagcrosses this ABI as its own JSON rather than as a fixed struct of modalities the
ABI would then owe forever. Malformed input fails
vllm_engine_loadwithVLLM_ERR_INVALID_ARGUMENT.Both surfaces land on
EngineParams::multimodal→LoadedEngine::mm_config(),so the flags and the ABI resolve one config object per engine and cannot
drift.
3. The call site —
src/vllm/entrypoints/openai/chat_mm.cppMakeQwen3VLImageChatFnnow runsValidateChatMmLimits(the port of thechat_utils.py:648-662tracker) as step 0, over aBaseProcessingInfofoldingthe engine's config with the seam's own declared ceiling,
Qwen3VLChatSupportedMmLimits() == {"image": 1}.That declaration answers the question #686 left open — "the model's own
supported limit (the
min()fold's other operand) — today nothing declaresone". This seam locates a single image part and routes no video or audio, so its
honest ceiling is one image and every other modality is absent, which
context.py:414-415reads as limit 0. A user limit can only lower it, so--limit-mm-per-prompt '{"image": 99}'still refuses the second image — and thencorrectly omits the
Set --limit-mm-per-prompt to increase this limit.hint,because raising the user's limit would not help.
The e2e 400 test, and exactly what it distinguishes
tests/vllm/entrypoints/openai/test_api_server.cpp, three legs through the realApiServer::handle_chat_completions:MakeQwen3VLImageChatFn) + a three-image body→
400/BadRequestError/"At most 1 image(s) may be provided in one prompt.", and asserted!= 500and!= 200and nochoiceskey;200with a real completion,so the 400 is a limit decision and not "multimodal is off"; the same seam
then refuses three;
500/InternalServerError, so "status == 400" cannot be satisfied by a handlerthat answers 400 to every seam failure.
Plus
--language-model-only→400 "At most 0 image(s) … Set --limit-mm-per-prompt to increase this limit."with a text request on the sameserver still
200.RED first, behaviourally (these are verbatim from the pre-implementation
run):
A correction to #686, recorded rather than quietly fixed
The RED run says something the issue does not. Against the production seam
the pre-L2 behaviour was not the truncated 200 #686 describes — it was an
HTTP 500:
MakeQwen3VLImageChatFninjects one placeholder marker per imagepart while routing only the first image, so
ExpandImagePlaceholdersraised"more image placeholders than grids" and the client got a server fault carrying
an internal message. The truncated 200 is real for the validate-then-build seam
shape and is pinned as its own leg (the
CHECK( 200 == 400 )above). Both arewrong the same way — neither is upstream's refusal — so the issue's diagnosis
(first image,
break, no refusal) stands and only its stated consequence wasunderstated.
.agents/specs/mm-serving.mdis corrected accordingly, as the issueasked.
Mutation proof
Restored with
cp+touch(notcp -a, which preserves mtime and makes ninjaskip the relink), rebuilt and re-verified green between every mutation. All four
mutated files verified byte-for-byte identical to the working tree afterwards by
md5.
a.multimodal.language_model_only = false;test_serve_mm_limits8/10, 7 assertionsCHECK(Contains(run.output, "server: multimodal limits language-model-only=ON")),image=0, andCHECK_FALSE(Contains(run.output, "image=4"))in the precedence casemm_config_()instead ofmm_config_(params.multimodal)test_capi56/57, 3 assertionsCHECK(e.mm_config().GetLimitPerPrompt("image") == 2)test_openai_api_server54/56 andtest_chat_mm9/11CHECK(refused.status == 400)→500;CHECK(j.at("error").at("message") == "At most 1 image(s)…")std::runtime_errortest_openai_api_server54/56CHECK(refused.status == 400)→CHECK(500 == 400)andCHECK("InternalServerError" == BadRequestError)(c) is the one worth reading twice: it is exactly the failure mode L1's
single-refusal-type design avoided, and it is now pinned by a live call site. Note
the message text survived unchanged — a test asserting only the message would
have passed it. The status assertion is what caught it. An existing guard also
catches it earlier: L1's
test_processing_limitsgoes 5/19 under the samemutation, so the unit layer sees it too; the new e2e leg is what proves the HTTP
consequence.
Gates — verified on aarch64, not x86
Ran on
kairos-4db2(Thor, 14 cores) in the bakedvllmcpp-build:aarch64container, CPU-only (
-DVLLM_CPP_CUDA=OFF). aarch64 is a CI-supported lane(
build-test-cpu-arm64), so this is legitimate coverage, but it is not anx86 run.
include/vllm.hchanged) — 1326/1326 targets,0 warnings, 0 errors under
-Werror.ctest -j 6: 446/447 passed, 2 skipped.test_serve_mm_limits10/10 (101 assertions) — newtest_chat_mm11/11 (126 assertions)test_openai_api_server56/56 (638 assertions)test_capi57/57 (528 assertions)The one failure is not mine and is already tracked.
test_op_paritythrows[json.exception.type_error.302] type must be string, but is nullattests/parity/test_op_parity.cpp:1989. I built and ran it from a pristineorigin/maintree on the same host: identical failure, same file, same line.That is #737 ("main red: test_op_parity throws on the MiniMax-Music3 golden
manifest"), filed earlier today.
Checkers, run explicitly:
check-windows-portability.py— the only errors aresrc/vllm/multimodal/video_engine.cpp:21,59,64(VideoEngine uses unguarded POSIX stat in native Windows build #648, pre-existing).None of my files appear.
check-device-leakage.py—OK (DSR 32 == baseline 32, ratchet holds). Myfiles contribute zero DSR entries; the
server_main.cppDSR-ALLOWs inthe listing are the pre-existing
VT_BENCH_PROFILE_CONTROLguards whose linenumbers shifted.
check-surface-coverage.py— OK.check-doc-checkpoint.py --commit HEAD— OK.check-commit-trailers.py --range $(git merge-base origin/main HEAD)..HEAD—OK: commit trailer contract(run explicitly, per agent-preflight silently SKIPS commit-trailers when the branch is behind main, and still prints "All gates green" #653).What this does NOT claim
No memory win. Nothing gates vision-tower construction on the limits, so
--language-model-onlychanges what the server accepts, not what itallocates. That is wave L3 and it is owed with a measured RSS reduction.
docs/USAGE.md,docs/STATUS.mdand the roadmap row all say so explicitly.One call site of two.
process_inputs_mm(input_processor.cpp:321,352,upstream's
context.py:461) is still unwired, and this is a scope judgement areviewer should rule on rather than a silent omission. It needs the per-model
get_supported_mm_limits()hook that L1 already recorded as absent (the modelsthat would implement it are the M2 towers). Wiring it first would mean inventing
a supported-limits source inside the engine, which the mirror rule forbids. The
chat call site is wired because it has a concrete source — the seam's own
implemented arm — and every path the OpenAI server can reach today goes through
it.
L4 (the
qwen3_next.py:325kernel gate) is untouched, as scoped.🤖 Generated with Claude Code
Review repairs (
e8e6c891a, aarch64)The fresh review returned FAIL on finding 1 with finding 2 a real mirror
divergence, and confirmed the core L2 deliverable sound. The implementation is
unchanged apart from one parser rule; everything else was a false or imprecise
claim.
1 (MEDIUM, blocking) —
include/vllm.hclaimed an enforcement the C ABIcannot reach. The v19 block said the two fields are "ENFORCED, not recorded"
and that an engine loaded with
language_model_onlyanswers a multimodalrequest with At most 0 image(s)…. Re-verified at this head:
grep -c set_multimodal_chat_fn src/capi/vllm_c.cppis 0, the seam isinstalled exactly once at
server_main.cpp:1117, andserving_chat.cpp:677gates the whole multimodal branch on
if (mm_chat_fn_). So on a C-ABI enginemm_inputsstays empty,ValidateChatMmLimitsnever runs, animage_urlpartis dropped, and the flag changes nothing a
vllm_chatcaller can observe.Took remediation (a), correct the wording, not (b). (b) means building a
C-ABI multimodal request path — a new capability, not an L2 repair — and the
header now says exactly that, alongside where enforcement does live and what a
C-ABI caller actually gets. It is pinned behaviourally, not just reworded: a
new
test_capicase builds alanguage_model_onlyengine, sends a realimage_urlchat body throughvllm_chat, and assertsVLLM_OK+ achat.completion+ noerror— the opposite of the 400 the same configproduces on the server path.
2 (LOW, real divergence) —
extra="forbid"is the builtin modalities' rule,not a global one. Re-derived rather than inherited: fetched
multimodal.pyatthe pinned
5559679229bc, read the four declarations, and ran them underpydantic 2.12.5.
BaseDummyOptions(:17-21) — what_validate_limit_per_prompt'selseat:233builds for any modality outside image/video/audio — is the only one ofthe four declared without
config=ConfigDict(extra="forbid")(cf.:24,33,41).So
'{"pointcloud": {"count": 2, "foo": 3}}'is accepted upstream and wasrefused here. Now mirrored: a non-builtin modality's unlisted key is dropped
(and announced, as every drop is) with its value unvalidated; the three builtin
modalities keep
extra="forbid"+Field(None, gt=0);count— declared onBaseDummyOptionsitself — stays validated for all of them. No ABI bump: v19 isunreleased and lands in this PR.
3 — the
arg_utils.pycitation.:375-377is inside theunion_dict_and_strbranch (:374-378).limit_per_prompt: dict[str, BaseDummyOptions]takes the plain-dictbranch,parse_type(json.loads), at:379-381— the annotation has nostrarm, anddict[…]reports__module__ == "builtins", sois_not_builtinis False. Fixed in all fiveplaces it appeared (the review named four; the fifth was
include/vllm/config/multimodal.h:97) and above in this body.4 — the ceiling is a number, not yet a message.
chat_mm.hclaimedQwen3VLChatSupportedMmLimits()satisfies AGENTS.md's "an unimplemented arm isrefused with a message naming the missing piece". It does not: the client gets
upstream's generic At most 0 video(s) may be provided in one prompt.,
indistinguishable from a configured limit — the only present signal is the
withheld
--limit-mm-per-prompthint. Claim withdrawn, behaviour owed to#758, filed for it. Not fixed in flow deliberately: naming the arm diverges
from a verbatim-ported message three suites assert byte-for-byte, so it takes
its own spec and fresh review. Also corrected there —
get_supported_mm_limitsis not onQwen3VLProcessingInfo; it is inheritedfrom
Qwen2VLProcessingInfoatqwen2_vl.py:851-852({"image": None, "video": None}), whichqwen3_vl.py:848subclasses.5 —
docs/USAGE.mdrendered the hint without the backtickscontext.py:426carries; the console example below it had them. Both match thereal string now.
While re-deriving, the dataclass anchors themselves proved off by one to four
lines (
:20forcount,:23,32,39for the forbid decorators,:26-28,…forthe option fields,
:17-43for the block). Corrected everywhere against thelines actually read.
Repair-round gates — aarch64, not x86
kairos-4db2(Thor, 14 cores), bakedvllmcpp-build:aarch64, CPU-only(
-DVLLM_CPP_CUDA=OFF).include/vllm.hchanged): 1355/1355 targets,0 warnings, 0 errors under
-Werror.ctest -j 6: 456/457, 2 skipped. The one failure istest_op_parity(
[json.exception.type_error.302]attest_op_parity.cpp:1989) — main red: test_op_parity throws on the MiniMax-Music3 golden manifest — null where a string is required #737,already reproduced from pristine main.
test_serve_mm_limits11/11 (109 assertions) ·test_chat_mm11/11(126) ·
test_capi58/58 (536) ·test_openai_api_server56/56 ·test_processing_limits19/19 (78) ·test_multimodal_config7/7 (21).A correction to this PR's own gate line.
test_openai_api_server'sassertion count is not a pin: three runs of one binary gave 632 / 648 / 651,
because SSE-chunk loops assert per chunk received. Its case count, 56/56, is
stable and is the number to quote. The 638 recorded at the L2 landing was one
sample of that spread, not a target.
Checkers, run explicitly:
check-commit-trailers --range $(git merge-base origin/main HEAD)..HEADOK (per #653) ·check-doc-checkpoint --commit HEADOK ·
check-device-leakageOK (DSR 32 == baseline 32) ·check-windows-portabilityOK (verified, not assumed — #648 fixed it) ·check-public-doc-tables/check-surface-coverage/check-agent-recordOK.Preflight's remaining reds are
audit-live-rows/test_audit_live_rows(#731; the stale ACTIVE row it names is
MODEL-MUSIC-minimax-music3-…, not mine) andtest_cpu_x86_llamacpp_floor(#618,
NO_QUIET_WINDOWat load 209 on a contended box).Mutations
cp+touch+ rebuild, re-verified green between each, both files md5-identicalto the working tree afterwards.
IsBuiltinModalityforced totrue— the pre-repair "forbid extras everywhere"test_serve_mm_limits10 passed / 1 failed, Status FAILURElimit_mm_per_prompt: "pointcloud" has no option "foo"vllm_chat— the change that would make the v19 paragraph falsetest_capi57 passed / 1 failedREQUIRE( st == VLLM_OK )→REQUIRE( 1 == 0 ), loggingvllm_chat: At most 0 image(s) may be provided in one prompt.Worth reading twice: under the first mutation doctest printed
assertions: 100 | 100 passed | 0 failed— a thrown case contributes nofailed assertion. Only the case count and
Status: FAILURE!caught it.An earlier form of that mutation (deleting the escape block outright) failed to
build on
-Werror=unused-function, and the stale binary re-ran and printedStatus: SUCCESS!— the mutation was re-shaped to keep the function usedrather than accepted as "not caught".