| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- /**
- * Server-side Provider Configuration
- *
- * Loads provider configs from YAML (primary) + environment variables (fallback).
- * Keys never leave the server — only provider IDs and metadata are exposed via API.
- */
- import fs from 'fs';
- import path from 'path';
- import yaml from 'js-yaml';
- import { createLogger } from '@/lib/logger';
- const log = createLogger('ServerProviderConfig');
- // ---------------------------------------------------------------------------
- // Types
- // ---------------------------------------------------------------------------
- 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>;
- }
- // ---------------------------------------------------------------------------
- // Env-var prefix mappings
- // ---------------------------------------------------------------------------
- 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',
- GROK: 'grok',
- 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_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',
- };
- const IMAGE_ENV_MAP: Record<string, string> = {
- 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',
- };
- // ---------------------------------------------------------------------------
- // YAML loading
- // ---------------------------------------------------------------------------
- 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 {};
- }
- }
- // ---------------------------------------------------------------------------
- // Env-var helpers
- // ---------------------------------------------------------------------------
- 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> = {};
- // First, add everything from YAML as defaults
- 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,
- };
- }
- }
- }
- // Then, apply env vars (env takes priority over YAML)
- 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]) {
- // YAML entry exists — env vars override individual fields
- if (envApiKey) result[providerId].apiKey = envApiKey;
- if (envBaseUrl) result[providerId].baseUrl = envBaseUrl;
- if (envModels) result[providerId].models = envModels;
- continue;
- }
- // Activate on API key, or base URL alone for keyless providers (e.g. Ollama)
- if (
- requiresBaseUrl
- ? !envBaseUrl
- : !(envApiKey || (envBaseUrl && keylessProviders.has(providerId)))
- )
- continue;
- result[providerId] = {
- apiKey: envApiKey || '',
- baseUrl: envBaseUrl,
- models: envModels,
- };
- }
- return result;
- }
- // ---------------------------------------------------------------------------
- // Module-level cache (process singleton)
- // ---------------------------------------------------------------------------
- const DEFAULT_FILENAME = 'server-providers.yml';
- /** Cache keyed by YAML filename (empty string = default file). */
- 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),
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — LLM
- // ---------------------------------------------------------------------------
- /** Returns server-configured LLM providers (no apiKeys) */
- 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;
- }
- /** Resolve API key: client key > server key > empty string */
- export function resolveApiKey(providerId: string, clientKey?: string): string {
- if (clientKey) return clientKey;
- return getConfig().providers[providerId]?.apiKey || '';
- }
- /** Resolve base URL: client > server > undefined */
- export function resolveBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined {
- if (clientBaseUrl) return clientBaseUrl;
- return getConfig().providers[providerId]?.baseUrl;
- }
- /** Resolve proxy URL for a provider (server config only) */
- export function resolveProxy(providerId: string): string | undefined {
- return getConfig().providers[providerId]?.proxy;
- }
- // ---------------------------------------------------------------------------
- // Public API — TTS
- // ---------------------------------------------------------------------------
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — ASR
- // ---------------------------------------------------------------------------
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — PDF
- // ---------------------------------------------------------------------------
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — Image Generation
- // ---------------------------------------------------------------------------
- export function getServerImageProviders(): Record<string, Record<string, never>> {
- const cfg = getConfig();
- const result: Record<string, Record<string, never>> = {};
- for (const id of Object.keys(cfg.image)) {
- result[id] = {};
- }
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — Video Generation
- // ---------------------------------------------------------------------------
- export function getServerVideoProviders(): Record<string, Record<string, never>> {
- const cfg = getConfig();
- const result: Record<string, Record<string, never>> = {};
- for (const id of Object.keys(cfg.video)) {
- result[id] = {};
- }
- 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;
- }
- // ---------------------------------------------------------------------------
- // Public API — Web Search (Tavily)
- // ---------------------------------------------------------------------------
- /** Returns server-configured web search providers (no apiKeys exposed) */
- 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;
- }
- /** Resolve Tavily API key: client key > server key > TAVILY_API_KEY env > empty */
- 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 || '';
- }
|