Files

1 line
14 KiB
JSON

{"repo": "18000520222/laughing-train", "files": {"erdi-crm-upload/lib/llm.ts": {"0dc2ef4462cb060a004280fe3921b52d77b7c406": {"t": 1785745604.7546997, "n": 4219, "h": "2039bacfd99e5e3d", "c": "// lib/llm.ts — 统一 LLM 调用层(多 provider 自动选择 + 降级)\n//\n// 设计目标:整套自动化(翻译 + AI 回复)只依赖这一个入口。\n// 通过环境变量决定用哪家,优先级:OPENAI > DEEPSEEK > GEMINI。\n// 任一 key 都没有时,isLLMAvailable() 返回 false,上层据此降级\n// (例如翻译降级回 LibreTranslate,自动回复直接跳过)。\n//\n// 三家都兼容 OpenAI Chat Completions 风格(Gemini 走其 OpenAI 兼容端点),\n// 所以只用一套调用代码。\n\nexport type LLMProvider = 'openai' | 'deepseek' | 'gemini';\n\ninterface ProviderConfig {\n provider: LLMProvider;\n apiKey: string;\n baseUrl: string;\n model: string;\n}\n\n/**\n * 根据环境变量解析当前可用的所有 provider 列表,支持按优先级 fallback。\n */\nexport function resolveProviders(): ProviderConfig[] {\n const forced = (process.env.LLM_PROVIDER || '').toLowerCase() as LLMProvider | '';\n\n const openaiKey = process.env.OPENAI_API_KEY;\n const deepseekKey = process.env.DEEPSEEK_API_KEY;\n const geminiKey = process.env.GEMINI_API_KEY;\n\n const build = (p: LLMProvider): ProviderConfig | null => {\n switch (p) {\n case 'openai':\n if (!openaiKey) return null;\n return {\n provider: 'openai',\n apiKey: openaiKey,\n baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',\n model: process.env.OPENAI_MODEL || 'gpt-4o-mini',\n };\n case 'deepseek':\n if (!deepseekKey) return null;\n return {\n provider: 'deepseek',\n apiKey: deepseekKey,\n baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',\n model: process.env.DEEPSEEK_MODEL || 'deepseek-chat',\n };\n case 'gemini':\n if (!geminiKey) return null;\n return {\n provider: 'gemini',\n apiKey: geminiKey,\n baseUrl:\n process.env.GEMINI_BASE_URL ||\n 'https://generativelanguage.googleapis.com/v1beta/openai',\n model: process.env.GEMINI_MODEL || 'gemini-2.0-flash',\n };\n }\n };\n\n if (forced) {\n const c = build(forced);\n return c ? [c] : [];\n }\n\n const list: ProviderConfig[] = [];\n const order: LLMProvider[] = ['openai', 'deepseek', 'gemini'];\n for (const p of order) {\n const c = build(p);\n if (c) list.push(c);\n }\n return list;\n}\n\n/**\n * 根据环境变量解析当前可用的最优先 provider。返回 null 表示没有任何可用 key。\n */\nexport function resolveProvider(): ProviderConfig | null {\n const list = resolveProviders();\n return list.length > 0 ? list[0] : null;\n}\n\nexport function isLLMAvailable(): boolean {\n return resolveProviders().length > 0;\n}\n\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface ChatOptions {\n temperature?: number;\n maxTokens?: number;\n /** 强制要求模型输出 JSON(用于结构化结果,如意图识别) */\n json?: boolean;\n timeoutMs?: number;\n}\n\n/**\n * 统一聊天补全,支持自动在可用 provider 列表中降级/重试,保障 100% 可用性。\n */\nexport async function chat(messages: ChatMessage[], opts: ChatOptions = {}): Promise<string> {\n const providers = resolveProviders();\n if (providers.length === 0) {\n throw new Error('NO_LLM_PROVIDER');\n }\n\n let lastError: any = null;\n\n for (const cfg of providers) {\n try {\n const body: Record<string, unknown> = {\n model: cfg.model,\n messages,\n temperature: opts.temperature ?? 0.3,\n };\n if (opts.maxTokens) body.max_tokens = opts.maxTokens;\n if (opts.json) body.response_format = { type: 'json_object' };\n\n const res = await fetch(`${cfg.baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(opts.timeoutMs ?? 20000),\n });\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n throw new Error(`LLM_HTTP_${res.status}: ${errText.slice(0, 300)}`);\n }\n\n const data = await res.json();\n const content = data?.choices?.[0]?.message?.content;\n if (typeof content !== 'string') {\n throw new Error('LLM_EMPTY_RESPONSE');\n }\n return content.trim();\n } catch (err: any) {\n console.warn(`[LLM] Provider ${cfg.provider} failed, trying next... Error:`, err?.message || err);\n lastError = err;\n }\n }\n\n throw lastError || new Error('All LLM providers failed');\n}\n"}, "6c77edd77b43362bc071cb1440c4b6e8c4868880": {"t": 1785754708.36356, "n": 4219, "h": "2039bacfd99e5e3d", "c": "// lib/llm.ts — 统一 LLM 调用层(多 provider 自动选择 + 降级)\n//\n// 设计目标:整套自动化(翻译 + AI 回复)只依赖这一个入口。\n// 通过环境变量决定用哪家,优先级:OPENAI > DEEPSEEK > GEMINI。\n// 任一 key 都没有时,isLLMAvailable() 返回 false,上层据此降级\n// (例如翻译降级回 LibreTranslate,自动回复直接跳过)。\n//\n// 三家都兼容 OpenAI Chat Completions 风格(Gemini 走其 OpenAI 兼容端点),\n// 所以只用一套调用代码。\n\nexport type LLMProvider = 'openai' | 'deepseek' | 'gemini';\n\ninterface ProviderConfig {\n provider: LLMProvider;\n apiKey: string;\n baseUrl: string;\n model: string;\n}\n\n/**\n * 根据环境变量解析当前可用的所有 provider 列表,支持按优先级 fallback。\n */\nexport function resolveProviders(): ProviderConfig[] {\n const forced = (process.env.LLM_PROVIDER || '').toLowerCase() as LLMProvider | '';\n\n const openaiKey = process.env.OPENAI_API_KEY;\n const deepseekKey = process.env.DEEPSEEK_API_KEY;\n const geminiKey = process.env.GEMINI_API_KEY;\n\n const build = (p: LLMProvider): ProviderConfig | null => {\n switch (p) {\n case 'openai':\n if (!openaiKey) return null;\n return {\n provider: 'openai',\n apiKey: openaiKey,\n baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',\n model: process.env.OPENAI_MODEL || 'gpt-4o-mini',\n };\n case 'deepseek':\n if (!deepseekKey) return null;\n return {\n provider: 'deepseek',\n apiKey: deepseekKey,\n baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',\n model: process.env.DEEPSEEK_MODEL || 'deepseek-chat',\n };\n case 'gemini':\n if (!geminiKey) return null;\n return {\n provider: 'gemini',\n apiKey: geminiKey,\n baseUrl:\n process.env.GEMINI_BASE_URL ||\n 'https://generativelanguage.googleapis.com/v1beta/openai',\n model: process.env.GEMINI_MODEL || 'gemini-2.0-flash',\n };\n }\n };\n\n if (forced) {\n const c = build(forced);\n return c ? [c] : [];\n }\n\n const list: ProviderConfig[] = [];\n const order: LLMProvider[] = ['openai', 'deepseek', 'gemini'];\n for (const p of order) {\n const c = build(p);\n if (c) list.push(c);\n }\n return list;\n}\n\n/**\n * 根据环境变量解析当前可用的最优先 provider。返回 null 表示没有任何可用 key。\n */\nexport function resolveProvider(): ProviderConfig | null {\n const list = resolveProviders();\n return list.length > 0 ? list[0] : null;\n}\n\nexport function isLLMAvailable(): boolean {\n return resolveProviders().length > 0;\n}\n\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface ChatOptions {\n temperature?: number;\n maxTokens?: number;\n /** 强制要求模型输出 JSON(用于结构化结果,如意图识别) */\n json?: boolean;\n timeoutMs?: number;\n}\n\n/**\n * 统一聊天补全,支持自动在可用 provider 列表中降级/重试,保障 100% 可用性。\n */\nexport async function chat(messages: ChatMessage[], opts: ChatOptions = {}): Promise<string> {\n const providers = resolveProviders();\n if (providers.length === 0) {\n throw new Error('NO_LLM_PROVIDER');\n }\n\n let lastError: any = null;\n\n for (const cfg of providers) {\n try {\n const body: Record<string, unknown> = {\n model: cfg.model,\n messages,\n temperature: opts.temperature ?? 0.3,\n };\n if (opts.maxTokens) body.max_tokens = opts.maxTokens;\n if (opts.json) body.response_format = { type: 'json_object' };\n\n const res = await fetch(`${cfg.baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(opts.timeoutMs ?? 20000),\n });\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n throw new Error(`LLM_HTTP_${res.status}: ${errText.slice(0, 300)}`);\n }\n\n const data = await res.json();\n const content = data?.choices?.[0]?.message?.content;\n if (typeof content !== 'string') {\n throw new Error('LLM_EMPTY_RESPONSE');\n }\n return content.trim();\n } catch (err: any) {\n console.warn(`[LLM] Provider ${cfg.provider} failed, trying next... Error:`, err?.message || err);\n lastError = err;\n }\n }\n\n throw lastError || new Error('All LLM providers failed');\n}\n"}, "bf0cc815c17db53f8b8c26efcaa10736512bab24": {"t": 1785754709.7857533, "n": 3489, "h": "65fd34ffe069cc6d", "c": "// lib/llm.ts — 统一 LLM 调用层(多 provider 自动选择 + 降级)\n//\n// 设计目标:整套自动化(翻译 + AI 回复)只依赖这一个入口。\n// 通过环境变量决定用哪家,优先级:OPENAI > DEEPSEEK > GEMINI。\n// 任一 key 都没有时,isLLMAvailable() 返回 false,上层据此降级\n// (例如翻译降级回 LibreTranslate,自动回复直接跳过)。\n//\n// 三家都兼容 OpenAI Chat Completions 风格(Gemini 走其 OpenAI 兼容端点),\n// 所以只用一套调用代码。\n\nexport type LLMProvider = 'openai' | 'deepseek' | 'gemini';\n\ninterface ProviderConfig {\n provider: LLMProvider;\n apiKey: string;\n baseUrl: string;\n model: string;\n}\n\n/**\n * 根据环境变量解析当前可用的 provider。返回 null 表示没有任何可用 key。\n * 可用 LLM_PROVIDER 环境变量强制指定(openai/deepseek/gemini)。\n */\nexport function resolveProvider(): ProviderConfig | null {\n const forced = (process.env.LLM_PROVIDER || '').toLowerCase() as LLMProvider | '';\n\n const openaiKey = process.env.OPENAI_API_KEY;\n const deepseekKey = process.env.DEEPSEEK_API_KEY;\n const geminiKey = process.env.GEMINI_API_KEY;\n\n const build = (p: LLMProvider): ProviderConfig | null => {\n switch (p) {\n case 'openai':\n if (!openaiKey) return null;\n return {\n provider: 'openai',\n apiKey: openaiKey,\n baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',\n model: process.env.OPENAI_MODEL || 'gpt-4o-mini',\n };\n case 'deepseek':\n if (!deepseekKey) return null;\n return {\n provider: 'deepseek',\n apiKey: deepseekKey,\n baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',\n model: process.env.DEEPSEEK_MODEL || 'deepseek-chat',\n };\n case 'gemini':\n if (!geminiKey) return null;\n return {\n provider: 'gemini',\n apiKey: geminiKey,\n baseUrl:\n process.env.GEMINI_BASE_URL ||\n 'https://generativelanguage.googleapis.com/v1beta/openai',\n model: process.env.GEMINI_MODEL || 'gemini-2.0-flash',\n };\n }\n };\n\n if (forced) {\n const c = build(forced);\n if (c) return c;\n }\n return build('openai') || build('deepseek') || build('gemini');\n}\n\nexport function isLLMAvailable(): boolean {\n return resolveProvider() !== null;\n}\n\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface ChatOptions {\n temperature?: number;\n maxTokens?: number;\n /** 强制要求模型输出 JSON(用于结构化结果,如意图识别) */\n json?: boolean;\n timeoutMs?: number;\n}\n\n/**\n * 统一聊天补全。失败抛错由调用方决定降级策略。\n */\nexport async function chat(messages: ChatMessage[], opts: ChatOptions = {}): Promise<string> {\n const cfg = resolveProvider();\n if (!cfg) {\n throw new Error('NO_LLM_PROVIDER');\n }\n\n const body: Record<string, unknown> = {\n model: cfg.model,\n messages,\n temperature: opts.temperature ?? 0.3,\n };\n if (opts.maxTokens) body.max_tokens = opts.maxTokens;\n if (opts.json) body.response_format = { type: 'json_object' };\n\n const res = await fetch(`${cfg.baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(opts.timeoutMs ?? 20000),\n });\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n throw new Error(`LLM_HTTP_${res.status}: ${errText.slice(0, 300)}`);\n }\n\n const data = await res.json();\n const content = data?.choices?.[0]?.message?.content;\n if (typeof content !== 'string') {\n throw new Error('LLM_EMPTY_RESPONSE');\n }\n return content.trim();\n}\n"}}}}