diff --git a/changelog.d/fixes/17788-opencode-zen-mimo-v2.5-free-deprecated.md b/changelog.d/fixes/17788-opencode-zen-mimo-v2.5-free-deprecated.md new file mode 100644 index 000000000000..0339cad4083a --- /dev/null +++ b/changelog.d/fixes/17788-opencode-zen-mimo-v2.5-free-deprecated.md @@ -0,0 +1 @@ +- **fix(providers):** OpenCode Zen's free `mimo-v2.5-free` id is deprecated upstream (`410 Model mimo-v2.5-free has been deprecated. Use mimo-v2.6-flash-free instead.`), but it was still an active candidate, so Auto could keep selecting a model the upstream refuses. The Zen registry now advertises `mimo-v2.6-flash-free` and keeps the old id as a compatibility alias, the free-model catalog lists the replacement, and clients pinned to `mimo-v2.5-free` are forwarded to the supported id by the built-in deprecation table — the same rotation recipe as the DeepSeek V4 → V4.1 free snapshot. ([#17788](https://github.com/diegosouzapw/OmniRoute/pull/17788)) - thanks @skygunner diff --git a/open-sse/config/providers/registry/opencode/zen/index.ts b/open-sse/config/providers/registry/opencode/zen/index.ts index 952fb4ce45b5..7b2aab0a12ab 100644 --- a/open-sse/config/providers/registry/opencode/zen/index.ts +++ b/open-sse/config/providers/registry/opencode/zen/index.ts @@ -197,13 +197,16 @@ export const opencode_zenProvider: RegistryEntry = { contextLength: 330000, maxOutputTokens: 128000, }, - { id: "mimo-v2.5-free", name: "MiMo V2.5 Free", contextLength: 1048576 }, // MiMo V2.6 Flash Free ships a 1M window upstream (same as V2.5); without // this row it falls through to the 200000 provider default and clients // compact far too early. + // 2026-10-21 sync: mimo-v2.5-free was deprecated upstream (410 "Model + // mimo-v2.5-free has been deprecated. Use mimo-v2.6-flash-free instead.") and + // replaced by mimo-v2.6-flash-free; keep the old id as a compatibility alias. { id: "mimo-v2.6-flash-free", name: "MiMo V2.6 Flash Free", + aliases: ["mimo-v2.5-free"], contextLength: 1048576, maxOutputTokens: 131072, }, diff --git a/open-sse/executors/opencodeFreeTierContract.ts b/open-sse/executors/opencodeFreeTierContract.ts index 9f6e6ae1c06c..52cb9e02ccc5 100644 --- a/open-sse/executors/opencodeFreeTierContract.ts +++ b/open-sse/executors/opencodeFreeTierContract.ts @@ -117,12 +117,13 @@ interface OpencodeFreeCatalog { * Legacy free ids kept only as the unsynced-catalog fallback (see the block above). * * `deepseek-v4.1-flash-free` replaced the delisted `deepseek-v4-flash-free` in the zen - * registry; the rotating `-free` ids are covered by the suffix rule alongside this set. + * registry and `mimo-v2.6-flash-free` replaced the deprecated `mimo-v2.5-free`; the + * rotating `-free` ids are covered by the suffix rule alongside this set. */ const OPENCODE_LEGACY_FREE_MODELS = new Set([ "big-pickle", "deepseek-v4.1-flash-free", - "mimo-v2.5-free", + "mimo-v2.6-flash-free", "hy3-free", "nemotron-3-ultra-free", "north-mini-code-free", diff --git a/open-sse/services/modelDeprecation.ts b/open-sse/services/modelDeprecation.ts index ecd2a392f05a..4a930d300dd2 100644 --- a/open-sse/services/modelDeprecation.ts +++ b/open-sse/services/modelDeprecation.ts @@ -61,6 +61,12 @@ const BUILT_IN_ALIASES: Record = { // `400 Model is unavailable`. Clients pinned to the old id keep working. "deepseek-v4-flash-free": "deepseek-v4.1-flash-free", + // OpenCode Zen deprecated its free MiMo V2.5 snapshot on 2026-10-21: the + // upstream answers the old id with `410 Model mimo-v2.5-free has been + // deprecated. Use mimo-v2.6-flash-free instead.` Clients pinned to the old id + // keep working. + "mimo-v2.5-free": "mimo-v2.6-flash-free", + // Mistral short aliases "mistral-large": "mistral-large-latest", "mistral-small": "mistral-small-latest", diff --git a/tests/integration/freeModelBenchmarkShared.ts b/tests/integration/freeModelBenchmarkShared.ts index d66ded17ea5e..00c8c168d299 100644 --- a/tests/integration/freeModelBenchmarkShared.ts +++ b/tests/integration/freeModelBenchmarkShared.ts @@ -178,8 +178,8 @@ export const FREE_MODELS: FreeModelSpec[] = [ { provider: "opencode", model: "opencode/big-pickle", displayName: "Big Pickle (OpenCode)" }, { provider: "opencode", - model: "opencode/mimo-v2.5-free", - displayName: "MiMo V2.5 Free (OpenCode)", + model: "opencode/mimo-v2.6-flash-free", + displayName: "MiMo V2.6 Flash Free (OpenCode)", }, ]; @@ -209,7 +209,7 @@ export const NEWLY_ENABLED_MODELS: FreeModelSpec[] = FREE_MODELS.filter((m) => "opencode/north-mini-code-free", "opencode/laguna-s-2.1-free", "opencode/big-pickle", - "opencode/mimo-v2.5-free", + "opencode/mimo-v2.6-flash-free", ].includes(m.model) ); diff --git a/tests/integration/liveDefaultComboShared.ts b/tests/integration/liveDefaultComboShared.ts index d5d9291e63cf..dd614889340f 100644 --- a/tests/integration/liveDefaultComboShared.ts +++ b/tests/integration/liveDefaultComboShared.ts @@ -50,7 +50,7 @@ async function apiFetch(path: string, options: RequestInit = {}): Promise { + assert.equal(resolveModelAlias("mimo-v2.5-free", "opencode-zen"), "mimo-v2.6-flash-free"); + assert.equal(resolveModelAlias("mimo-v2.5-free", "opencode"), "mimo-v2.6-flash-free"); + assert.equal(resolveModelAlias("mimo-v2.5-free"), "mimo-v2.6-flash-free"); +}); + +test("the replacement id is not rewritten", () => { + assert.equal(resolveModelAlias("mimo-v2.6-flash-free", "opencode-zen"), "mimo-v2.6-flash-free"); +}); + +test("opencode-zen registry advertises v2.6 flash and keeps the old id as an alias", () => { + const replacement = opencode_zenProvider.models.find( + (model) => model.id === "mimo-v2.6-flash-free" + ); + assert.ok(replacement, "registry must list mimo-v2.6-flash-free"); + assert.deepEqual(replacement.aliases, ["mimo-v2.5-free"]); + assert.equal( + opencode_zenProvider.models.some((model) => model.id === "mimo-v2.5-free"), + false, + "the deprecated id must not remain a standalone catalog row" + ); +}); + +test("free-model catalog lists v2.6 flash instead of the deprecated id", () => { + assert.ok( + FREE_MODEL_BUDGETS.some( + (entry) => + entry.provider === "opencode-zen" && entry.modelId === "opencode/mimo-v2.6-flash-free" + ) + ); + assert.equal( + FREE_MODEL_BUDGETS.some((entry) => entry.modelId === "opencode/mimo-v2.5-free"), + false + ); +}); diff --git a/tests/unit/opencode-mimo-v2.5-free-chatcore-forward-17788.test.ts b/tests/unit/opencode-mimo-v2.5-free-chatcore-forward-17788.test.ts new file mode 100644 index 000000000000..5192cdc66d10 --- /dev/null +++ b/tests/unit/opencode-mimo-v2.5-free-chatcore-forward-17788.test.ts @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mimo-forward-17788-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "mimo-forward-17788-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); + +const originalFetch = globalThis.fetch; + +test.after(() => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +/** + * #17472 / #17788: OpenCode Zen answers the retired `mimo-v2.5-free` id with + * `410 ... Use mimo-v2.6-flash-free instead.` A client pinned to the old id must + * reach the upstream under the replacement id through the real chatCore path. + */ +for (const provider of ["opencode-zen", "opencode"]) { + test(`chatCore forwards a ${provider} request pinned to mimo-v2.5-free as mimo-v2.6-flash-free`, async () => { + const sentModels: unknown[] = []; + globalThis.fetch = async (_url: unknown, init?: RequestInit) => { + const parsed = JSON.parse(String(init?.body ?? "{}")) as Record; + sentModels.push(parsed.model); + return new Response( + JSON.stringify({ + id: "chatcmpl-1", + object: "chat.completion", + created: 1, + model: parsed.model, + choices: [ + { index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }; + + try { + const body = { + model: "mimo-v2.5-free", + stream: false, + messages: [{ role: "user", content: "hello" }], + }; + const result = await handleChatCore({ + body: structuredClone(body), + modelInfo: { provider, model: "mimo-v2.5-free", extendedContext: false }, + credentials: { apiKey: "sk-test", providerSpecificData: {} }, + log: { debug() {}, info() {}, warn() {}, error() {} }, + clientRawRequest: { + endpoint: "/v1/chat/completions", + body: structuredClone(body), + headers: new Headers({ accept: "application/json" }), + }, + userAgent: "mimo-forward-17788-test", + }); + + assert.equal(result.success, true, "request must not be rejected as a retired model"); + assert.deepEqual(sentModels, ["mimo-v2.6-flash-free"]); + await result.response?.body?.cancel?.().catch(() => {}); + } finally { + globalThis.fetch = originalFetch; + } + }); +}