const axios = require('axios') const ProxyHelper = require('../utils/proxyHelper') const logger = require('../utils/logger') const config = require('../../config/config') const apiKeyService = require('./apiKeyService') // Gemini API 配置 const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1' const DEFAULT_MODEL = 'models/gemini-2.0-flash-exp' // 创建代理 agent(使用统一的代理工具) function createProxyAgent(proxyConfig) { return ProxyHelper.createProxyAgent(proxyConfig) } // 转换 OpenAI 消息格式到 Gemini 格式 function convertMessagesToGemini(messages) { const contents = [] let systemInstruction = '' for (const message of messages) { if (message.role === 'system') { systemInstruction += (systemInstruction ? '\n\n' : '') + message.content } else if (message.role === 'user') { contents.push({ role: 'user', parts: [{ text: message.content }] }) } else if (message.role === 'assistant') { contents.push({ role: 'model', parts: [{ text: message.content }] }) } } return { contents, systemInstruction } } // 转换 Gemini 响应到 OpenAI 格式 function convertGeminiResponse(geminiResponse, model, stream = false) { if (stream) { // 流式响应 const candidate = geminiResponse.candidates?.[0] if (!candidate) { return null } const content = candidate.content?.parts?.[0]?.text || '' const finishReason = candidate.finishReason?.toLowerCase() return { id: `chatcmpl-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, delta: { content }, finish_reason: finishReason === 'stop' ? 'stop' : null } ] } } else { // 非流式响应 const candidate = geminiResponse.candidates?.[0] if (!candidate) { throw new Error('No response from Gemini') } const content = candidate.content?.parts?.[0]?.text || '' const finishReason = candidate.finishReason?.toLowerCase() || 'stop' // 计算 token 使用量 const usage = geminiResponse.usageMetadata || { promptTokenCount: 0, candidatesTokenCount: 0, totalTokenCount: 0 } return { id: `chatcmpl-${Date.now()}`, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, message: { role: 'assistant', content }, finish_reason: finishReason } ], usage: { prompt_tokens: usage.promptTokenCount, completion_tokens: usage.candidatesTokenCount, total_tokens: usage.totalTokenCount } } } } // 处理流式响应 async function* handleStreamResponse(response, model, apiKeyId, accountId = null) { let buffer = '' let totalUsage = { promptTokenCount: 0, candidatesTokenCount: 0, totalTokenCount: 0 } try { for await (const chunk of response.data) { buffer += chunk.toString() // 处理 SSE 格式的数据 const lines = buffer.split('\n') buffer = lines.pop() || '' // 保留最后一个不完整的行 for (const line of lines) { if (!line.trim()) { continue } // 处理 SSE 格式: "data: {...}" let jsonData = line if (line.startsWith('data: ')) { jsonData = line.substring(6).trim() } if (!jsonData || jsonData === '[DONE]') { continue } try { const data = JSON.parse(jsonData) // 更新使用量统计 if (data.usageMetadata) { totalUsage = data.usageMetadata } // 转换并发送响应 const openaiResponse = convertGeminiResponse(data, model, true) if (openaiResponse) { yield `data: ${JSON.stringify(openaiResponse)}\n\n` } // 检查是否结束 if (data.candidates?.[0]?.finishReason === 'STOP') { // 记录使用量 if (apiKeyId && totalUsage.totalTokenCount > 0) { await apiKeyService .recordUsage( apiKeyId, totalUsage.promptTokenCount || 0, // inputTokens totalUsage.candidatesTokenCount || 0, // outputTokens 0, // cacheCreateTokens (Gemini 没有这个概念) 0, // cacheReadTokens (Gemini 没有这个概念) model, accountId ) .catch((error) => { logger.error('❌ Failed to record Gemini usage:', error) }) } yield 'data: [DONE]\n\n' return } } catch (e) { logger.debug('Error parsing JSON line:', e.message, 'Line:', jsonData) } } } // 处理剩余的 buffer if (buffer.trim()) { try { let jsonData = buffer.trim() if (jsonData.startsWith('data: ')) { jsonData = jsonData.substring(6).trim() } if (jsonData && jsonData !== '[DONE]') { const data = JSON.parse(jsonData) const openaiResponse = convertGeminiResponse(data, model, true) if (openaiResponse) { yield `data: ${JSON.stringify(openaiResponse)}\n\n` } } } catch (e) { logger.debug('Error parsing final buffer:', e.message) } } yield 'data: [DONE]\n\n' } catch (error) { // 检查是否是请求被中止 if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') { logger.info('Stream request was aborted by client') } else { logger.error('Stream processing error:', error) yield `data: ${JSON.stringify({ error: { message: error.message, type: 'stream_error' } })}\n\n` } } } // 发送请求到 Gemini async function sendGeminiRequest({ messages, model = DEFAULT_MODEL, temperature = 0.7, maxTokens = 4096, stream = false, accessToken, proxy, apiKeyId, signal, projectId, location = 'us-central1', accountId = null }) { // 确保模型名称格式正确 if (!model.startsWith('models/')) { model = `models/${model}` } // 转换消息格式 const { contents, systemInstruction } = convertMessagesToGemini(messages) // 构建请求体 const requestBody = { contents, generationConfig: { temperature, maxOutputTokens: maxTokens, candidateCount: 1 } } if (systemInstruction) { requestBody.systemInstruction = { parts: [{ text: systemInstruction }] } } // 配置请求选项 let apiUrl if (projectId) { // 使用项目特定的 URL 格式(Google Cloud/Workspace 账号) apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse` logger.debug(`Using project-specific URL with projectId: ${projectId}, location: ${location}`) } else { // 使用标准 URL 格式(个人 Google 账号) apiUrl = `${GEMINI_API_BASE}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse` logger.debug('Using standard URL without projectId') } const axiosConfig = { method: 'POST', url: apiUrl, headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, data: requestBody, timeout: config.requestTimeout || 600000 } // 添加代理配置 const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info(`🌐 Using proxy for Gemini API request: ${ProxyHelper.getProxyDescription(proxy)}`) } else { logger.debug('🌐 No proxy configured for Gemini API request') } // 添加 AbortController 信号支持 if (signal) { axiosConfig.signal = signal logger.debug('AbortController signal attached to request') } if (stream) { axiosConfig.responseType = 'stream' } try { logger.debug('Sending request to Gemini API') const response = await axios(axiosConfig) if (stream) { return handleStreamResponse(response, model, apiKeyId, accountId) } else { // 非流式响应 const openaiResponse = convertGeminiResponse(response.data, model, false) // 记录使用量 if (apiKeyId && openaiResponse.usage) { await apiKeyService .recordUsage( apiKeyId, openaiResponse.usage.prompt_tokens || 0, openaiResponse.usage.completion_tokens || 0, 0, // cacheCreateTokens 0, // cacheReadTokens model, accountId ) .catch((error) => { logger.error('❌ Failed to record Gemini usage:', error) }) } return openaiResponse } } catch (error) { // 检查是否是请求被中止 if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') { logger.info('Gemini request was aborted by client') const err = new Error('Request canceled by client') err.status = 499 err.error = { message: 'Request canceled by client', type: 'canceled', code: 'request_canceled' } throw err } logger.error('Gemini API request failed:', error.response?.data || error.message) // 转换错误格式 if (error.response) { const geminiError = error.response.data?.error const err = new Error(geminiError?.message || 'Gemini API request failed') err.status = error.response.status err.error = { message: geminiError?.message || 'Gemini API request failed', type: geminiError?.code || 'api_error', code: geminiError?.code } throw err } const err = new Error(error.message) err.status = 500 err.error = { message: error.message, type: 'network_error' } throw err } } // 获取可用模型列表 async function getAvailableModels(accessToken, proxy, projectId, location = 'us-central1') { let apiUrl if (projectId) { // 使用项目特定的 URL 格式 apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/models` logger.debug(`Fetching models with projectId: ${projectId}, location: ${location}`) } else { // 使用标准 URL 格式 apiUrl = `${GEMINI_API_BASE}/models` logger.debug('Fetching models without projectId') } const axiosConfig = { method: 'GET', url: apiUrl, headers: { Authorization: `Bearer ${accessToken}` }, timeout: config.requestTimeout || 600000 } const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini models request: ${ProxyHelper.getProxyDescription(proxy)}` ) } else { logger.debug('🌐 No proxy configured for Gemini models request') } try { const response = await axios(axiosConfig) const models = response.data.models || [] // 转换为 OpenAI 格式 return models .filter((model) => model.supportedGenerationMethods?.includes('generateContent')) .map((model) => ({ id: model.name.replace('models/', ''), object: 'model', created: Date.now() / 1000, owned_by: 'google' })) } catch (error) { logger.error('Failed to get Gemini models:', error) // 返回默认模型列表 return [ { id: 'gemini-2.0-flash-exp', object: 'model', created: Date.now() / 1000, owned_by: 'google' } ] } } // Count Tokens API - 用于Gemini CLI兼容性 async function countTokens({ model, content, accessToken, proxy, projectId, location = 'us-central1' }) { // 确保模型名称格式正确 if (!model.startsWith('models/')) { model = `models/${model}` } // 转换内容格式 - 支持多种输入格式 let requestBody if (Array.isArray(content)) { // 如果content是数组,直接使用 requestBody = { contents: content } } else if (typeof content === 'string') { // 如果是字符串,转换为Gemini格式 requestBody = { contents: [ { parts: [{ text: content }] } ] } } else if (content.parts || content.role) { // 如果已经是Gemini格式的单个content requestBody = { contents: [content] } } else { // 其他情况,尝试直接使用 requestBody = { contents: content } } // 构建API URL - countTokens需要使用generativelanguage API const GENERATIVE_API_BASE = 'https://generativelanguage.googleapis.com/v1beta' let apiUrl if (projectId) { // 使用项目特定的 URL 格式(Google Cloud/Workspace 账号) apiUrl = `${GENERATIVE_API_BASE}/projects/${projectId}/locations/${location}/${model}:countTokens` logger.debug( `Using project-specific countTokens URL with projectId: ${projectId}, location: ${location}` ) } else { // 使用标准 URL 格式(个人 Google 账号) apiUrl = `${GENERATIVE_API_BASE}/${model}:countTokens` logger.debug('Using standard countTokens URL without projectId') } const axiosConfig = { method: 'POST', url: apiUrl, headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'X-Goog-User-Project': projectId || undefined }, data: requestBody, timeout: config.requestTimeout || 600000 } // 添加代理配置 const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini countTokens request: ${ProxyHelper.getProxyDescription(proxy)}` ) } else { logger.debug('🌐 No proxy configured for Gemini countTokens request') } try { logger.debug(`Sending countTokens request to: ${apiUrl}`) logger.debug(`Request body: ${JSON.stringify(requestBody, null, 2)}`) const response = await axios(axiosConfig) // 返回符合Gemini API格式的响应 return { totalTokens: response.data.totalTokens || 0, totalBillableCharacters: response.data.totalBillableCharacters || 0, ...response.data } } catch (error) { logger.error(`Gemini countTokens API request failed for URL: ${apiUrl}`) logger.error( 'Request config:', JSON.stringify( { url: apiUrl, headers: axiosConfig.headers, data: requestBody }, null, 2 ) ) logger.error('Error details:', error.response?.data || error.message) // 转换错误格式 if (error.response) { const geminiError = error.response.data?.error const errorObj = new Error( geminiError?.message || `Gemini countTokens API request failed (Status: ${error.response.status})` ) errorObj.status = error.response.status errorObj.error = { message: geminiError?.message || `Gemini countTokens API request failed (Status: ${error.response.status})`, type: geminiError?.code || 'api_error', code: geminiError?.code } throw errorObj } const errorObj = new Error(error.message) errorObj.status = 500 errorObj.error = { message: error.message, type: 'network_error' } throw errorObj } } module.exports = { sendGeminiRequest, getAvailableModels, convertMessagesToGemini, convertGeminiResponse, countTokens }