mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
This commit is contained in:
1
changelog.d/features/13214-auto-routing-families.md
Normal file
1
changelog.d/features/13214-auto-routing-families.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** Add `auto/kimi`, `auto/qwen`, `auto/deepseek`, `auto/gpt`, and the `auto/claude-haiku` fast variant to the built-in routing catalog, including bare `k3` models on Kimi coding and web backends (issue #13214).
|
||||
@@ -43,6 +43,7 @@ export const AUTO_TEMPLATE_VARIANTS: Record<string, AutoVariant | undefined> = {
|
||||
"auto/smart": "smart",
|
||||
"auto/claude-opus": "smart",
|
||||
"auto/claude-sonnet": "coding",
|
||||
"auto/claude-haiku": "fast",
|
||||
"auto/best-free": "cheap",
|
||||
// Subscription-first routing (see `subscriptionLadder.ts`). `auto/subscription`
|
||||
// maps to no weight variant on purpose: its pool is already restricted to
|
||||
|
||||
@@ -13,7 +13,18 @@
|
||||
* in isolation without touching the DB/registry-backed virtual factory.
|
||||
*/
|
||||
|
||||
export type ModelFamily = "glm" | "minimax" | "mimo" | "zai" | "gemma" | "llama" | "gemini";
|
||||
export type ModelFamily =
|
||||
| "glm"
|
||||
| "minimax"
|
||||
| "mimo"
|
||||
| "zai"
|
||||
| "gemma"
|
||||
| "llama"
|
||||
| "gemini"
|
||||
| "kimi"
|
||||
| "qwen"
|
||||
| "deepseek"
|
||||
| "gpt";
|
||||
|
||||
export const MODEL_FAMILIES: readonly ModelFamily[] = [
|
||||
"glm",
|
||||
@@ -23,6 +34,10 @@ export const MODEL_FAMILIES: readonly ModelFamily[] = [
|
||||
"gemma",
|
||||
"llama",
|
||||
"gemini",
|
||||
"kimi",
|
||||
"qwen",
|
||||
"deepseek",
|
||||
"gpt",
|
||||
];
|
||||
|
||||
const MODEL_FAMILY_SET: ReadonlySet<string> = new Set(MODEL_FAMILIES);
|
||||
@@ -36,6 +51,10 @@ const FAMILY_ID_PATTERNS: ReadonlyArray<{ family: ModelFamily; pattern: RegExp }
|
||||
{ family: "gemma", pattern: /^gemma-/i },
|
||||
{ family: "llama", pattern: /^llama-/i },
|
||||
{ family: "gemini", pattern: /^gemini-/i },
|
||||
{ family: "kimi", pattern: /^kimi-/i },
|
||||
{ family: "qwen", pattern: /^qwen/i },
|
||||
{ family: "deepseek", pattern: /^deepseek-/i },
|
||||
{ family: "gpt", pattern: /^gpt-/i },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -79,7 +98,8 @@ interface FamilyPoolCandidate {
|
||||
/**
|
||||
* Build the candidate filter for `auto/<family>`. Provider-override families
|
||||
* (currently only `zai`) filter by connection provider id; every other family
|
||||
* filters by `detectModelFamily(model) === family`.
|
||||
* filters by `detectModelFamily(model) === family`, with a provider-scoped
|
||||
* exception for Kimi backends exposing the bare `k3` model id.
|
||||
*/
|
||||
export function buildFamilyCandidateFilter(
|
||||
family: ModelFamily
|
||||
@@ -88,7 +108,17 @@ export function buildFamilyCandidateFilter(
|
||||
if (providerOverride) {
|
||||
return (candidate) => candidate.provider === providerOverride;
|
||||
}
|
||||
return (candidate) => detectModelFamily(candidate.model) === family;
|
||||
return (candidate) => {
|
||||
if (detectModelFamily(candidate.model) === family) return true;
|
||||
// Coding OAuth/API-key and web share the bare k3 id. Do not classify an
|
||||
// unrelated provider's k3 (or other models on these providers) as Kimi.
|
||||
const bareModel = candidate.model.slice(candidate.model.lastIndexOf("/") + 1);
|
||||
return (
|
||||
family === "kimi" &&
|
||||
["kimi-coding", "kimi-coding-apikey", "kimi-web"].includes(candidate.provider) &&
|
||||
bareModel.toLowerCase() === "k3"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/** Advertised `auto/<family>` catalog ids (#6453), e.g. `auto/glm`, `auto/minimax`. */
|
||||
|
||||
@@ -16,6 +16,7 @@ import path from "node:path";
|
||||
|
||||
import {
|
||||
detectModelFamily,
|
||||
buildFamilyCandidateFilter,
|
||||
isValidModelFamily,
|
||||
AUTO_FAMILY_IDS,
|
||||
} from "../../../open-sse/services/autoCombo/modelFamily";
|
||||
@@ -67,7 +68,7 @@ describe("detectModelFamily (pure)", () => {
|
||||
});
|
||||
|
||||
it("returns null for unrelated model ids", () => {
|
||||
assert.equal(detectModelFamily("gpt-4o"), null);
|
||||
assert.equal(detectModelFamily("unrelated-model"), null);
|
||||
assert.equal(detectModelFamily(""), null);
|
||||
assert.equal(detectModelFamily(null), null);
|
||||
});
|
||||
@@ -78,22 +79,38 @@ describe("detectModelFamily (pure)", () => {
|
||||
assert.equal(detectModelFamily("zai-glm-5.2"), null);
|
||||
});
|
||||
|
||||
it("isValidModelFamily accepts exactly the 7 advertised families", () => {
|
||||
for (const family of ["glm", "minimax", "mimo", "zai", "gemma", "llama", "gemini"]) {
|
||||
it("isValidModelFamily accepts exactly the 11 advertised families", () => {
|
||||
for (const family of [
|
||||
"glm",
|
||||
"minimax",
|
||||
"mimo",
|
||||
"zai",
|
||||
"gemma",
|
||||
"llama",
|
||||
"gemini",
|
||||
"kimi",
|
||||
"qwen",
|
||||
"deepseek",
|
||||
"gpt",
|
||||
]) {
|
||||
assert.equal(isValidModelFamily(family), true);
|
||||
}
|
||||
assert.equal(isValidModelFamily("gpt"), false);
|
||||
assert.equal(isValidModelFamily("unknown"), false);
|
||||
assert.equal(isValidModelFamily(undefined), false);
|
||||
});
|
||||
|
||||
it("advertises exactly one auto/<family> catalog id per family", () => {
|
||||
assert.deepEqual([...AUTO_FAMILY_IDS].sort(), [
|
||||
"auto/deepseek",
|
||||
"auto/gemini",
|
||||
"auto/gemma",
|
||||
"auto/glm",
|
||||
"auto/gpt",
|
||||
"auto/kimi",
|
||||
"auto/llama",
|
||||
"auto/mimo",
|
||||
"auto/minimax",
|
||||
"auto/qwen",
|
||||
"auto/zai",
|
||||
]);
|
||||
});
|
||||
@@ -230,9 +247,127 @@ describe("auto/<family> materialization (#6453)", () => {
|
||||
});
|
||||
|
||||
it("isRecognizedBuiltinAuto recognizes every auto/<family> id", () => {
|
||||
for (const family of ["glm", "minimax", "mimo", "zai", "gemma", "llama", "gemini"]) {
|
||||
for (const family of [
|
||||
"glm",
|
||||
"minimax",
|
||||
"mimo",
|
||||
"zai",
|
||||
"gemma",
|
||||
"llama",
|
||||
"gemini",
|
||||
"kimi",
|
||||
"qwen",
|
||||
"deepseek",
|
||||
"gpt",
|
||||
]) {
|
||||
assert.equal(builtinCatalog.isRecognizedBuiltinAuto(`auto/${family}`, family), true);
|
||||
}
|
||||
assert.equal(builtinCatalog.isRecognizedBuiltinAuto("auto/unknownfam", "unknownfam"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("additional auto-routing families (#13214)", () => {
|
||||
it("materializes Qwen, DeepSeek and GPT without crossing family boundaries", async () => {
|
||||
for (const [provider, defaultModel] of [
|
||||
["qwen", "qwen3-14b"],
|
||||
["deepseek", "deepseek-chat"],
|
||||
["openai", "gpt-4o"],
|
||||
]) {
|
||||
await providersDb.createProviderConnection({
|
||||
provider,
|
||||
defaultModel,
|
||||
authType: "apikey",
|
||||
name: provider,
|
||||
apiKey: "test",
|
||||
});
|
||||
}
|
||||
for (const [family, provider] of [
|
||||
["qwen", "qwen"],
|
||||
["deepseek", "deepseek"],
|
||||
["gpt", "openai"],
|
||||
]) {
|
||||
const combo = await builtinCatalog.createBuiltinAutoCombo(`auto/${family}`, family);
|
||||
assert.equal(combo.id, `auto/${family}`);
|
||||
assert.equal(combo.strategy, "auto");
|
||||
assert.ok(
|
||||
combo.models.some((model) => model.providerId === provider),
|
||||
family
|
||||
);
|
||||
assert.ok(
|
||||
combo.models.every((model) => detectModelFamily(model.model) === family),
|
||||
family
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("materializes Kimi across coding, web and Moonshot without unrelated models", async () => {
|
||||
for (const [provider, defaultModel] of [
|
||||
["kimi-coding", "k3"],
|
||||
["kimi-coding-apikey", "k3"],
|
||||
["kimi-web", "k3"],
|
||||
["moonshot", "kimi-k2.5"],
|
||||
["openai", "gpt-4o"],
|
||||
]) {
|
||||
await providersDb.createProviderConnection({
|
||||
provider,
|
||||
defaultModel,
|
||||
authType: "apikey",
|
||||
name: provider,
|
||||
apiKey: "test",
|
||||
});
|
||||
}
|
||||
const combo = await builtinCatalog.createBuiltinAutoCombo("auto/kimi", "kimi");
|
||||
assert.equal(combo.id, "auto/kimi");
|
||||
assert.equal(combo.strategy, "auto");
|
||||
for (const provider of ["kimi-coding", "kimi-coding-apikey", "kimi-web", "moonshot"]) {
|
||||
assert.ok(
|
||||
combo.models.some((model) => model.providerId === provider),
|
||||
provider
|
||||
);
|
||||
}
|
||||
assert.ok(combo.models.every((model) => model.providerId !== "openai"));
|
||||
});
|
||||
|
||||
it("detects new families across provider-prefixed and bare model ids", () => {
|
||||
for (const [model, family] of [
|
||||
["moonshot/kimi-k3", "kimi"],
|
||||
["QWEN3-14B", "qwen"],
|
||||
["qwen2.5-coder-32b-instruct", "qwen"],
|
||||
["qwen3.5-plus", "qwen"],
|
||||
["deepseek/deepseek-chat", "deepseek"],
|
||||
["openai/gpt-4o", "gpt"],
|
||||
]) {
|
||||
assert.equal(detectModelFamily(model), family);
|
||||
}
|
||||
assert.equal(detectModelFamily("k3"), null);
|
||||
assert.equal(detectModelFamily("not-qwen3"), null);
|
||||
});
|
||||
|
||||
it("includes Kimi k3 only on known Kimi backends, alongside prefix matches", () => {
|
||||
const filter = buildFamilyCandidateFilter("kimi");
|
||||
for (const provider of ["kimi-coding", "kimi-web", "kimi-coding-apikey"]) {
|
||||
assert.equal(filter({ provider, model: "k3" }), true);
|
||||
assert.equal(filter({ provider, model: `${provider}/k3` }), true);
|
||||
assert.equal(filter({ provider, model: "gpt-4o" }), false);
|
||||
}
|
||||
assert.equal(filter({ provider: "moonshot", model: "kimi-k3" }), true);
|
||||
assert.equal(filter({ provider: "custom", model: "kimi-k2.5" }), true);
|
||||
assert.equal(filter({ provider: "custom", model: "k3" }), false);
|
||||
assert.equal(buildFamilyCandidateFilter("gpt")({ provider: "kimi-web", model: "k3" }), false);
|
||||
assert.equal(buildFamilyCandidateFilter("zai")({ provider: "glm", model: "glm-5.2" }), false);
|
||||
});
|
||||
|
||||
it("maps Haiku to fast while preserving Opus and Sonnet variants", () => {
|
||||
for (const [suffix, variant] of [
|
||||
["claude-haiku", "fast"],
|
||||
["claude-opus", "smart"],
|
||||
["claude-sonnet", "coding"],
|
||||
]) {
|
||||
assert.equal(builtinCatalog.AUTO_TEMPLATE_VARIANTS[`auto/${suffix}`], variant);
|
||||
assert.equal(builtinCatalog.isRecognizedBuiltinAuto(`auto/${suffix}`, suffix), true);
|
||||
assert.deepEqual(builtinCatalog.resolveBuiltinAutoSpec(`auto/${suffix}`, suffix), {
|
||||
variant,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user