provider-config.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /**
  2. * Server-side Provider Configuration
  3. *
  4. * Loads provider configs from YAML (primary) + environment variables (fallback).
  5. * Keys never leave the server — only provider IDs and metadata are exposed via API.
  6. */
  7. import fs from 'fs';
  8. import path from 'path';
  9. import yaml from 'js-yaml';
  10. import { createLogger } from '@/lib/logger';
  11. const log = createLogger('ServerProviderConfig');
  12. // ---------------------------------------------------------------------------
  13. // Types
  14. // ---------------------------------------------------------------------------
  15. interface ServerProviderEntry {
  16. apiKey: string;
  17. baseUrl?: string;
  18. models?: string[];
  19. proxy?: string;
  20. }
  21. interface ServerConfig {
  22. providers: Record<string, ServerProviderEntry>;
  23. tts: Record<string, ServerProviderEntry>;
  24. asr: Record<string, ServerProviderEntry>;
  25. pdf: Record<string, ServerProviderEntry>;
  26. image: Record<string, ServerProviderEntry>;
  27. video: Record<string, ServerProviderEntry>;
  28. webSearch: Record<string, ServerProviderEntry>;
  29. }
  30. // ---------------------------------------------------------------------------
  31. // Env-var prefix mappings
  32. // ---------------------------------------------------------------------------
  33. const LLM_ENV_MAP: Record<string, string> = {
  34. OPENAI: 'openai',
  35. ANTHROPIC: 'anthropic',
  36. GOOGLE: 'google',
  37. DEEPSEEK: 'deepseek',
  38. QWEN: 'qwen',
  39. KIMI: 'kimi',
  40. MINIMAX: 'minimax',
  41. GLM: 'glm',
  42. SILICONFLOW: 'siliconflow',
  43. DOUBAO: 'doubao',
  44. GROK: 'grok',
  45. OLLAMA: 'ollama',
  46. };
  47. const TTS_ENV_MAP: Record<string, string> = {
  48. TTS_OPENAI: 'openai-tts',
  49. TTS_AZURE: 'azure-tts',
  50. TTS_GLM: 'glm-tts',
  51. TTS_QWEN: 'qwen-tts',
  52. TTS_DOUBAO: 'doubao-tts',
  53. TTS_ELEVENLABS: 'elevenlabs-tts',
  54. TTS_MINIMAX: 'minimax-tts',
  55. };
  56. const ASR_ENV_MAP: Record<string, string> = {
  57. ASR_OPENAI: 'openai-whisper',
  58. ASR_QWEN: 'qwen-asr',
  59. };
  60. const PDF_ENV_MAP: Record<string, string> = {
  61. PDF_UNPDF: 'unpdf',
  62. PDF_MINERU: 'mineru',
  63. };
  64. const IMAGE_ENV_MAP: Record<string, string> = {
  65. IMAGE_SEEDREAM: 'seedream',
  66. IMAGE_QWEN_IMAGE: 'qwen-image',
  67. IMAGE_NANO_BANANA: 'nano-banana',
  68. IMAGE_MINIMAX: 'minimax-image',
  69. IMAGE_GROK: 'grok-image',
  70. };
  71. const VIDEO_ENV_MAP: Record<string, string> = {
  72. VIDEO_SEEDANCE: 'seedance',
  73. VIDEO_KLING: 'kling',
  74. VIDEO_VEO: 'veo',
  75. VIDEO_SORA: 'sora',
  76. VIDEO_MINIMAX: 'minimax-video',
  77. VIDEO_GROK: 'grok-video',
  78. };
  79. const WEB_SEARCH_ENV_MAP: Record<string, string> = {
  80. TAVILY: 'tavily',
  81. };
  82. // ---------------------------------------------------------------------------
  83. // YAML loading
  84. // ---------------------------------------------------------------------------
  85. type YamlData = Partial<{
  86. providers: Record<string, Partial<ServerProviderEntry>>;
  87. tts: Record<string, Partial<ServerProviderEntry>>;
  88. asr: Record<string, Partial<ServerProviderEntry>>;
  89. pdf: Record<string, Partial<ServerProviderEntry>>;
  90. image: Record<string, Partial<ServerProviderEntry>>;
  91. video: Record<string, Partial<ServerProviderEntry>>;
  92. 'web-search': Record<string, Partial<ServerProviderEntry>>;
  93. }>;
  94. function loadYamlFile(filename: string): YamlData {
  95. try {
  96. const filePath = path.join(process.cwd(), filename);
  97. if (!fs.existsSync(filePath)) return {};
  98. const raw = fs.readFileSync(filePath, 'utf-8');
  99. const parsed = yaml.load(raw) as Record<string, unknown> | null;
  100. if (!parsed || typeof parsed !== 'object') return {};
  101. return parsed as YamlData;
  102. } catch (e) {
  103. log.warn(`[ServerProviderConfig] Failed to load ${filename}:`, e);
  104. return {};
  105. }
  106. }
  107. // ---------------------------------------------------------------------------
  108. // Env-var helpers
  109. // ---------------------------------------------------------------------------
  110. function loadEnvSection(
  111. envMap: Record<string, string>,
  112. yamlSection: Record<string, Partial<ServerProviderEntry>> | undefined,
  113. {
  114. requiresBaseUrl = false,
  115. keylessProviders = new Set<string>(),
  116. }: { requiresBaseUrl?: boolean; keylessProviders?: Set<string> } = {},
  117. ): Record<string, ServerProviderEntry> {
  118. const result: Record<string, ServerProviderEntry> = {};
  119. // First, add everything from YAML as defaults
  120. if (yamlSection) {
  121. for (const [id, entry] of Object.entries(yamlSection)) {
  122. if (
  123. requiresBaseUrl
  124. ? !!entry?.baseUrl
  125. : entry?.apiKey || (entry?.baseUrl && keylessProviders.has(id))
  126. ) {
  127. result[id] = {
  128. apiKey: entry.apiKey || '',
  129. baseUrl: entry.baseUrl,
  130. models: entry.models,
  131. proxy: entry.proxy,
  132. };
  133. }
  134. }
  135. }
  136. // Then, apply env vars (env takes priority over YAML)
  137. for (const [prefix, providerId] of Object.entries(envMap)) {
  138. const envApiKey = process.env[`${prefix}_API_KEY`] || undefined;
  139. const envBaseUrl = process.env[`${prefix}_BASE_URL`] || undefined;
  140. const envModelsStr = process.env[`${prefix}_MODELS`];
  141. const envModels = envModelsStr
  142. ? envModelsStr
  143. .split(',')
  144. .map((m) => m.trim())
  145. .filter(Boolean)
  146. : undefined;
  147. if (result[providerId]) {
  148. // YAML entry exists — env vars override individual fields
  149. if (envApiKey) result[providerId].apiKey = envApiKey;
  150. if (envBaseUrl) result[providerId].baseUrl = envBaseUrl;
  151. if (envModels) result[providerId].models = envModels;
  152. continue;
  153. }
  154. // Activate on API key, or base URL alone for keyless providers (e.g. Ollama)
  155. if (
  156. requiresBaseUrl
  157. ? !envBaseUrl
  158. : !(envApiKey || (envBaseUrl && keylessProviders.has(providerId)))
  159. )
  160. continue;
  161. result[providerId] = {
  162. apiKey: envApiKey || '',
  163. baseUrl: envBaseUrl,
  164. models: envModels,
  165. };
  166. }
  167. return result;
  168. }
  169. // ---------------------------------------------------------------------------
  170. // Module-level cache (process singleton)
  171. // ---------------------------------------------------------------------------
  172. const DEFAULT_FILENAME = 'server-providers.yml';
  173. /** Cache keyed by YAML filename (empty string = default file). */
  174. const _configs: Map<string, ServerConfig> = new Map();
  175. function buildConfig(yamlData: YamlData): ServerConfig {
  176. return {
  177. providers: loadEnvSection(LLM_ENV_MAP, yamlData.providers, {
  178. keylessProviders: new Set(['ollama']),
  179. }),
  180. tts: loadEnvSection(TTS_ENV_MAP, yamlData.tts),
  181. asr: loadEnvSection(ASR_ENV_MAP, yamlData.asr),
  182. pdf: loadEnvSection(PDF_ENV_MAP, yamlData.pdf, { requiresBaseUrl: true }),
  183. image: loadEnvSection(IMAGE_ENV_MAP, yamlData.image),
  184. video: loadEnvSection(VIDEO_ENV_MAP, yamlData.video),
  185. webSearch: loadEnvSection(WEB_SEARCH_ENV_MAP, yamlData['web-search']),
  186. };
  187. }
  188. function logConfig(config: ServerConfig, label: string): void {
  189. const counts = [
  190. Object.keys(config.providers).length,
  191. Object.keys(config.tts).length,
  192. Object.keys(config.asr).length,
  193. Object.keys(config.pdf).length,
  194. Object.keys(config.image).length,
  195. Object.keys(config.video).length,
  196. Object.keys(config.webSearch).length,
  197. ];
  198. if (counts.some((c) => c > 0)) {
  199. log.info(
  200. `[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`,
  201. );
  202. }
  203. }
  204. function getConfig(): ServerConfig {
  205. const cached = _configs.get('');
  206. if (cached) return cached;
  207. const yamlData = loadYamlFile(DEFAULT_FILENAME);
  208. const config = buildConfig(yamlData);
  209. logConfig(config, DEFAULT_FILENAME);
  210. _configs.set('', config);
  211. return config;
  212. }
  213. // ---------------------------------------------------------------------------
  214. // Public API — LLM
  215. // ---------------------------------------------------------------------------
  216. /** Returns server-configured LLM providers (no apiKeys) */
  217. export function getServerProviders(): Record<string, { models?: string[]; baseUrl?: string }> {
  218. const cfg = getConfig();
  219. const result: Record<string, { models?: string[]; baseUrl?: string }> = {};
  220. for (const [id, entry] of Object.entries(cfg.providers)) {
  221. result[id] = {};
  222. if (entry.models && entry.models.length > 0) result[id].models = entry.models;
  223. if (entry.baseUrl) result[id].baseUrl = entry.baseUrl;
  224. }
  225. return result;
  226. }
  227. /** Resolve API key: client key > server key > empty string */
  228. export function resolveApiKey(providerId: string, clientKey?: string): string {
  229. if (clientKey) return clientKey;
  230. return getConfig().providers[providerId]?.apiKey || '';
  231. }
  232. /** Resolve base URL: client > server > undefined */
  233. export function resolveBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined {
  234. if (clientBaseUrl) return clientBaseUrl;
  235. return getConfig().providers[providerId]?.baseUrl;
  236. }
  237. /** Resolve proxy URL for a provider (server config only) */
  238. export function resolveProxy(providerId: string): string | undefined {
  239. return getConfig().providers[providerId]?.proxy;
  240. }
  241. // ---------------------------------------------------------------------------
  242. // Public API — TTS
  243. // ---------------------------------------------------------------------------
  244. export function getServerTTSProviders(): Record<string, { baseUrl?: string }> {
  245. const cfg = getConfig();
  246. const result: Record<string, { baseUrl?: string }> = {};
  247. for (const [id, entry] of Object.entries(cfg.tts)) {
  248. result[id] = {};
  249. if (entry.baseUrl) result[id].baseUrl = entry.baseUrl;
  250. }
  251. return result;
  252. }
  253. export function resolveTTSApiKey(providerId: string, clientKey?: string): string {
  254. if (clientKey) return clientKey;
  255. return getConfig().tts[providerId]?.apiKey || '';
  256. }
  257. export function resolveTTSBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined {
  258. if (clientBaseUrl) return clientBaseUrl;
  259. return getConfig().tts[providerId]?.baseUrl;
  260. }
  261. // ---------------------------------------------------------------------------
  262. // Public API — ASR
  263. // ---------------------------------------------------------------------------
  264. export function getServerASRProviders(): Record<string, { baseUrl?: string }> {
  265. const cfg = getConfig();
  266. const result: Record<string, { baseUrl?: string }> = {};
  267. for (const [id, entry] of Object.entries(cfg.asr)) {
  268. result[id] = {};
  269. if (entry.baseUrl) result[id].baseUrl = entry.baseUrl;
  270. }
  271. return result;
  272. }
  273. export function resolveASRApiKey(providerId: string, clientKey?: string): string {
  274. if (clientKey) return clientKey;
  275. return getConfig().asr[providerId]?.apiKey || '';
  276. }
  277. export function resolveASRBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined {
  278. if (clientBaseUrl) return clientBaseUrl;
  279. return getConfig().asr[providerId]?.baseUrl;
  280. }
  281. // ---------------------------------------------------------------------------
  282. // Public API — PDF
  283. // ---------------------------------------------------------------------------
  284. export function getServerPDFProviders(): Record<string, { baseUrl?: string }> {
  285. const cfg = getConfig();
  286. const result: Record<string, { baseUrl?: string }> = {};
  287. for (const [id, entry] of Object.entries(cfg.pdf)) {
  288. result[id] = {};
  289. if (entry.baseUrl) result[id].baseUrl = entry.baseUrl;
  290. }
  291. return result;
  292. }
  293. export function resolvePDFApiKey(providerId: string, clientKey?: string): string {
  294. if (clientKey) return clientKey;
  295. return getConfig().pdf[providerId]?.apiKey || '';
  296. }
  297. export function resolvePDFBaseUrl(providerId: string, clientBaseUrl?: string): string | undefined {
  298. if (clientBaseUrl) return clientBaseUrl;
  299. return getConfig().pdf[providerId]?.baseUrl;
  300. }
  301. // ---------------------------------------------------------------------------
  302. // Public API — Image Generation
  303. // ---------------------------------------------------------------------------
  304. export function getServerImageProviders(): Record<string, Record<string, never>> {
  305. const cfg = getConfig();
  306. const result: Record<string, Record<string, never>> = {};
  307. for (const id of Object.keys(cfg.image)) {
  308. result[id] = {};
  309. }
  310. return result;
  311. }
  312. export function resolveImageApiKey(providerId: string, clientKey?: string): string {
  313. if (clientKey) return clientKey;
  314. return getConfig().image[providerId]?.apiKey || '';
  315. }
  316. export function resolveImageBaseUrl(
  317. providerId: string,
  318. clientBaseUrl?: string,
  319. ): string | undefined {
  320. if (clientBaseUrl) return clientBaseUrl;
  321. return getConfig().image[providerId]?.baseUrl;
  322. }
  323. // ---------------------------------------------------------------------------
  324. // Public API — Video Generation
  325. // ---------------------------------------------------------------------------
  326. export function getServerVideoProviders(): Record<string, Record<string, never>> {
  327. const cfg = getConfig();
  328. const result: Record<string, Record<string, never>> = {};
  329. for (const id of Object.keys(cfg.video)) {
  330. result[id] = {};
  331. }
  332. return result;
  333. }
  334. export function resolveVideoApiKey(providerId: string, clientKey?: string): string {
  335. if (clientKey) return clientKey;
  336. return getConfig().video[providerId]?.apiKey || '';
  337. }
  338. export function resolveVideoBaseUrl(
  339. providerId: string,
  340. clientBaseUrl?: string,
  341. ): string | undefined {
  342. if (clientBaseUrl) return clientBaseUrl;
  343. return getConfig().video[providerId]?.baseUrl;
  344. }
  345. // ---------------------------------------------------------------------------
  346. // Public API — Web Search (Tavily)
  347. // ---------------------------------------------------------------------------
  348. /** Returns server-configured web search providers (no apiKeys exposed) */
  349. export function getServerWebSearchProviders(): Record<string, { baseUrl?: string }> {
  350. const cfg = getConfig();
  351. const result: Record<string, { baseUrl?: string }> = {};
  352. for (const [id, entry] of Object.entries(cfg.webSearch)) {
  353. result[id] = {};
  354. if (entry.baseUrl) result[id].baseUrl = entry.baseUrl;
  355. }
  356. return result;
  357. }
  358. /** Resolve Tavily API key: client key > server key > TAVILY_API_KEY env > empty */
  359. export function resolveWebSearchApiKey(clientKey?: string): string {
  360. if (clientKey) return clientKey;
  361. const serverKey = getConfig().webSearch.tavily?.apiKey;
  362. if (serverKey) return serverKey;
  363. return process.env.TAVILY_API_KEY || '';
  364. }