export type ProviderKind = "openai-compatible" | "anthropic" | "gemini"; export type ProviderId = | "deepseek" | "zhipu" | "openai" | "anthropic" | "gemini" | "custom-openai"; export type ModelProvider = { id: ProviderId; name: string; kind: ProviderKind; baseUrl?: string; apiKeyEnv: string; apiKey?: string; configured: boolean; }; export type ChatModel = { id: string; label: string; providerId: ProviderId; providerName: string; providerKind: ProviderKind; model: string; baseUrl: string; enabled: boolean; configured: boolean; apiKeyConfigured: boolean; apiKeyPreview: string; apiKeySource: "model" | "provider-env" | "none"; configUpdatedAt?: string; accent: "red" | "green" | "blue" | "gold" | "ink"; }; import { getSavedModelConfig, maskApiKey } from "./modelConfigStore.js"; const env = (key: string, fallback = "") => process.env[key]?.trim() || fallback; export const providers = (): ModelProvider[] => [ { id: "deepseek", name: "DeepSeek", kind: "openai-compatible", baseUrl: env("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), apiKeyEnv: "DEEPSEEK_API_KEY", configured: Boolean(env("DEEPSEEK_API_KEY")) }, { id: "zhipu", name: "Zhipu GLM", kind: "openai-compatible", baseUrl: env("ZHIPU_BASE_URL", "https://open.bigmodel.cn/api/paas/v4"), apiKeyEnv: "ZHIPU_API_KEY", configured: Boolean(env("ZHIPU_API_KEY")) }, { id: "openai", name: "OpenAI", kind: "openai-compatible", baseUrl: env("OPENAI_BASE_URL", "https://api.openai.com/v1"), apiKeyEnv: "OPENAI_API_KEY", configured: Boolean(env("OPENAI_API_KEY")) }, { id: "anthropic", name: "Anthropic", kind: "anthropic", baseUrl: "https://api.anthropic.com/v1", apiKeyEnv: "ANTHROPIC_API_KEY", configured: Boolean(env("ANTHROPIC_API_KEY")) }, { id: "gemini", name: "Google Gemini", kind: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", apiKeyEnv: "GEMINI_API_KEY", configured: Boolean(env("GEMINI_API_KEY")) } ]; const resolveModel = ( model: Omit< ChatModel, | "providerName" | "providerKind" | "configured" | "baseUrl" | "enabled" | "apiKeyConfigured" | "apiKeyPreview" | "apiKeySource" >, provider: ModelProvider ): ChatModel => { const saved = getSavedModelConfig(model.id); const envApiKey = env(provider.apiKeyEnv); const apiKey = saved?.apiKey || envApiKey; const apiKeySource = saved?.apiKey ? "model" : envApiKey ? "provider-env" : "none"; return { ...model, providerName: provider.name, providerKind: provider.kind, model: saved?.model || model.model, baseUrl: saved?.baseUrl || provider.baseUrl || "", enabled: saved?.enabled ?? true, configured: Boolean(apiKey), apiKeyConfigured: Boolean(apiKey), apiKeyPreview: maskApiKey(apiKey), apiKeySource, configUpdatedAt: saved?.updatedAt }; }; export const registry = (): ChatModel[] => { const providerMap = new Map(providers().map((provider) => [provider.id, provider])); const baseModels: Array< Omit< ChatModel, | "providerName" | "providerKind" | "configured" | "baseUrl" | "enabled" | "apiKeyConfigured" | "apiKeyPreview" | "apiKeySource" > > = [ { id: "deepseek-v4-pro", label: "DeepSeekV4-pro", providerId: "deepseek", model: env("DEEPSEEK_MODEL_V4_PRO", "DeepSeekV4-pro"), accent: "red" }, { id: "deepseek-v4-flash", label: "DeepSeekV4-flash", providerId: "deepseek", model: env("DEEPSEEK_MODEL_V4_FLASH", "DeepSeekV4-flash"), accent: "red" }, { id: "glm-5-1", label: "GLM5.1", providerId: "zhipu", model: env("ZHIPU_MODEL_GLM_5_1", "GLM5.1"), accent: "green" }, { id: "gpt-5-5", label: "GPT-5.5", providerId: "openai", model: env("OPENAI_MODEL_GPT_5_5", "gpt-5.5"), accent: "blue" }, { id: "claude-opus-4-7", label: "Claude Opus 4.7", providerId: "anthropic", model: env("ANTHROPIC_MODEL_CLAUDE_OPUS_4_7", "claude-opus-4.7"), accent: "gold" }, { id: "gemini-3-1-pro", label: "Gemini-3.1-pro", providerId: "gemini", model: env("GEMINI_MODEL_3_1_PRO", "Gemini-3.1-pro"), accent: "ink" }, { id: "gemini-3-5-flash", label: "gemini3.5-flash", providerId: "gemini", model: env("GEMINI_MODEL_3_5_FLASH", "gemini3.5-flash"), accent: "ink" } ]; const models = baseModels.map((model) => { const provider = providerMap.get(model.providerId); if (!provider) { throw new Error(`Missing provider for ${model.id}`); } return resolveModel(model, provider); }); const customIds = env("CUSTOM_OPENAI_MODEL_IDS") .split(",") .map((value) => value.trim()) .filter(Boolean); if (customIds.length && env("CUSTOM_OPENAI_BASE_URL")) { const customConfigured = Boolean(env("CUSTOM_OPENAI_API_KEY")); models.push( ...customIds.map((modelId) => resolveModel( { id: `custom-${modelId.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`, label: modelId, providerId: "custom-openai" as const, model: modelId, configUpdatedAt: undefined, accent: "blue" as const }, { id: "custom-openai", name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"), kind: "openai-compatible", baseUrl: env("CUSTOM_OPENAI_BASE_URL"), apiKeyEnv: "CUSTOM_OPENAI_API_KEY", configured: customConfigured } ) ) ); } return models; }; export const findModel = (modelId: string) => registry().find((model) => model.id === modelId); export const providerForModel = (model: ChatModel): ModelProvider => { const saved = getSavedModelConfig(model.id); if (model.providerId === "custom-openai") { const envApiKey = env("CUSTOM_OPENAI_API_KEY"); return { id: "custom-openai", name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"), kind: "openai-compatible", baseUrl: saved?.baseUrl || env("CUSTOM_OPENAI_BASE_URL"), apiKeyEnv: "CUSTOM_OPENAI_API_KEY", apiKey: saved?.apiKey, configured: Boolean(saved?.apiKey || envApiKey) }; } const provider = providers().find((item) => item.id === model.providerId); if (!provider) { throw new Error(`Provider not found for ${model.label}`); } const envApiKey = env(provider.apiKeyEnv); return { ...provider, baseUrl: saved?.baseUrl || provider.baseUrl, apiKey: saved?.apiKey, configured: Boolean(saved?.apiKey || envApiKey) }; }; export const apiKeyForProvider = (provider: ModelProvider) => provider.apiKey || env(provider.apiKeyEnv);