fix(sf): tighten mimo and openrouter model policy

This commit is contained in:
Mikael Hugo 2026-04-29 21:49:49 +02:00
parent 9c4bf9b3e6
commit 3d3a8e26e3
6 changed files with 164 additions and 4 deletions

View file

@ -158,6 +158,7 @@ const BARE_MODEL_FAMILY_PRIORITY: Array<{
"xiaomi-token-plan-ams",
"xiaomi-token-plan-sgp",
"xiaomi-token-plan-cn",
"opencode-go",
],
},
];

View file

@ -22,6 +22,8 @@ export function normalizedModelName(model: {
}): string {
const provider = model.provider?.toLowerCase();
const id = model.id.toLowerCase();
const mimoName = normalizedMiMoModelName(id);
if (mimoName) return mimoName;
if (
(provider === "kimi-coding" && id === "kimi-for-coding") ||
id === "kimi-k2.6" ||
@ -41,6 +43,24 @@ export function normalizedModelName(model: {
return model.id;
}
function normalizedMiMoModelName(id: string): string | undefined {
const bareId = id.startsWith("xiaomi/") ? id.slice("xiaomi/".length) : id;
switch (bareId) {
case "mimo-v2.5-pro":
return "MiMo V2.5 Pro";
case "mimo-v2.5":
return "MiMo V2.5";
case "mimo-v2-pro":
return "MiMo V2 Pro";
case "mimo-v2-omni":
return "MiMo V2 Omni";
case "mimo-v2-flash":
return "MiMo V2 Flash";
default:
return undefined;
}
}
/**
* Return a display label that preserves both model identity and wire route.
*

View file

@ -55,19 +55,41 @@ function resolveProviderModelAllowList(
);
}
function providerModelAllowEntryMatches(
allowedModel: string,
modelKey: string,
): boolean {
const allowedKey = allowedModel.trim().toLowerCase();
if (!allowedKey) return false;
if (allowedKey === modelKey) return true;
if (allowedKey.startsWith(":")) return modelKey.endsWith(allowedKey);
if (!allowedKey.includes("*")) return false;
const pattern = `^${allowedKey
.split("*")
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join(".*")}$`;
return new RegExp(pattern).test(modelKey);
}
export function isProviderModelAllowed(
provider: string,
modelId: string,
providerModelAllow: ProviderModelAllowList | undefined,
): boolean {
const modelKey = modelId.trim().toLowerCase();
if (
provider.toLowerCase() === "openrouter" &&
!providerModelAllowEntryMatches(":free", modelKey)
) {
return false;
}
const allowedModels = resolveProviderModelAllowList(
providerModelAllow,
provider,
);
if (allowedModels === undefined) return true;
const modelKey = modelId.trim().toLowerCase();
return allowedModels.some(
(allowedModel) => allowedModel.trim().toLowerCase() === modelKey,
return allowedModels.some((allowedModel) =>
providerModelAllowEntryMatches(allowedModel, modelKey),
);
}
@ -77,7 +99,10 @@ export function filterModelsByProviderModelAllow<
models: readonly T[],
providerModelAllow: ProviderModelAllowList | undefined,
): T[] {
if (!providerModelAllow || Object.keys(providerModelAllow).length === 0)
if (
(!providerModelAllow || Object.keys(providerModelAllow).length === 0) &&
!models.some((model) => model.provider.toLowerCase() === "openrouter")
)
return [...models];
return models.filter((model) =>
isProviderModelAllowed(model.provider, model.id, providerModelAllow),

View file

@ -395,6 +395,44 @@ test("resolveModelId: bare Xiaomi IDs prefer xiaomi direct provider", () => {
assert.equal(result.provider, "xiaomi");
});
test("resolveModelId: bare Xiaomi IDs can use OpenCode Go exact model when direct Xiaomi is absent", () => {
const availableModels = [
{ id: "mimo-v2-pro", provider: "opencode-go" },
{ id: "mimo-v2-omni", provider: "opencode-go" },
];
const result = resolveModelId("mimo-v2-pro", availableModels, "openrouter");
assert.ok(result, "should resolve exact MiMo model from OpenCode Go");
assert.equal(result.provider, "opencode-go");
assert.equal(result.id, "mimo-v2-pro");
});
test("resolveModelId: Xiaomi MiMo V2.5 Pro does not fall back to V2", () => {
const availableModels = [
{ id: "mimo-v2-pro", provider: "xiaomi" },
{ id: "mimo-v2-omni", provider: "xiaomi" },
{ id: "xiaomi/mimo-v2-pro", provider: "openrouter" },
];
const result = resolveModelId(
"mimo-v2.5-pro",
availableModels,
"xiaomi",
);
assert.equal(result, undefined);
});
test("resolveModelId: Xiaomi MiMo V2.5 non-pro does not fall back to V2", () => {
const availableModels = [
{ id: "mimo-v2-pro", provider: "xiaomi" },
{ id: "mimo-v2-omni", provider: "xiaomi" },
{ id: "mimo-v2-flash", provider: "xiaomi" },
];
const result = resolveModelId("mimo-v2.5", availableModels, "xiaomi");
assert.equal(result, undefined);
});
// ─── selectAndApplyModel verbose-gating tests ──────────────────────────
test("model change notify in selectAndApplyModel is gated behind verbose flag", () => {

View file

@ -30,3 +30,19 @@ test("model identity: K2.5 remains distinct from K2.6", () => {
assert.equal(normalizedModelName(model), "Kimi K2.5");
assert.equal(formatModelIdentity(model), "Kimi K2.5 (kimi-coding/k2p5)");
});
test("model identity: Xiaomi MiMo V2 and V2.5 lines stay distinct", () => {
const v25Pro = { provider: "xiaomi", id: "mimo-v2.5-pro" };
const v2Pro = { provider: "xiaomi", id: "mimo-v2-pro" };
const openRouterOmni = { provider: "openrouter", id: "xiaomi/mimo-v2-omni" };
assert.equal(normalizedModelName(v25Pro), "MiMo V2.5 Pro");
assert.equal(formatModelIdentity(v25Pro), "MiMo V2.5 Pro (xiaomi/mimo-v2.5-pro)");
assert.equal(normalizedModelName(v2Pro), "MiMo V2 Pro");
assert.equal(formatModelIdentity(v2Pro), "MiMo V2 Pro (xiaomi/mimo-v2-pro)");
assert.equal(normalizedModelName(openRouterOmni), "MiMo V2 Omni");
assert.equal(
formatModelIdentity(openRouterOmni),
"MiMo V2 Omni (openrouter/xiaomi/mimo-v2-omni)",
);
});

View file

@ -56,6 +56,66 @@ test("provider_model_allow: provider absent from allow-list is unrestricted", ()
assert.ok(filtered.some((m) => m.provider === "zai" && m.id === "glm-5"));
});
test("provider_model_allow: OpenRouter defaults to free models only", () => {
const models = [
{ provider: "openrouter", id: "qwen/qwen3-4b:free" },
{ provider: "openrouter", id: "z-ai/glm-5.1" },
{ provider: "zai", id: "glm-5.1" },
];
const filtered = filterModelsByProviderModelAllow(models, undefined);
assert.deepEqual(
filtered.map((m) => `${m.provider}/${m.id}`),
["openrouter/qwen/qwen3-4b:free", "zai/glm-5.1"],
);
});
test("provider_model_allow: supports OpenRouter free-model suffix patterns", () => {
const models = [
{ provider: "openrouter", id: "qwen/qwen3-coder:free" },
{ provider: "openrouter", id: "openai/gpt-oss-120b:free" },
{ provider: "openrouter", id: "minimax/minimax-m2.5" },
{ provider: "zai", id: "glm-4.6" },
];
const suffixFiltered = filterModelsByProviderModelAllow(models, {
openrouter: [":free"],
});
assert.deepEqual(
suffixFiltered.map((m) => `${m.provider}/${m.id}`),
[
"openrouter/qwen/qwen3-coder:free",
"openrouter/openai/gpt-oss-120b:free",
"zai/glm-4.6",
],
);
const globFiltered = filterModelsByProviderModelAllow(models, {
openrouter: ["*:free"],
});
assert.deepEqual(
globFiltered.map((m) => `${m.provider}/${m.id}`),
suffixFiltered.map((m) => `${m.provider}/${m.id}`),
);
});
test("provider_model_allow: OpenRouter paid models stay blocked even when explicitly listed", () => {
const models = [
{ provider: "openrouter", id: "z-ai/glm-5.1" },
{ provider: "openrouter", id: "qwen/qwen3-4b:free" },
];
const filtered = filterModelsByProviderModelAllow(models, {
openrouter: ["z-ai/glm-5.1", "qwen/qwen3-4b:free"],
});
assert.deepEqual(
filtered.map((m) => `${m.provider}/${m.id}`),
["openrouter/qwen/qwen3-4b:free"],
);
});
test("provider_model_allow: validates shape and normalizes provider IDs", () => {
const result = validatePreferences({
provider_model_allow: {