diff --git a/changelog.d/fixes/13847-combo-multimodal-capability.md b/changelog.d/fixes/13847-combo-multimodal-capability.md new file mode 100644 index 0000000000..9f5cf33b5e --- /dev/null +++ b/changelog.d/fixes/13847-combo-multimodal-capability.md @@ -0,0 +1 @@ +- **fix(combos):** Keep vision capability consistent for MiMo V2.5 and Step 3.7 Flash provider/free variants so `/v1/combos` no longer under-reports multimodal combos whose members are already advertised as vision-capable by `/v1/models` ([#13847](https://github.com/diegosouzapw/OmniRoute/issues/13847)). diff --git a/src/shared/constants/visionModels.ts b/src/shared/constants/visionModels.ts index bcad514058..e03b3799c1 100644 --- a/src/shared/constants/visionModels.ts +++ b/src/shared/constants/visionModels.ts @@ -34,6 +34,14 @@ export const VISION_MODEL_ID_FRAGMENTS = [ "minicpm-v", "moondream", "mimo-vl", + // #13847: MiMo V2.5 is multimodal across the provider aliases that expose it + // (including `*-free` variants). Keep the known text-only Pro siblings out in + // isVisionModelId() below so this shared heuristic stays safe for routing, + // `/v1/models`, combo projection and lite compression alike. + "mimo-v2.5", + // #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", "kimi-vl", "glm-4v", "glm-4.5v", @@ -74,5 +82,13 @@ export const VISION_MODEL_ID_FRAGMENTS = [ export function isVisionModelId(modelId: string | null | undefined): boolean { if (!modelId) return false; const normalized = String(modelId).toLowerCase(); + + // Xiaomi documents the Pro chat variants as text-only even though the base + // MiMo V2.5 model is multimodal. Keep these exclusions beside the shared + // heuristic so every consumer gets the same verdict instead of relying on a + // resolver-specific exception. + if (/(?:^|\/)mimo-v2\.5-pro(?:$|[:/])/i.test(normalized)) return false; + if (/(?:^|\/)mimo-v2-pro(?:$|[:/])/i.test(normalized)) return false; + return VISION_MODEL_ID_FRAGMENTS.some((fragment) => normalized.includes(fragment)); } diff --git a/tests/unit/combo-multimodal-capability-13847.test.ts b/tests/unit/combo-multimodal-capability-13847.test.ts new file mode 100644 index 0000000000..57f74dd4c0 --- /dev/null +++ b/tests/unit/combo-multimodal-capability-13847.test.ts @@ -0,0 +1,51 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts"; +import { computeComboCapabilities } from "../../src/app/api/v1/combos/projectCombo.ts"; + +const REPORTED_VISION_MEMBERS = [ + "opencode-go/mimo-v2.5", + "kilo-gateway/stepfun/step-3.7-flash:free", + "opencode-zen/mimo-v2.5-free", + "opencode/mimo-v2.5-free", + "command-code/xiaomi/mimo-v2.5", +] as const; + +test("#13847 reported combo members resolve as vision-capable", () => { + for (const model of REPORTED_VISION_MEMBERS) { + assert.equal( + getResolvedModelCapabilities(model).supportsVision, + true, + `${model} should retain its vision capability through provider/free-route qualification` + ); + } +}); + +test("#13847 known MiMo Pro text-only siblings stay excluded", () => { + for (const model of [ + "mimo-v2.5-pro", + "command-code/xiaomi/mimo-v2.5-pro", + "mimo-v2-pro", + ]) { + assert.notEqual( + getResolvedModelCapabilities(model).supportsVision, + true, + `${model} must not inherit the MiMo V2.5 multimodal heuristic` + ); + } +}); + +test("#13847 combo projection agrees with vision-capable members", () => { + const combo = { + name: "default-observer", + strategy: "priority", + models: REPORTED_VISION_MEMBERS.map((model) => ({ kind: "model", model })), + }; + + assert.deepEqual(computeComboCapabilities(combo), { + multimodal: true, + reasoning: true, + caching: false, + }); +});