From f7ac29d363ae9ff04cbc7ed08f5c69471476f143 Mon Sep 17 00:00:00 2001 From: Xmon Dai Date: Wed, 23 Sep 2026 12:41:08 +0800 Subject: [PATCH] fix(capabilities): resolve the MiMo v2.6 family as vision-capable (#14587) The v2.6 generation flips the older '*-pro is text-only' rule: models.dev lists mimo-v2.6-pro with image input, but every capability source in the repo stops at v2.5, so resolveVisionCapability returned null and both /v1/models and the combo projection advertised the model text-only. Add the 'mimo-v2.6' fragment to VISION_MODEL_ID_FRAGMENTS (the shared last-resort heuristic, #4072) and rescope the stale 'pro = text-only' comment in modelSpecs.ts to the v2.5 family. The fragment matches both provider-qualified ids from the report and cannot over-match the text-only earlier-generation pro siblings, which never contain v2.6. --- src/shared/constants/modelSpecs.ts | 12 ++-- src/shared/constants/visionModels.ts | 11 ++++ tests/unit/14587-mimo-v2-6-vision.test.ts | 76 +++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/unit/14587-mimo-v2-6-vision.test.ts diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 3f84b77ef469..73d13798b98f 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -613,10 +613,14 @@ export const MODEL_SPECS: Record = { }, // ── Xiaomi MiMo V2.5 (1M context, consensus across 7+ sync sources) ── - // Vision: ONLY mimo-v2.5 and mimo-v2-omni accept images per Xiaomi's docs - // (mimo.mi.com .../image-understanding). The *-pro chat models are TEXT-ONLY; - // models.dev mislabels them (hermes-agent#18884) — a hard override in - // src/lib/modelCapabilities.ts also beats that wrong synced attachment. + // Vision: in the v2.5 generation only `mimo-v2.5` and `mimo-v2-omni` accept + // images per Xiaomi's docs (mimo.mi.com .../image-understanding). The v2.5 + // `*-pro` chat models are TEXT-ONLY; models.dev mislabels them + // (hermes-agent#18884) — a hard override in src/lib/modelCapabilities.ts + // also beats that wrong synced attachment. The v2.6 generation flips the + // `*-pro` rule (#14587): `mimo-v2.6-pro` accepts image input, covered by the + // `mimo-v2.6-pro` / `mimo-v2.6-flash` fragments in the shared vision + // heuristic rather than a spec. "mimo-v2.5-pro": { maxOutputTokens: 131072, contextWindow: 1048576, diff --git a/src/shared/constants/visionModels.ts b/src/shared/constants/visionModels.ts index e03b3799c172..53fe05224a3f 100644 --- a/src/shared/constants/visionModels.ts +++ b/src/shared/constants/visionModels.ts @@ -39,6 +39,17 @@ export const VISION_MODEL_ID_FRAGMENTS = [ // isVisionModelId() below so this shared heuristic stays safe for routing, // `/v1/models`, combo projection and lite compression alike. "mimo-v2.5", + // #14587: the v2.6 generation flips the `*-pro` rule — models.dev lists + // `mimo-v2.6-pro` / `mimo-v2.6-flash` with image input, while `mimo-v2.5-pro` + // and `mimo-v2-pro` stay text-only. Keep the fragments scoped to the + // documented pro/flash family so unrelated future v2.6 ids (a hypothetical + // `mimo-v2.6-distill-qwen-9b` or `mimo-v2.6-tts`) do not inherit the vision + // verdict; they can be added here if Xiaomi ever documents image input for + // them. Matches the provider-qualified ids from the report + // (`opencode-go/mimo-v2.6-pro`, `command-code/xiaomi/mimo-v2.6-pro`) and + // cannot over-match the older generations, whose ids never contain `v2.6`. + "mimo-v2.6-pro", + "mimo-v2.6-flash", // #13847: Step 3.7 Flash is exposed through provider-qualified `:free` routes // as well as direct registry entries. The capability must survive that suffix. "step-3.7-flash", diff --git a/tests/unit/14587-mimo-v2-6-vision.test.ts b/tests/unit/14587-mimo-v2-6-vision.test.ts new file mode 100644 index 000000000000..b2526933de06 --- /dev/null +++ b/tests/unit/14587-mimo-v2-6-vision.test.ts @@ -0,0 +1,76 @@ +/** + * #14587 — MiMo v2.6 family is natively vision-capable but absent from every + * capability source, so `resolveVisionCapability` falls through the whole + * cascade and returns `null`, which `/v1/models` and the combo projection both + * render as text-only. One unresolvable member then flips an entire combo to + * `multimodal: false`. + * + * The last-resort id heuristic (VISION_MODEL_ID_FRAGMENTS, #4072) is the + * established channel for exactly this case (see + * vision-models-cc-fragments.test.ts). The `mimo-v2.6-pro` / `mimo-v2.6-flash` + * fragments must match the documented v2.6 family through provider-qualified + * ids without over-matching the text-only `*-pro` siblings from earlier + * generations, and without inheriting the verdict for unrelated v2.6 ids. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts"; +import { isVisionModelId } from "../../src/shared/constants/visionModels.ts"; + +// ── The two ids from the report, plus the bare and sibling variants ── + +const V26_VISION = [ + "opencode-go/mimo-v2.6-pro", + "command-code/xiaomi/mimo-v2.6-pro", + "mimo-v2.6-pro", + "xiaomi-mimo/mimo-v2.6-pro:free", + "mimo-v2.6-flash", + "opencode-go/mimo-v2.6-pro-ultraspeed", +]; + +for (const id of V26_VISION) { + test(`#${id} resolves supportsVision=true`, () => { + assert.equal(isVisionModelId(id), true, `isVisionModelId(${id})`); + const caps = getResolvedModelCapabilities(id); + assert.equal(caps.supportsVision, true, `getResolvedModelCapabilities(${id}).supportsVision`); + }); +} + +// ── Negative guards: the text-only *-pro siblings must not over-match ── + +const STILL_TEXT_ONLY = [ + "mimo-v2.5-pro", + "opencode-go/mimo-v2.5-pro", + "command-code/mimo-v2.5-pro", + "mimo-v2-pro", + "command-code/xiaomi/mimo-v2-pro", + "mimo-v2-flash", + "gemma-2-9b", + // The narrowed v2.6 fragments must not hand the vision verdict to + // undocumented siblings of the same generation (maintainer review, #14595). + "mimo-v2.6-distill-qwen-9b", + "opencode-go/mimo-v2.6-tts", +]; + +for (const id of STILL_TEXT_ONLY) { + test(`#${id} stays non-vision (no false positive)`, () => { + assert.equal(isVisionModelId(id), false, `isVisionModelId(${id})`); + }); +} + +test("resolved capabilities keep the earlier-generation pro verdicts", () => { + assert.notEqual( + getResolvedModelCapabilities("opencode-go/mimo-v2.5-pro").supportsVision, + true, + "mimo-v2.5-pro is text-only per Xiaomi docs" + ); + assert.notEqual( + getResolvedModelCapabilities("command-code/xiaomi/mimo-v2-pro").supportsVision, + true, + "mimo-v2-pro is text-only per Xiaomi docs" + ); + // And the multimodal base siblings from earlier generations are untouched. + assert.equal(getResolvedModelCapabilities("opencode-go/mimo-v2.5").supportsVision, true); + assert.equal(getResolvedModelCapabilities("mimo-v2-omni").supportsVision, true); +});