xiaobo ren
Fix API key handling: clean whitespace/newlines, update test models to use correct names
8602f0b
| /** | |
| * 多 API 配置和路由 | |
| * 根据模型名称自动选择正确的 API 端点和密钥 | |
| */ | |
| /** | |
| * 清理 API 密钥 - 移除换行符、空格等特殊字符 | |
| */ | |
| function cleanApiKey(key) { | |
| if (!key) return null; | |
| return String(key).trim().replace(/\r?\n/g, '').replace(/\s+/g, ''); | |
| } | |
| // API 配置 | |
| const API_CONFIGS = { | |
| // Google Gemini | |
| gemini: { | |
| baseUrl: 'https://generativelanguage.googleapis.com/v1', | |
| apiKey: cleanApiKey(process.env.GEMINI_API_KEY), | |
| models: [ | |
| 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', | |
| 'gemini-2.0-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash-lite', 'gemini-2.0-flash-lite-001', | |
| // 保留旧模型名称以兼容 | |
| 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro', 'gemini-3-pro-preview' | |
| ], | |
| format: 'gemini', // 使用 Gemini 原生格式 | |
| }, | |
| // OpenAI (ChatGPT) | |
| openai: { | |
| baseUrl: 'https://api.openai.com/v1', | |
| apiKey: cleanApiKey(process.env.OPENAI_API_KEY), | |
| models: [ | |
| 'gpt-5.1', 'gpt-5-mini', 'gpt-5-nano', | |
| 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', | |
| 'gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo' // 保留旧模型以兼容 | |
| ], | |
| format: 'openai', // 使用 OpenAI 格式 | |
| }, | |
| // Grok (xAI) | |
| grok: { | |
| baseUrl: 'https://api.x.ai/v1', | |
| apiKey: cleanApiKey(process.env.GROK_API_KEY), | |
| models: [ | |
| 'grok-4-0709', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning', | |
| 'grok-4-fast-reasoning', 'grok-4-fast-non-reasoning', | |
| 'grok-3', 'grok-3-mini', 'grok-code-fast-1', | |
| 'grok-4', 'grok-4-auto', 'grok-beta' // 保留旧名称以兼容 | |
| ], | |
| format: 'openai', // Grok 使用 OpenAI 兼容格式 | |
| }, | |
| // Claude (Anthropic) | |
| claude: { | |
| baseUrl: 'https://api.anthropic.com/v1', | |
| apiKey: cleanApiKey(process.env.CLAUDE_API_KEY), | |
| models: [ | |
| 'claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-20250514', | |
| 'claude-opus-4-1', 'claude-opus-4', 'claude-sonnet-4', | |
| 'claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20240620', | |
| 'claude-3-opus-20240229', 'claude-3-5-haiku-20241022', | |
| 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307' | |
| ], | |
| format: 'claude', // 使用 Claude 格式 | |
| }, | |
| }; | |
| /** | |
| * 根据模型名称获取 API 配置 | |
| */ | |
| export function getApiConfigForModel(modelName) { | |
| // 检查每个 API 提供商的模型列表 | |
| for (const [provider, config] of Object.entries(API_CONFIGS)) { | |
| if (config.models.includes(modelName)) { | |
| return { ...config, provider }; | |
| } | |
| } | |
| // 默认使用 Gemini | |
| return { ...API_CONFIGS.gemini, provider: 'gemini' }; | |
| } | |
| /** | |
| * 构建 API 请求 | |
| */ | |
| export function buildApiRequest(modelName, systemPrompt, userPrompt, temperature = 0.7) { | |
| const config = getApiConfigForModel(modelName); | |
| if (config.format === 'gemini') { | |
| // Google Gemini 格式 - 使用 v1 API | |
| // v1 API 使用 models/{model}:generateContent 格式 | |
| if (!config.apiKey) { | |
| throw new Error('GEMINI_API_KEY not configured'); | |
| } | |
| const geminiModel = modelName.replace('-latest', ''); // 移除 -latest 后缀 | |
| return { | |
| url: `${config.baseUrl}/models/${geminiModel}:generateContent?key=${config.apiKey}`, | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: { | |
| contents: [{ | |
| parts: [{ | |
| text: `${systemPrompt}\n\n${userPrompt}` | |
| }] | |
| }], | |
| generationConfig: { | |
| temperature: temperature, | |
| } | |
| } | |
| }; | |
| } else if (config.format === 'claude') { | |
| // Claude 格式 | |
| if (!config.apiKey) { | |
| throw new Error('CLAUDE_API_KEY not configured'); | |
| } | |
| return { | |
| url: `${config.baseUrl}/messages`, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': config.apiKey, | |
| 'anthropic-version': '2023-06-01', | |
| }, | |
| body: { | |
| model: modelName, | |
| max_tokens: 8192, | |
| temperature: temperature, | |
| messages: [ | |
| { role: 'user', content: `${systemPrompt}\n\n${userPrompt}` } | |
| ] | |
| } | |
| }; | |
| } else { | |
| // OpenAI 兼容格式 (OpenAI, Grok) | |
| // GPT-5 系列模型只支持 temperature=1(默认值) | |
| const isGPT5 = modelName.startsWith('gpt-5'); | |
| const requestBody = { | |
| model: modelName, | |
| messages: [ | |
| { role: 'system', content: systemPrompt }, | |
| { role: 'user', content: userPrompt }, | |
| ], | |
| }; | |
| // 只有非 GPT-5 模型才设置 temperature | |
| if (!isGPT5) { | |
| requestBody.temperature = temperature; | |
| } | |
| // 确保 API 密钥存在 | |
| if (!config.apiKey) { | |
| throw new Error(`API key not configured for ${config.provider || 'unknown provider'}`); | |
| } | |
| return { | |
| url: `${config.baseUrl}/chat/completions`, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${config.apiKey}`, | |
| }, | |
| body: requestBody | |
| }; | |
| } | |
| } | |
| /** | |
| * 解析 API 响应 | |
| */ | |
| export function parseApiResponse(responseJson, modelName) { | |
| const config = getApiConfigForModel(modelName); | |
| if (config.format === 'gemini') { | |
| // Google Gemini 响应格式 | |
| return responseJson.candidates?.[0]?.content?.parts?.[0]?.text || null; | |
| } else if (config.format === 'claude') { | |
| // Claude 响应格式 | |
| return responseJson.content?.[0]?.text || null; | |
| } else { | |
| // OpenAI 兼容格式 | |
| return responseJson.choices?.[0]?.message?.content || null; | |
| } | |
| } | |
| export default API_CONFIGS; | |