cc / src /services /geminiRelayService.js
hequ's picture
Upload 224 files
6c6056a verified
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
}