Row: -
A PNG, a JPEG, or an http(s) image URL sent to a Qwen3-VL or dots3-note server comes back as HTTP 500, not 400.
oai::DefaultImageCodec refuses container formats by design — the PNG/JPEG→RGB decode is a named residual, and refusing is the correct behaviour under AGENTS.md §"Shared seams". The defect is the STATUS, not the refusal. A request the server cannot serve because of the request's own content is a client error; a 500 tells the caller to retry and tells an operator to go looking for a crash that did not happen.
Where it comes from:
src/vllm/entrypoints/openai/mm_chat_qwen3vl.cpp:64 and :76 throw a bare std::runtime_error, which falls through to the generic handler and becomes 500.
src/vllm/entrypoints/openai/mm_chat_dots3note.cpp is inconsistent with itself: it converts to vllm::v1::InputValidationError at :273, with a comment that says exactly why ("a bare runtime_error falls through"), and still throws std::runtime_error at :379.
src/vllm/entrypoints/openai/mm_chat_deepseek_v4.cpp does it correctly as of #2411 W5: it catches the codec's std::invalid_argument and the data-URI failures and re-throws vllm::v1::InputValidationError, which the server renders as 400 with a message naming the unsupported container format.
Found while wiring the DeepSeek-V4 vision seam (#2411 W5). That wave fixed its own seam and deliberately did not widen scope into the other two, which is why this issue exists rather than a silent drive-by edit.
Suggested shape: the conversion belongs in one place rather than three. Either the install's catch normalises a codec refusal, or the codec throws a type the seams do not each have to remember to translate. Whichever is chosen, the fix wants a case per seam that asserts the status code, because the current dots3-note file shows that knowing the rule is not enough to apply it consistently.
Row:
-A PNG, a JPEG, or an
http(s)image URL sent to a Qwen3-VL or dots3-note server comes back as HTTP 500, not 400.oai::DefaultImageCodecrefuses container formats by design — the PNG/JPEG→RGB decode is a named residual, and refusing is the correct behaviour under AGENTS.md §"Shared seams". The defect is the STATUS, not the refusal. A request the server cannot serve because of the request's own content is a client error; a 500 tells the caller to retry and tells an operator to go looking for a crash that did not happen.Where it comes from:
src/vllm/entrypoints/openai/mm_chat_qwen3vl.cpp:64and:76throw a barestd::runtime_error, which falls through to the generic handler and becomes 500.src/vllm/entrypoints/openai/mm_chat_dots3note.cppis inconsistent with itself: it converts tovllm::v1::InputValidationErrorat:273, with a comment that says exactly why ("a bareruntime_errorfalls through"), and still throwsstd::runtime_errorat:379.src/vllm/entrypoints/openai/mm_chat_deepseek_v4.cppdoes it correctly as of #2411 W5: it catches the codec'sstd::invalid_argumentand the data-URI failures and re-throwsvllm::v1::InputValidationError, which the server renders as 400 with a message naming the unsupported container format.Found while wiring the DeepSeek-V4 vision seam (#2411 W5). That wave fixed its own seam and deliberately did not widen scope into the other two, which is why this issue exists rather than a silent drive-by edit.
Suggested shape: the conversion belongs in one place rather than three. Either the install's catch normalises a codec refusal, or the codec throws a type the seams do not each have to remember to translate. Whichever is chosen, the fix wants a case per seam that asserts the status code, because the current dots3-note file shows that knowing the rule is not enough to apply it consistently.