fix: feat(providers): add kimi/qwen/deepseek/gpt auto-routing families (#13214) (#13709)

This commit is contained in:
keii-2596
2026-09-18 23:25:53 +08:00
committed by GitHub
parent 2374bbf1ee
commit 80ea176022
4 changed files with 175 additions and 8 deletions

View 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).

View File

@@ -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

View File

@@ -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`. */

View File

@@ -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,
});
}
});
});