531 lines
14 KiB
TypeScript
531 lines
14 KiB
TypeScript
/**
|
|
* Unified provider registry — single source of truth for all provider metadata.
|
|
* All consumer files (provider-label, provider-icon, models-page, add/edit sheets)
|
|
* should derive their data from this registry.
|
|
*/
|
|
|
|
import type { ModelProviderOption } from "@/api/models"
|
|
|
|
export interface ProviderDefinition {
|
|
key: string
|
|
label: string
|
|
labelZh?: string
|
|
iconSlug?: string
|
|
domain?: string
|
|
defaultApiBase?: string
|
|
requiresApiKey: boolean
|
|
isLocal: boolean
|
|
priority: number
|
|
commonModels?: string[]
|
|
aliases?: string[]
|
|
/** Whether this provider supports the OpenAI-compatible /models listing endpoint. */
|
|
supportsFetch?: boolean
|
|
}
|
|
|
|
export const PROVIDERS: ProviderDefinition[] = [
|
|
{
|
|
key: "openai",
|
|
label: "OpenAI",
|
|
iconSlug: "openai",
|
|
domain: "openai.com",
|
|
defaultApiBase: "https://api.openai.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 100,
|
|
commonModels: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o3-mini"],
|
|
aliases: ["gpt"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "anthropic",
|
|
label: "Anthropic",
|
|
iconSlug: "anthropic",
|
|
domain: "anthropic.com",
|
|
defaultApiBase: "https://api.anthropic.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 95,
|
|
commonModels: [
|
|
"claude-sonnet-4-20250514",
|
|
"claude-haiku-4-20250414",
|
|
"claude-3-5-sonnet-20241022",
|
|
],
|
|
aliases: ["claude"],
|
|
},
|
|
{
|
|
key: "gemini",
|
|
label: "Google Gemini",
|
|
iconSlug: "googlegemini",
|
|
domain: "gemini.google.com",
|
|
defaultApiBase: "https://generativelanguage.googleapis.com/v1beta",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 90,
|
|
commonModels: ["gemini-2.0-flash", "gemini-2.5-pro", "gemini-1.5-flash"],
|
|
aliases: ["google"],
|
|
},
|
|
{
|
|
key: "deepseek",
|
|
label: "DeepSeek",
|
|
iconSlug: "deepseek",
|
|
domain: "deepseek.com",
|
|
defaultApiBase: "https://api.deepseek.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 85,
|
|
commonModels: ["deepseek-chat", "deepseek-reasoner"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "openrouter",
|
|
label: "OpenRouter",
|
|
iconSlug: "openrouter",
|
|
domain: "openrouter.ai",
|
|
defaultApiBase: "https://openrouter.ai/api/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 80,
|
|
commonModels: [
|
|
"openai/gpt-4o",
|
|
"anthropic/claude-sonnet-4",
|
|
"google/gemini-2.0-flash",
|
|
],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "qwen-portal",
|
|
label: "Qwen",
|
|
labelZh: "Qwen (阿里云)",
|
|
iconSlug: "alibabacloud",
|
|
domain: "qwenlm.ai",
|
|
defaultApiBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 75,
|
|
commonModels: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
|
aliases: ["qwen"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "qwen-intl",
|
|
label: "Qwen International",
|
|
iconSlug: "alibabacloud",
|
|
domain: "alibabacloud.com",
|
|
defaultApiBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 74,
|
|
commonModels: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
|
aliases: ["qwen-international", "dashscope-intl"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "moonshot",
|
|
label: "Moonshot",
|
|
labelZh: "Moonshot (月之暗面)",
|
|
domain: "moonshot.ai",
|
|
defaultApiBase: "https://api.moonshot.cn/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 70,
|
|
commonModels: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "volcengine",
|
|
label: "Volcengine",
|
|
labelZh: "Volcengine (火山引擎)",
|
|
iconSlug: "bytedance",
|
|
domain: "volcengine.com",
|
|
defaultApiBase: "https://ark.cn-beijing.volces.com/api/v3",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 69,
|
|
commonModels: ["doubao-1.5-pro", "doubao-1.5-lite"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "zhipu",
|
|
label: "Zhipu AI",
|
|
labelZh: "Zhipu AI (智谱)",
|
|
iconSlug: "zhipu",
|
|
domain: "zhipuai.cn",
|
|
defaultApiBase: "https://open.bigmodel.cn/api/paas/v4",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 68,
|
|
commonModels: ["glm-4-plus", "glm-4-flash"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "groq",
|
|
label: "Groq",
|
|
iconSlug: "groq",
|
|
domain: "groq.com",
|
|
defaultApiBase: "https://api.groq.com/openai/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 65,
|
|
commonModels: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "mistral",
|
|
label: "Mistral AI",
|
|
iconSlug: "mistralai",
|
|
domain: "mistral.ai",
|
|
defaultApiBase: "https://api.mistral.ai/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 64,
|
|
commonModels: ["mistral-large-latest", "mistral-small-latest"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "nvidia",
|
|
label: "NVIDIA",
|
|
iconSlug: "nvidia",
|
|
domain: "nvidia.com",
|
|
defaultApiBase: "https://integrate.api.nvidia.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 63,
|
|
commonModels: ["meta/llama-3.1-405b-instruct"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "cerebras",
|
|
label: "Cerebras",
|
|
iconSlug: "cerebras",
|
|
domain: "cerebras.ai",
|
|
defaultApiBase: "https://api.cerebras.ai/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 62,
|
|
commonModels: ["llama3.1-8b", "llama3.1-70b"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "azure",
|
|
label: "Azure OpenAI",
|
|
iconSlug: "microsoftazure",
|
|
domain: "azure.com",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 61,
|
|
commonModels: ["gpt-4o", "gpt-4o-mini"],
|
|
},
|
|
{
|
|
key: "github-copilot",
|
|
label: "GitHub Copilot",
|
|
iconSlug: "githubcopilot",
|
|
domain: "github.com",
|
|
requiresApiKey: false,
|
|
isLocal: true,
|
|
priority: 55,
|
|
},
|
|
{
|
|
key: "antigravity",
|
|
label: "Google Code Assist",
|
|
domain: "antigravity.google",
|
|
requiresApiKey: false,
|
|
isLocal: false,
|
|
priority: 54,
|
|
},
|
|
{
|
|
key: "ollama",
|
|
label: "Ollama",
|
|
labelZh: "Ollama (本地)",
|
|
iconSlug: "ollama",
|
|
domain: "ollama.com",
|
|
defaultApiBase: "http://localhost:11434/v1",
|
|
requiresApiKey: false,
|
|
isLocal: true,
|
|
priority: 50,
|
|
commonModels: ["llama3", "mistral", "codellama", "qwen2.5"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "vllm",
|
|
label: "VLLM",
|
|
labelZh: "VLLM (本地)",
|
|
domain: "vllm.ai",
|
|
defaultApiBase: "http://localhost:8000/v1",
|
|
requiresApiKey: false,
|
|
isLocal: true,
|
|
priority: 49,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "lmstudio",
|
|
label: "LM Studio",
|
|
labelZh: "LM Studio (本地)",
|
|
domain: "lmstudio.ai",
|
|
defaultApiBase: "http://localhost:1234/v1",
|
|
requiresApiKey: false,
|
|
isLocal: true,
|
|
priority: 48,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "venice",
|
|
label: "Venice AI",
|
|
iconSlug: "venice",
|
|
domain: "venice.ai",
|
|
defaultApiBase: "https://api.venice.ai/api/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 45,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "shengsuanyun",
|
|
label: "ShengsuanYun",
|
|
labelZh: "ShengsuanYun (神算云)",
|
|
domain: "shengsuanyun.com",
|
|
defaultApiBase: "https://router.shengsuanyun.com/api/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 44,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "siliconflow",
|
|
label: "SiliconFlow",
|
|
labelZh: "硅基流动",
|
|
domain: "siliconflow.cn",
|
|
defaultApiBase: "https://api.siliconflow.cn/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 43.5,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "vivgrid",
|
|
label: "Vivgrid",
|
|
domain: "vivgrid.com",
|
|
defaultApiBase: "https://api.vivgrid.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 43,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "minimax",
|
|
label: "MiniMax",
|
|
domain: "minimaxi.com",
|
|
defaultApiBase: "https://api.minimaxi.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 42,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "longcat",
|
|
label: "LongCat",
|
|
domain: "longcat.chat",
|
|
defaultApiBase: "https://api.longcat.chat/openai",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 41,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "modelscope",
|
|
label: "ModelScope",
|
|
labelZh: "ModelScope (魔搭社区)",
|
|
domain: "modelscope.cn",
|
|
defaultApiBase: "https://api-inference.modelscope.cn/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 40,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "mimo",
|
|
label: "Xiaomi MiMo",
|
|
iconSlug: "xiaomi",
|
|
domain: "xiaomi.com",
|
|
defaultApiBase: "https://api.xiaomimimo.com/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 39,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "avian",
|
|
label: "Avian",
|
|
domain: "avian.io",
|
|
defaultApiBase: "https://api.avian.io/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 38,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "zai",
|
|
label: "Z.ai",
|
|
domain: "z.ai",
|
|
defaultApiBase: "https://api.z.ai/api/coding/paas/v4",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 37,
|
|
aliases: ["z.ai", "z-ai"],
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "novita",
|
|
label: "Novita AI",
|
|
domain: "novita.ai",
|
|
defaultApiBase: "https://api.novita.ai/openai",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 36,
|
|
supportsFetch: true,
|
|
},
|
|
{
|
|
key: "litellm",
|
|
label: "LiteLLM",
|
|
domain: "litellm.ai",
|
|
defaultApiBase: "http://localhost:4000/v1",
|
|
requiresApiKey: true,
|
|
isLocal: false,
|
|
priority: 35,
|
|
supportsFetch: true,
|
|
},
|
|
]
|
|
|
|
// ── Derived data for consumers ───────────────────────────────────────────────
|
|
|
|
export const PROVIDER_MAP = new Map(PROVIDERS.map((p) => [p.key, p]))
|
|
|
|
export const PROVIDER_LABELS: Record<string, string> = Object.fromEntries(
|
|
PROVIDERS.map((p) => [p.key, p.labelZh || p.label]),
|
|
)
|
|
|
|
export const PROVIDER_ALIASES: Record<string, string> = Object.fromEntries(
|
|
PROVIDERS.flatMap((p) => (p.aliases || []).map((a) => [a, p.key])),
|
|
)
|
|
|
|
export const KNOWN_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key))
|
|
|
|
export const FETCHABLE_PROVIDER_KEYS = new Set(
|
|
PROVIDERS.filter((p) => p.supportsFetch).map((p) => p.key),
|
|
)
|
|
|
|
export const PROVIDER_ICON_SLUGS: Record<string, string> = Object.fromEntries(
|
|
PROVIDERS.filter((p) => p.iconSlug).map((p) => [p.key, p.iconSlug!]),
|
|
)
|
|
|
|
export const PROVIDER_DOMAINS: Record<string, string> = Object.fromEntries(
|
|
PROVIDERS.filter((p) => p.domain).map((p) => [p.key, p.domain!]),
|
|
)
|
|
|
|
export const PROVIDER_PRIORITY: Record<string, number> = Object.fromEntries(
|
|
PROVIDERS.map((p) => [p.key, p.priority]),
|
|
)
|
|
|
|
export const PROVIDER_API_BASES: Record<string, string> = Object.fromEntries(
|
|
PROVIDERS.filter((p) => p.defaultApiBase).map((p) => [
|
|
p.key,
|
|
p.defaultApiBase!,
|
|
]),
|
|
)
|
|
|
|
/**
|
|
* Find the closest known provider key by edit distance.
|
|
* Returns the key if distance <= 2, otherwise undefined.
|
|
*/
|
|
export function findClosestProvider(input: string): string | undefined {
|
|
const lower = input.toLowerCase()
|
|
let best: string | undefined
|
|
let bestDist = 3 // only accept distance <= 2
|
|
|
|
for (const key of KNOWN_PROVIDER_KEYS) {
|
|
const dist = editDistance(lower, key)
|
|
if (dist < bestDist) {
|
|
bestDist = dist
|
|
best = key
|
|
}
|
|
}
|
|
// Also check aliases
|
|
for (const alias of Object.keys(PROVIDER_ALIASES)) {
|
|
const dist = editDistance(lower, alias)
|
|
if (dist < bestDist) {
|
|
bestDist = dist
|
|
best = PROVIDER_ALIASES[alias]
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
function editDistance(a: string, b: string): number {
|
|
const m = a.length
|
|
const n = b.length
|
|
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
new Array(n + 1).fill(0),
|
|
)
|
|
for (let i = 0; i <= m; i++) dp[i][0] = i
|
|
for (let j = 0; j <= n; j++) dp[0][j] = j
|
|
for (let i = 1; i <= m; i++) {
|
|
for (let j = 1; j <= n; j++) {
|
|
dp[i][j] =
|
|
a[i - 1] === b[j - 1]
|
|
? dp[i - 1][j - 1]
|
|
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
}
|
|
}
|
|
return dp[m][n]
|
|
}
|
|
|
|
// ── Backend options merge ────────────────────────────────────────────────────
|
|
|
|
export interface MergedProvider extends ProviderDefinition {
|
|
createAllowed: boolean
|
|
defaultModelAllowed: boolean
|
|
defaultAuthMethod?: string
|
|
authMethodLocked?: boolean
|
|
}
|
|
|
|
/**
|
|
* Merge the frontend PROVIDERS registry with backend provider_options.
|
|
* Frontend provides presentation data (labels, icons, priority, etc.).
|
|
* Backend provides authoritative availability and policy fields.
|
|
*/
|
|
export function mergeWithBackendOptions(
|
|
backendOptions: ModelProviderOption[],
|
|
): MergedProvider[] {
|
|
const backendMap = new Map(backendOptions.map((o) => [o.id, o]))
|
|
const merged: MergedProvider[] = []
|
|
|
|
// Start with frontend providers, enriched with backend policy
|
|
for (const p of PROVIDERS) {
|
|
const backend = backendMap.get(p.key)
|
|
merged.push({
|
|
...p,
|
|
createAllowed: backend?.create_allowed ?? false,
|
|
defaultModelAllowed: backend?.default_model_allowed ?? false,
|
|
defaultAuthMethod: backend?.default_auth_method,
|
|
authMethodLocked: backend?.auth_method_locked,
|
|
})
|
|
if (backend) backendMap.delete(p.key)
|
|
}
|
|
|
|
// Add providers only known to the backend
|
|
for (const [key, backend] of backendMap) {
|
|
merged.push({
|
|
key,
|
|
label: key,
|
|
requiresApiKey: !backend.empty_api_key_allowed,
|
|
isLocal: backend.empty_api_key_allowed,
|
|
priority: 0,
|
|
createAllowed: backend.create_allowed,
|
|
defaultModelAllowed: backend.default_model_allowed,
|
|
defaultAuthMethod: backend.default_auth_method,
|
|
authMethodLocked: backend.auth_method_locked,
|
|
defaultApiBase: backend.default_api_base || undefined,
|
|
})
|
|
}
|
|
|
|
return merged.sort((a, b) => b.priority - a.priority)
|
|
}
|