| |
| |
| |
| |
| |
| |
|
|
| import fs from 'fs'; |
| import path from 'path'; |
| import yaml from 'js-yaml'; |
| import { createLogger } from '@/lib/logger'; |
|
|
| const log = createLogger('ServerProviderConfig'); |
|
|
| |
| |
| |
|
|
| interface ServerProviderEntry { |
| apiKey: string; |
| baseUrl?: string; |
| models?: string[]; |
| proxy?: string; |
| } |
|
|
| interface ServerConfig { |
| providers: Record<string, ServerProviderEntry>; |
| tts: Record<string, ServerProviderEntry>; |
| asr: Record<string, ServerProviderEntry>; |
| pdf: Record<string, ServerProviderEntry>; |
| image: Record<string, ServerProviderEntry>; |
| video: Record<string, ServerProviderEntry>; |
| webSearch: Record<string, ServerProviderEntry>; |
| } |
|
|
| |
| |
| |
|
|
| const LLM_ENV_MAP: Record<string, string> = { |
| OPENAI: 'openai', |
| ANTHROPIC: 'anthropic', |
| GOOGLE: 'google', |
| DEEPSEEK: 'deepseek', |
| QWEN: 'qwen', |
| KIMI: 'kimi', |
| MINIMAX: 'minimax', |
| GLM: 'glm', |
| SILICONFLOW: 'siliconflow', |
| DOUBAO: 'doubao', |
| OPENROUTER: 'openrouter', |
| GROK: 'grok', |
| TENCENT: 'tencent-hunyuan', |
| TENCENT_HUNYUAN: 'tencent-hunyuan', |
| XIAOMI: 'xiaomi', |
| MIMO: 'xiaomi', |
| OLLAMA: 'ollama', |
| }; |
|
|
| const TTS_ENV_MAP: Record<string, string> = { |
| TTS_OPENAI: 'openai-tts', |
| TTS_AZURE: 'azure-tts', |
| TTS_GLM: 'glm-tts', |
| TTS_QWEN: 'qwen-tts', |
| TTS_VOXCPM: 'voxcpm-tts', |
| TTS_DOUBAO: 'doubao-tts', |
| TTS_ELEVENLABS: 'elevenlabs-tts', |
| TTS_MINIMAX: 'minimax-tts', |
| }; |
|
|
| const ASR_ENV_MAP: Record<string, string> = { |
| ASR_OPENAI: 'openai-whisper', |
| ASR_QWEN: 'qwen-asr', |
| }; |
|
|
| const PDF_ENV_MAP: Record<string, string> = { |
| PDF_UNPDF: 'unpdf', |
| PDF_MINERU: 'mineru', |
| PDF_MINERU_CLOUD: 'mineru-cloud', |
| }; |
|
|
| const IMAGE_ENV_MAP: Record<string, string> = { |
| IMAGE_OPENAI: 'openai-image', |
| IMAGE_SEEDREAM: 'seedream', |
| IMAGE_QWEN_IMAGE: 'qwen-image', |
| IMAGE_NANO_BANANA: 'nano-banana', |
| IMAGE_MINIMAX: 'minimax-image', |
| IMAGE_GROK: 'grok-image', |
| }; |
|
|
| const VIDEO_ENV_MAP: Record<string, string> = { |
| VIDEO_SEEDANCE: 'seedance', |
| VIDEO_KLING: 'kling', |
| VIDEO_VEO: 'veo', |
| VIDEO_SORA: 'sora', |
| VIDEO_MINIMAX: 'minimax-video', |
| VIDEO_GROK: 'grok-video', |
| }; |
|
|
| const WEB_SEARCH_ENV_MAP: Record<string, string> = { |
| TAVILY: 'tavily', |
| }; |
|
|
| |
| |
| |
|
|
| type YamlData = Partial<{ |
| providers: Record<string, Partial<ServerProviderEntry>>; |
| tts: Record<string, Partial<ServerProviderEntry>>; |
| asr: Record<string, Partial<ServerProviderEntry>>; |
| pdf: Record<string, Partial<ServerProviderEntry>>; |
| image: Record<string, Partial<ServerProviderEntry>>; |
| video: Record<string, Partial<ServerProviderEntry>>; |
| 'web-search': Record<string, Partial<ServerProviderEntry>>; |
| }>; |
|
|
| function loadYamlFile(filename: string): YamlData { |
| try { |
| const filePath = path.join(process.cwd(), filename); |
| if (!fs.existsSync(filePath)) return {}; |
| const raw = fs.readFileSync(filePath, 'utf-8'); |
| const parsed = yaml.load(raw) as Record<string, unknown> | null; |
| if (!parsed || typeof parsed !== 'object') return {}; |
| return parsed as YamlData; |
| } catch (e) { |
| log.warn(`[ServerProviderConfig] Failed to load ${filename}:`, e); |
| return {}; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function loadEnvSection( |
| envMap: Record<string, string>, |
| yamlSection: Record<string, Partial<ServerProviderEntry>> | undefined, |
| { |
| requiresBaseUrl = false, |
| keylessProviders = new Set<string>(), |
| }: { requiresBaseUrl?: boolean; keylessProviders?: Set<string> } = {}, |
| ): Record<string, ServerProviderEntry> { |
| const result: Record<string, ServerProviderEntry> = {}; |
|
|
| |
| if (yamlSection) { |
| for (const [id, entry] of Object.entries(yamlSection)) { |
| if ( |
| requiresBaseUrl |
| ? !!entry?.baseUrl |
| : entry?.apiKey || (entry?.baseUrl && keylessProviders.has(id)) |
| ) { |
| result[id] = { |
| apiKey: entry.apiKey || '', |
| baseUrl: entry.baseUrl, |
| models: entry.models, |
| proxy: entry.proxy, |
| }; |
| } |
| } |
| } |
|
|
| |
| for (const [prefix, providerId] of Object.entries(envMap)) { |
| const envApiKey = process.env[`${prefix}_API_KEY`] || undefined; |
| const envBaseUrl = process.env[`${prefix}_BASE_URL`] || undefined; |
| const envModelsStr = process.env[`${prefix}_MODELS`]; |
| const envModels = envModelsStr |
| ? envModelsStr |
| .split(',') |
| .map((m) => m.trim()) |
| .filter(Boolean) |
| : undefined; |
|
|
| if (result[providerId]) { |
| |
| if (envApiKey) result[providerId].apiKey = envApiKey; |
| if (envBaseUrl) result[providerId].baseUrl = envBaseUrl; |
| if (envModels) result[providerId].models = envModels; |
| continue; |
| } |
|
|
| |
| if ( |
| requiresBaseUrl |
| ? !envBaseUrl |
| : !(envApiKey || (envBaseUrl && keylessProviders.has(providerId))) |
| ) |
| continue; |
| result[providerId] = { |
| apiKey: envApiKey || '', |
| baseUrl: envBaseUrl, |
| models: envModels, |
| }; |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
|
|
| const DEFAULT_FILENAME = 'server-providers.yml'; |
|
|
| |
| const _configs: Map<string, ServerConfig> = new Map(); |
|
|
| function buildConfig(yamlData: YamlData): ServerConfig { |
| return { |
| providers: loadEnvSection(LLM_ENV_MAP, yamlData.providers, { |
| keylessProviders: new Set(['ollama']), |
| }), |
| tts: loadEnvSection(TTS_ENV_MAP, yamlData.tts, { |
| keylessProviders: new Set(['voxcpm-tts']), |
| }), |
| asr: loadEnvSection(ASR_ENV_MAP, yamlData.asr), |
| pdf: loadEnvSection(PDF_ENV_MAP, yamlData.pdf, { requiresBaseUrl: true }), |
| image: loadEnvSection(IMAGE_ENV_MAP, yamlData.image), |
| video: loadEnvSection(VIDEO_ENV_MAP, yamlData.video), |
| webSearch: loadEnvSection(WEB_SEARCH_ENV_MAP, yamlData['web-search']), |
| }; |
| } |
|
|
| function logConfig(config: ServerConfig, label: string): void { |
| const counts = [ |
| Object.keys(config.providers).length, |
| Object.keys(config.tts).length, |
| Object.keys(config.asr).length, |
| Object.keys(config.pdf).length, |
| Object.keys(config.image).length, |
| Object.keys(config.video).length, |
| Object.keys(config.webSearch).length, |
| ]; |
| if (counts.some((c) => c > 0)) { |
| log.info( |
| `[ServerProviderConfig] Loaded (${label}): ${counts[0]} LLM, ${counts[1]} TTS, ${counts[2]} ASR, ${counts[3]} PDF, ${counts[4]} Image, ${counts[5]} Video, ${counts[6]} WebSearch providers`, |
| ); |
| } |
| } |
|
|
| function getConfig(): ServerConfig { |
| const cached = _configs.get(''); |
| if (cached) return cached; |
|
|
| const yamlData = loadYamlFile(DEFAULT_FILENAME); |
| const config = buildConfig(yamlData); |
| logConfig(config, DEFAULT_FILENAME); |
| _configs.set('', config); |
| return config; |
| } |
|
|
| |
| |
| |
|
|
| |
| export function getServerProviders(): Record<string, { models?: string[]; baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { models?: string[]; baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.providers)) { |
| result[id] = {}; |
| if (entry.models && entry.models.length > 0) result[id].models = entry.models; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| |
| export function resolveApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().providers[providerId]?.apiKey || ''; |
| } |
|
|
| |
| export function resolveBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().providers[providerId]?.baseUrl; |
| } |
|
|
| |
| export function resolveProxy(providerId: string): string | undefined { |
| return getConfig().providers[providerId]?.proxy; |
| } |
|
|
| |
| |
| |
|
|
| export function getServerTTSProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.tts)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| export function resolveTTSApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().tts[providerId]?.apiKey || ''; |
| } |
|
|
| export function resolveTTSBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().tts[providerId]?.baseUrl; |
| } |
|
|
| |
| |
| |
|
|
| export function getServerASRProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.asr)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| export function resolveASRApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().asr[providerId]?.apiKey || ''; |
| } |
|
|
| export function resolveASRBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().asr[providerId]?.baseUrl; |
| } |
|
|
| |
| |
| |
|
|
| export function getServerPDFProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.pdf)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| export function resolvePDFApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().pdf[providerId]?.apiKey || ''; |
| } |
|
|
| export function resolvePDFBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().pdf[providerId]?.baseUrl; |
| } |
|
|
| |
| |
| |
|
|
| export function getServerImageProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.image)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| export function resolveImageApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().image[providerId]?.apiKey || ''; |
| } |
|
|
| export function resolveImageBaseUrl( |
| providerId: string, |
| clientBaseUrl?: string, |
| ): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().image[providerId]?.baseUrl; |
| } |
|
|
| |
| |
| |
|
|
| export function getServerVideoProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.video)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| export function resolveVideoApiKey(providerId: string, clientKey?: string): string { |
| if (clientKey) return clientKey; |
| return getConfig().video[providerId]?.apiKey || ''; |
| } |
|
|
| export function resolveVideoBaseUrl( |
| providerId: string, |
| clientBaseUrl?: string, |
| ): string | undefined { |
| if (clientBaseUrl) return clientBaseUrl; |
| return getConfig().video[providerId]?.baseUrl; |
| } |
|
|
| |
| |
| |
|
|
| |
| export function getServerWebSearchProviders(): Record<string, { baseUrl?: string }> { |
| const cfg = getConfig(); |
| const result: Record<string, { baseUrl?: string }> = {}; |
| for (const [id, entry] of Object.entries(cfg.webSearch)) { |
| result[id] = {}; |
| if (entry.baseUrl) result[id].baseUrl = entry.baseUrl; |
| } |
| return result; |
| } |
|
|
| |
| export function resolveWebSearchApiKey(clientKey?: string): string { |
| if (clientKey) return clientKey; |
| const serverKey = getConfig().webSearch.tavily?.apiKey; |
| if (serverKey) return serverKey; |
| return process.env.TAVILY_API_KEY || ''; |
| } |
|
|