diff --git a/.agents/roadmap_v1.md b/.agents/roadmap_v1.md index 7aca46f69..3177db62a 100644 --- a/.agents/roadmap_v1.md +++ b/.agents/roadmap_v1.md @@ -196,6 +196,7 @@ issue is not yet placed. Keyed record: update in place, never append. | [#652](https://github.com/mudler/vllm.cpp/issues/652) | — | `model-matrix.md` prose counters drifted: LTX-2.5 reached the rows and the CI-enforced rollup but none of the five sentences that count them | bug | | [#659](https://github.com/mudler/vllm.cpp/issues/659) | — | LTX-2.5 device select adopts M3a's platform seam but not its companion capability guard: `ltx2_video.cpp` asks `CurrentPlatform().device_type()` and `TryGetBackend(...)` but never `supports_model_architecture`, so a PARTIAL backend (Metal 15/75 ops, Tenstorrent) is handed a queue and dies in a kernel bind where it used to be refused BY NAME (found while reviewing #553 for landing) | bug | | [#660](https://github.com/mudler/vllm.cpp/issues/660) | — | `check-device-leakage`'s `kcuda` bucket is the token grep `\bkCUDA\b`, so `minimax_h3_video.cpp:221-226`'s `static_cast(device)` hardcodes CUDA as enum value 1 and counts as 0. Gate strength plus an enum-ordering hazard; the H3 video lane should ask the same seam `ltx2_video.cpp` now does (found while reviewing #553) | bug | +| [#664](https://github.com/mudler/vllm.cpp/issues/664) | — | **FIXED 2026-08-13, `row/FIX-WINDOWS-POSIX-VIDEO-ENGINE`.** `windows-msvc-cpu` / `windows-msvc-vulkan` were RED on EVERY open PR (9 sampled across 5 unrelated lanes) from ONE file: `video_engine.cpp` reached Windows with ``, `::stat` and `S_ISDIR` (landed `cefacd2d0`), which `check-windows-portability.py:1675-1688` flags under `full_source_posix` — every scanned source, not only the platform-boundary set. `main` was never a denominator because the Windows jobs are PR-only and `skipped` on push (#584). Repaired at the SOURCE, not the checker: `IsDir`/`Exists` now take the `std::error_code` overloads through a file-local `NativePath`, which preserves `::stat`'s return-false-for-an-uninspectable-path behaviour that the THROWING overloads would have turned into a `filesystem_error` escaping a registry query | bug | | [#608](https://github.com/mudler/vllm.cpp/issues/608) | `TOOLS-PARSER-BREADTH` | **W0 (record backfill) landed 2026-08-13; row `INVENTORIED` → `PARTIAL`. W1 then shipped `inkling`, taking the registry to 42 names / 38 families.** Re-derived from the two REGISTRIES rather than from recipe usage: **five** `--tool-call-parser` names are upstream-only at the pin — `openai`, `inkling`, `minimax_m3` (W1, recipe demand) and `cohere_command3`, `cohere_command4` (W2, ZERO recipe uses, so usage-driven audits miss them). **Only `inkling` was PORTABLE from vLLM source, and it LANDED** (a `ParserEngineToolAdapter` over the already-ported Inkling engine; the gap was the registry face, not the grammar). Of the four left: `minimax_m3` is backed by the Rust crate; `openai` is a stub delegating to `vllm/parser/harmony.py`, which IS vLLM source but wraps the out-of-tree `openai_harmony` package (the SGLang secondary-oracle check was run and REFUSED — vLLM implements this path, so the rule does not admit a secondary); both Cohere names are shims over the out-of-tree `cohere_melody` package. So W1-remaining/W2 each owe a recorded decision before code rather than a text port. `nemotron_json`, `kimi_k3` and `ling3` are in NEITHER registry and arrive with the pin advance, not here. W3 ports upstream's shared `ToolParserTestConfig` harness. The earlier "six missing" framing was usage-derived: it listed `nemotron_json` as portable (it is not registered at the pin) and missed both Cohere entries | feature | | [#647](https://github.com/mudler/vllm.cpp/issues/647) | — | Oracle policy had no fallback and no pin concept: five upstreams beyond vLLM are already compared against (vLLM-Omni, SGLang, llama.cpp, `transformers`, tt-forge) with their pins scattered across individual specs or absent entirely. AGENTS.md now admits a named secondary oracle where vLLM implements nothing, `.agents/oracles/.md` pins each one file-per-oracle, and `check-oracle-pins.py` enforces both directions. The gateability debts for `sglang`, `diffusers` and `tt-forge` stay open on this issue | feature | | [#672](https://github.com/mudler/vllm.cpp/issues/672) | `MODEL-MUSIC-minimax-music3-mini-max-music3-for-conditional-generation` | MiniMax-Music3: port the text-to-music lane — ~8.6B Qwen3 global LLM (our LANDED architecture at vocab 200000) + 8-codebook RVQ depth decoder + 2.4B fp32 flow-matching DiT + DAC Flow-VAE. Our first music-generating model, and the first row whose primary oracle is not vLLM: absent from the pin, from vLLM `main` and from `vllm-omni`, so it gates against the OPEN diffusers PR #14456 with SGLang-Omni as the e2e cross-check, under the fallback rule added in #647 | feature | diff --git a/src/vllm/multimodal/video_engine.cpp b/src/vllm/multimodal/video_engine.cpp index 656a312f1..62604274a 100644 --- a/src/vllm/multimodal/video_engine.cpp +++ b/src/vllm/multimodal/video_engine.cpp @@ -11,15 +11,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include -#include - #include #include "vllm/entrypoints/openai/video_api.h" @@ -54,14 +54,44 @@ std::vector& RegistryStorage() { // detector may claim a checkpoint and exactly one entry may carry a name. const std::vector& OrderedRegistry() { return RegistryStorage(); } +// A caller's UTF-8 path as a native filesystem path. Byte-for-byte the spelling +// in v1/kv_offload/fs_io.cpp:31, and the same job the loader lane's Utf8Path +// does (gguf_reader.cpp:22, safetensors_reader.cpp:29) — no shared helper +// exists to call, because each is file-local to its own TU. The u8string step +// is not decoration: on Windows a narrow std::string handed to +// std::filesystem::path is interpreted in the ACTIVE CODE PAGE, so every +// non-ASCII checkpoint path silently resolves to the wrong file (or to none). +// Saying char8_t makes the UTF-8 explicit and the conversion to UTF-16 exact. +std::filesystem::path NativePath(const std::string& utf8) { +#if defined(_WIN32) + const std::u8string value(reinterpret_cast(utf8.data()), utf8.size()); + return std::filesystem::path(value); +#else + return std::filesystem::path(utf8); +#endif +} + +// BOTH probes take the std::error_code overloads, and that is load-bearing +// rather than stylistic. The `::stat` calls these replaced reported an +// uninspectable path — ENAMETOOLONG, ELOOP, EACCES on a parent — by returning +// -1, which arrived here as a plain `false` and became the registry's ordinary +// "no such file or directory" refusal. The THROWING +// std::filesystem::exists(p) / is_directory(p) overloads raise +// filesystem_error for exactly those cases instead, which would escape +// ReadVideoCheckpointTensorNames — whose header contract is to return false +// with *why set — and, through DescribeCheckpoint, escape LoadVideoEngine in +// place of the refusal that names the registered families. Returning false on +// error keeps the POSIX behaviour these calls had. Issue #664. bool IsDir(const std::string& path) { - struct stat st {}; - return ::stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode); + std::error_code error; + const bool result = std::filesystem::is_directory(NativePath(path), error); + return !error && result; } bool Exists(const std::string& path) { - struct stat st {}; - return ::stat(path.c_str(), &st) == 0; + std::error_code error; + const bool result = std::filesystem::exists(NativePath(path), error); + return !error && result; } std::string StripTrailingSlash(const std::string& dir) { diff --git a/tests/vllm/multimodal/test_video_engine.cpp b/tests/vllm/multimodal/test_video_engine.cpp index 8854fd3a8..6df8af57f 100644 --- a/tests/vllm/multimodal/test_video_engine.cpp +++ b/tests/vllm/multimodal/test_video_engine.cpp @@ -256,6 +256,52 @@ TEST_CASE("video engine registry: an unrecognizable checkpoint refuses instead o } } +// A path the OS cannot inspect AT ALL — one 300-character component, which is +// ENAMETOOLONG rather than ENOENT. This is the case that separates a REFUSAL +// from a THROW, and it is why the registry's existence probes must use the +// std::error_code overloads of : +// +// ::stat(path, &st) -> -1, so the probe returned false +// std::filesystem::exists(p, ec) -> false, ec = "File name too long" +// std::filesystem::exists(p) [throwing] -> THROWS filesystem_error +// +// The header's contract for ReadVideoCheckpointTensorNames is "returns false +// with *why holding the reason when the artifact cannot be enumerated" +// (include/vllm/multimodal/video_engine.h). A throwing overload silently trades +// that documented refusal for an exception escaping a registry query, and no +// token- or golden-based gate would ever see it, because every checkpoint those +// gates hand over is perfectly stattable. Issue #664. +TEST_CASE("video engine registry: an UNINSPECTABLE path refuses, and never throws") { + SeamWorkspace ws; + const std::string unstattable = ws.root + "/" + std::string(300, 'x'); + + std::vector names{"stale"}; + std::string why; + bool enumerated = true; + REQUIRE_NOTHROW(enumerated = vllm::multimodal::ReadVideoCheckpointTensorNames( + unstattable, &names, &why)); + CHECK_FALSE(enumerated); + CHECK(names.empty()); + CHECK(why == "no such file or directory"); + + // ...and the same path through the seam ends in the registry's OWN refusal, + // which names the registered families. std::filesystem::filesystem_error + // derives from std::runtime_error, so CHECK_THROWS_AS(std::runtime_error) + // would NOT tell the two apart — the message is what discriminates them, and + // a filesystem_error's what() knows nothing about video families. + vllm::multimodal::VideoModelParams mp = FixtureParams(ws.fixture); + mp.dit_path = unstattable; + try { + (void)vllm::multimodal::LoadVideoEngine(mp); + FAIL("an uninspectable dit_path must be refused, not detected around"); + } catch (const std::exception& e) { + const std::string msg = e.what(); + INFO(msg); + CHECK(msg.find("minimax-h3") != std::string::npos); + CHECK(msg.find("no such file or directory") != std::string::npos); + } +} + // ─── extras carry the family-specific knob, in BOTH directions ────────────── TEST_CASE("video engine seam: H3's partition rides in extras and still guards") { SeamWorkspace ws;