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
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion open-sse/config/providers/registry/opencode/zen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
5 changes: 3 additions & 2 deletions open-sse/executors/opencodeFreeTierContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions open-sse/services/modelDeprecation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const BUILT_IN_ALIASES: Record<string, string> = {
// `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",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/freeModelBenchmarkShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
},
];

Expand Down Expand Up @@ -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)
);

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/liveDefaultComboShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function apiFetch(path: string, options: RequestInit = {}): Promise<Respon
// always what gets read and tested — this list never overrides it.
const FALLBACK_COMBO_MODELS: { model: string; providerId: string }[] = [
{ model: "opencode/big-pickle", providerId: "opencode" },
{ model: "opencode/mimo-v2.5-free", providerId: "opencode" },
{ model: "opencode/mimo-v2.6-flash-free", providerId: "opencode" },
{ model: "opencode/laguna-s-2.1-free", providerId: "opencode" },
{ model: "openrouter/cohere/north-mini-code:free", providerId: "openrouter" },
{ model: "openrouter/poolside/laguna-m.1:free", providerId: "openrouter" },
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/opencode-mimo-v2.5-free-alias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import { test } from "node:test";

import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";
import { opencode_zenProvider } from "../../open-sse/config/providers/registry/opencode/zen/index.ts";
import { resolveModelAlias } from "../../open-sse/services/modelDeprecation.ts";

/**
* OpenCode Zen deprecated `mimo-v2.5-free`: the upstream answers the old id
* with `410 Model mimo-v2.5-free has been deprecated. Use mimo-v2.6-flash-free
* instead.` Pinned clients must be forwarded to the replacement, and the
* deprecated id must not stay in the advertised candidate catalog.
*/
test("deprecated mimo-v2.5-free forwards to mimo-v2.6-flash-free for opencode-zen", () => {
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
);
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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;
}
});
}
Loading