Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/shared/constants/modelSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,10 +613,14 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
},

// ── 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,
Expand Down
11 changes: 11 additions & 0 deletions src/shared/constants/visionModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/14587-mimo-v2-6-vision.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading