fix(sf): tighten mimo and openrouter model policy
This commit is contained in:
parent
9c4bf9b3e6
commit
3d3a8e26e3
6 changed files with 164 additions and 4 deletions
|
|
@ -158,6 +158,7 @@ const BARE_MODEL_FAMILY_PRIORITY: Array<{
|
|||
"xiaomi-token-plan-ams",
|
||||
"xiaomi-token-plan-sgp",
|
||||
"xiaomi-token-plan-cn",
|
||||
"opencode-go",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue