| import express from 'express'; |
| import fetch from 'node-fetch'; |
| import dotenv from 'dotenv'; |
| import { v4 as uuidv4 } from 'uuid'; |
| import cors from 'cors'; |
|
|
| |
| dotenv.config(); |
|
|
| |
| |
| |
| class Config { |
| constructor() { |
| this.initializeApiKeys(); |
| this.initializeAuth(); |
| |
| |
| this.usedApiKeys = []; |
| |
| |
| this.invalidApiKeys = []; |
| |
| |
| this.geminiSafety = [ |
| { |
| category: 'HARM_CATEGORY_HARASSMENT', |
| threshold: 'OFF', |
| }, |
| { |
| category: 'HARM_CATEGORY_HATE_SPEECH', |
| threshold: 'OFF', |
| }, |
| { |
| category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', |
| threshold: 'OFF', |
| }, |
| { |
| category: 'HARM_CATEGORY_DANGEROUS_CONTENT', |
| threshold: 'OFF', |
| }, |
| { |
| category: 'HARM_CATEGORY_CIVIC_INTEGRITY', |
| threshold: 'OFF', |
| }, |
| ]; |
| } |
| |
| |
| |
| |
| initializeApiKeys() { |
| const apiKeysEnv = process.env.GEMINI_API_KEYS; |
| if (!apiKeysEnv) { |
| console.error('❌ 错误: 未找到 GEMINI_API_KEYS 环境变量'); |
| process.exit(1); |
| } |
| |
| |
| this.apiKeys = apiKeysEnv |
| .split('\n') |
| .map(key => key.trim()) |
| .filter(key => key.length > 0); |
| |
| if (this.apiKeys.length === 0) { |
| console.error('❌ 错误: 没有找到有效的API Keys'); |
| process.exit(1); |
| } |
| |
| console.log(`✅ 成功加载 ${this.apiKeys.length} 个API Keys`); |
| } |
| |
| |
| |
| |
| initializeAuth() { |
| this.authToken = process.env.AUTH_TOKEN || 'sk-123456'; |
| } |
| |
| |
| |
| |
| getApiKey() { |
| if (this.apiKeys.length === 0) { |
| if (this.usedApiKeys.length > 0) { |
| this.apiKeys.push(...this.usedApiKeys); |
| this.usedApiKeys = []; |
| } else { |
| return null; |
| } |
| } |
| |
| const apiKey = this.apiKeys.shift(); |
| this.usedApiKeys.push(apiKey); |
| return apiKey; |
| } |
| |
| |
| |
| |
| getFirstAvailableApiKey() { |
| if (this.apiKeys.length > 0) { |
| return this.apiKeys[0]; |
| } |
| if (this.usedApiKeys.length > 0) { |
| return this.usedApiKeys[0]; |
| } |
| return null; |
| } |
| |
| |
| |
| |
| markKeyAsInvalid(apiKey) { |
| const usedIndex = this.usedApiKeys.indexOf(apiKey); |
| if (usedIndex !== -1) { |
| this.usedApiKeys.splice(usedIndex, 1); |
| } |
| |
| const mainIndex = this.apiKeys.indexOf(apiKey); |
| if (mainIndex !== -1) { |
| this.apiKeys.splice(mainIndex, 1); |
| } |
| |
| if (!this.invalidApiKeys.includes(apiKey)) { |
| this.invalidApiKeys.push(apiKey); |
| } |
| |
| console.warn(`⚠️ API Key 已标记为失效: ${apiKey.substring(0, 10)}...`); |
| } |
| |
| |
| |
| |
| moveToUsed(apiKey) { |
| if (!this.usedApiKeys.includes(apiKey)) { |
| this.usedApiKeys.push(apiKey); |
| } |
| } |
| |
| |
| |
| |
| validateAuth(authHeader) { |
| if (!authHeader) { |
| return false; |
| } |
| |
| const token = authHeader.replace('Bearer ', ''); |
| return token === this.authToken; |
| } |
| } |
|
|
| |
| |
| |
| class ImageProcessor { |
| |
| |
| |
| static parseDataUrl(dataUrl) { |
| try { |
| |
| const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/); |
| if (!match) { |
| throw new Error('无效的data URL格式'); |
| } |
| |
| const mimeType = match[1]; |
| const base64Data = match[2]; |
| |
| |
| const supportedMimeTypes = [ |
| 'image/jpeg', |
| 'image/jpg', |
| 'image/png', |
| 'image/gif', |
| 'image/webp', |
| 'image/bmp', |
| 'image/tiff' |
| ]; |
| |
| if (!supportedMimeTypes.includes(mimeType.toLowerCase())) { |
| throw new Error(`不支持的图片格式: ${mimeType}`); |
| } |
| |
| return { |
| mimeType, |
| data: base64Data |
| }; |
| } catch (error) { |
| console.error('解析图片data URL错误:', error); |
| throw error; |
| } |
| } |
| |
| |
| |
| |
| static validateBase64(base64String) { |
| try { |
| |
| if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) { |
| return false; |
| } |
| |
| |
| return base64String.length % 4 === 0; |
| } catch (error) { |
| return false; |
| } |
| } |
| |
| |
| |
| |
| static async fetchImageAsBase64(imageUrl) { |
| try { |
| const response = await fetch(imageUrl, { |
| timeout: 30000, |
| headers: { |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
| } |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`获取图片失败: HTTP ${response.status}`); |
| } |
| |
| const contentType = response.headers.get('content-type'); |
| if (!contentType || !contentType.startsWith('image/')) { |
| throw new Error(`URL返回的不是图片类型: ${contentType}`); |
| } |
| |
| const buffer = await response.buffer(); |
| const base64Data = buffer.toString('base64'); |
| |
| return { |
| mimeType: contentType, |
| data: base64Data |
| }; |
| } catch (error) { |
| console.error('下载图片错误:', error); |
| throw error; |
| } |
| } |
| } |
|
|
| |
| |
| |
| class MessageConverter { |
| |
| |
| |
| static async convertMessages(openaiMessages) { |
| const geminiMessages = []; |
| let currentRole = null; |
| let currentParts = []; |
| |
| for (const message of openaiMessages) { |
| let role = message.role; |
| let content = message.content; |
| |
| |
| if (role === 'system') { |
| role = 'user'; |
| } |
| |
| if (role === 'assistant') { |
| role = 'model'; |
| } |
| |
| |
| let parts = []; |
| |
| if (typeof content === 'string') { |
| |
| parts = [{ text: content }]; |
| } else if (Array.isArray(content)) { |
| |
| parts = await this.convertContentArray(content); |
| } else { |
| |
| parts = [{ text: String(content) }]; |
| } |
| |
| |
| if (role === currentRole) { |
| currentParts.push(...parts); |
| } else { |
| |
| if (currentRole !== null && currentParts.length > 0) { |
| geminiMessages.push({ |
| role: currentRole, |
| parts: currentParts |
| }); |
| } |
| |
| |
| currentRole = role; |
| currentParts = [...parts]; |
| } |
| } |
| |
| |
| if (currentRole !== null && currentParts.length > 0) { |
| geminiMessages.push({ |
| role: currentRole, |
| parts: currentParts |
| }); |
| } |
| |
| return geminiMessages; |
| } |
| |
| |
| |
| |
| static async convertContentArray(contentArray) { |
| const parts = []; |
| |
| for (const item of contentArray) { |
| try { |
| if (item.type === 'text') { |
| |
| parts.push({ text: item.text || '' }); |
| } else if (item.type === 'image_url') { |
| |
| const imagePart = await this.convertImageContent(item); |
| if (imagePart) { |
| parts.push(imagePart); |
| } |
| } else { |
| |
| console.warn(`未知的内容类型: ${item.type},将转为文本处理`); |
| parts.push({ text: JSON.stringify(item) }); |
| } |
| } catch (error) { |
| console.error('转换内容项错误:', error); |
| |
| continue; |
| } |
| } |
| |
| return parts; |
| } |
| |
| |
| |
| |
| static async convertImageContent(imageItem) { |
| try { |
| const imageUrl = imageItem.image_url?.url; |
| if (!imageUrl) { |
| throw new Error('缺少图片URL'); |
| } |
| |
| let imageData; |
| |
| if (imageUrl.startsWith('data:')) { |
| |
| imageData = ImageProcessor.parseDataUrl(imageUrl); |
| |
| |
| if (!ImageProcessor.validateBase64(imageData.data)) { |
| throw new Error('无效的base64图片数据'); |
| } |
| } else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { |
| |
| imageData = await ImageProcessor.fetchImageAsBase64(imageUrl); |
| } else { |
| throw new Error(`不支持的图片URL格式: ${imageUrl}`); |
| } |
| |
| |
| return { |
| inlineData: { |
| mimeType: imageData.mimeType, |
| data: imageData.data |
| } |
| }; |
| } catch (error) { |
| console.error('转换图片内容错误:', error); |
| |
| return { text: `[图片处理失败: ${error.message}]` }; |
| } |
| } |
| |
| |
| |
| |
| static extractParams(openaiRequest) { |
| return { |
| model: openaiRequest.model || 'gemini-1.5-flash', |
| messages: openaiRequest.messages || [], |
| stream: openaiRequest.stream || false, |
| temperature: openaiRequest.temperature, |
| maxTokens: openaiRequest.max_tokens, |
| topP: openaiRequest.top_p |
| }; |
| } |
| } |
|
|
| |
| |
| |
| class ModelManager { |
| constructor(config) { |
| this.config = config; |
| this.cachedModels = null; |
| this.cacheExpiry = null; |
| this.cacheTimeout = 5 * 60 * 1000; |
| } |
| |
| |
| |
| |
| async getModels() { |
| if (this.cachedModels && this.cacheExpiry && Date.now() < this.cacheExpiry) { |
| return { success: true, data: this.cachedModels }; |
| } |
| |
| const apiKey = this.config.getFirstAvailableApiKey(); |
| if (!apiKey) { |
| return { |
| success: false, |
| error: '没有可用的API Key', |
| status: 503 |
| }; |
| } |
| |
| try { |
| const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, { |
| method: 'GET', |
| headers: { |
| 'Content-Type': 'application/json' |
| } |
| }); |
| |
| if (!response.ok) { |
| return { |
| success: false, |
| error: `获取模型列表失败: ${response.status}`, |
| status: response.status |
| }; |
| } |
| |
| const geminiResponse = await response.json(); |
| const filteredModels = this.filterModels(geminiResponse.models || []); |
| |
| this.cachedModels = filteredModels; |
| this.cacheExpiry = Date.now() + this.cacheTimeout; |
| |
| return { success: true, data: filteredModels }; |
| |
| } catch (error) { |
| console.error('获取模型列表错误:', error); |
| return { |
| success: false, |
| error: '网络请求失败', |
| status: 500 |
| }; |
| } |
| } |
| |
| |
| |
| |
| filterModels(models) { |
| const allowedPrefixes = [ |
| 'models/gemini-2.5-flash', |
| 'models/gemini-2.0-flash', |
| 'models/gemini-1.5-flash' |
| ]; |
| |
| const excludedModels = [ |
| 'models/gemini-1.5-flash-8b' |
| ]; |
| |
| const filteredModels = models.filter(model => { |
| const modelName = model.name; |
| |
| if (excludedModels.some(excluded => modelName.startsWith(excluded))) { |
| return false; |
| } |
| if(modelName == "models/gemini-2.5-pro"){ |
| return true; |
| } |
| |
| return allowedPrefixes.some(prefix => modelName.startsWith(prefix)); |
| }); |
| |
| |
| const processedModels = filteredModels.map(model => { |
| const modelId = model.name.replace('models/', ''); |
| |
| return { |
| id: modelId, |
| object: 'model', |
| created: Math.floor(Date.now() / 1000), |
| owned_by: 'google', |
| permission: [ |
| { |
| id: `modelperm-${modelId}`, |
| object: 'model_permission', |
| created: Math.floor(Date.now() / 1000), |
| allow_create_engine: false, |
| allow_sampling: true, |
| allow_logprobs: false, |
| allow_search_indices: false, |
| allow_view: true, |
| allow_fine_tuning: false, |
| organization: '*', |
| group: null, |
| is_blocking: false |
| } |
| ], |
| root: modelId, |
| parent: null |
| }; |
| }); |
| |
| return { |
| object: 'list', |
| data: processedModels |
| }; |
| } |
| } |
|
|
| |
| |
| |
| class GeminiRequestBuilder { |
| constructor(config) { |
| this.config = config; |
| } |
| |
| |
| |
| |
| buildRequestBody(geminiMessages, params) { |
| const requestBody = { |
| contents: geminiMessages, |
| safetySettings: this.config.geminiSafety, |
| generationConfig: {} |
| }; |
| |
| if (params.temperature !== undefined) { |
| requestBody.generationConfig.temperature = params.temperature; |
| } |
| |
| if (params.maxTokens !== undefined) { |
| requestBody.generationConfig.maxOutputTokens = params.maxTokens; |
| } |
| |
| if (params.topP !== undefined) { |
| requestBody.generationConfig.topP = params.topP; |
| } |
| |
| return requestBody; |
| } |
| |
| |
| |
| |
| buildApiUrl(model, apiKey, isStream = false) { |
| const method = isStream ? 'streamGenerateContent' : 'generateContent'; |
| return `https://generativelanguage.googleapis.com/v1beta/models/${model}:${method}?key=${apiKey}`; |
| } |
| } |
|
|
| |
| |
| |
| class ResponseConverter { |
| |
| |
| |
| static convertStreamChunk(geminiData, requestId, model) { |
| try { |
| if (geminiData.candidates && geminiData.candidates[0]) { |
| const candidate = geminiData.candidates[0]; |
| if (candidate.content && candidate.content.parts) { |
| const text = candidate.content.parts[0]?.text || ''; |
| const openaiChunk = { |
| id: requestId, |
| object: 'chat.completion.chunk', |
| created: Math.floor(Date.now() / 1000), |
| model: model, |
| choices: [{ |
| index: 0, |
| delta: { content: text }, |
| finish_reason: candidate.finishReason === 'STOP' ? 'stop' : null |
| }] |
| }; |
| return `data: ${JSON.stringify(openaiChunk)}\n\n`; |
| } |
| } |
| return ''; |
| } catch (error) { |
| console.error('转换流响应块错误:', error); |
| return ''; |
| } |
| } |
| |
| |
| |
| |
| static convertNormalResponse(geminiResponse, requestId, model) { |
| const openaiResponse = { |
| id: requestId, |
| object: 'chat.completion', |
| created: Math.floor(Date.now() / 1000), |
| model: model, |
| choices: [], |
| usage: { |
| prompt_tokens: 0, |
| completion_tokens: 0, |
| total_tokens: 0 |
| } |
| }; |
| |
| if (geminiResponse.candidates && geminiResponse.candidates[0]) { |
| const candidate = geminiResponse.candidates[0]; |
| if (candidate.content && candidate.content.parts) { |
| const text = candidate.content.parts.map(part => part.text).join(''); |
| openaiResponse.choices.push({ |
| index: 0, |
| message: { |
| role: 'assistant', |
| content: text |
| }, |
| finish_reason: candidate.finishReason === 'STOP' ? 'stop' : 'length' |
| }); |
| } |
| } |
| |
| |
| if (geminiResponse.usageMetadata) { |
| openaiResponse.usage = { |
| prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, |
| completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, |
| total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0 |
| }; |
| } |
| |
| return openaiResponse; |
| } |
| |
| |
| |
| |
| static splitTextToFakeStream(text, requestId, model) { |
| const chunks = []; |
| const chunkSize = 3; |
| |
| for (let i = 0; i < text.length; i += chunkSize) { |
| const chunk = text.slice(i, i + chunkSize); |
| const isLast = i + chunkSize >= text.length; |
| |
| const openaiChunk = { |
| id: requestId, |
| object: 'chat.completion.chunk', |
| created: Math.floor(Date.now() / 1000), |
| model: model, |
| choices: [{ |
| index: 0, |
| delta: { content: chunk }, |
| finish_reason: isLast ? 'stop' : null |
| }] |
| }; |
| |
| chunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`); |
| } |
| |
| return chunks; |
| } |
| } |
|
|
| |
| |
| |
| class GeminiRealtimeStreamParser { |
| constructor(response, onChunk) { |
| this.response = response; |
| this.onChunk = onChunk; |
| this.buffer = ''; |
| this.bufferLv = 0; |
| this.inString = false; |
| this.escapeNext = false; |
| this.decoder = new TextDecoder(); |
| } |
| |
| async start() { |
| try { |
| for await (const chunk of this.response.body) { |
| const text = this.decoder.decode(chunk, { stream: true }); |
| await this.processText(text); |
| } |
| |
| await this.handleRemainingBuffer(); |
| } catch (error) { |
| console.error('流式解析错误:', error); |
| throw error; |
| } |
| } |
| |
| async processText(text) { |
| for (const char of text) { |
| if (this.escapeNext) { |
| if (this.bufferLv > 1) { |
| this.buffer += char; |
| } |
| this.escapeNext = false; |
| continue; |
| } |
| |
| if (char === '\\' && this.inString) { |
| this.escapeNext = true; |
| if (this.bufferLv > 1) { |
| this.buffer += char; |
| } |
| continue; |
| } |
| |
| if (char === '"') { |
| this.inString = !this.inString; |
| } |
| |
| if (!this.inString) { |
| if (char === '{' || char === '[') { |
| this.bufferLv++; |
| } else if (char === '}' || char === ']') { |
| this.bufferLv--; |
| } |
| } |
| |
| if (this.bufferLv > 1) { |
| if (this.inString && char === '\n') { |
| this.buffer += '\\n'; |
| } else { |
| this.buffer += char; |
| } |
| } else if (this.bufferLv === 1 && this.buffer) { |
| this.buffer += '}'; |
| |
| try { |
| const bufferJson = JSON.parse(this.buffer); |
| await this.onChunk(bufferJson); |
| } catch (parseError) { |
| console.error('解析Gemini流数据错误:', parseError); |
| } |
| |
| this.buffer = ''; |
| } |
| } |
| } |
| |
| async handleRemainingBuffer() { |
| if (this.buffer.trim() && this.bufferLv >= 1) { |
| try { |
| if (!this.buffer.endsWith('}')) { |
| this.buffer += '}'; |
| } |
| const bufferJson = JSON.parse(this.buffer); |
| await this.onChunk(bufferJson); |
| } catch (parseError) { |
| console.error('解析最后的缓冲区数据错误:', parseError); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| class AuthMiddleware { |
| constructor(config) { |
| this.config = config; |
| } |
| |
| middleware() { |
| return (req, res, next) => { |
| |
| if (req.path === '/health' || req.method === 'OPTIONS') { |
| return next(); |
| } |
| |
| const authHeader = req.headers.authorization; |
| |
| if (!this.config.validateAuth(authHeader)) { |
| return res.status(401).json({ |
| error: { |
| message: 'Invalid authentication credentials', |
| type: 'invalid_request_error', |
| code: 'invalid_api_key' |
| } |
| }); |
| } |
| |
| next(); |
| }; |
| } |
| } |
|
|
| |
| |
| |
| class ApiProxyService { |
| constructor() { |
| this.config = new Config(); |
| this.requestBuilder = new GeminiRequestBuilder(this.config); |
| this.modelManager = new ModelManager(this.config); |
| this.authMiddleware = new AuthMiddleware(this.config); |
| } |
| |
| |
| |
| |
| async handleChatRequest(req, res) { |
| try { |
| const requestId = `chatcmpl-${uuidv4()}`; |
| |
| const params = MessageConverter.extractParams(req.body); |
| |
| |
| const geminiMessages = await MessageConverter.convertMessages(params.messages); |
| |
| if (!geminiMessages || geminiMessages.length === 0) { |
| return res.status(400).json({ |
| error: { |
| message: '无效的消息格式或消息为空', |
| type: 'invalid_request_error', |
| code: 'invalid_messages' |
| } |
| }); |
| } |
| |
| const requestBody = this.requestBuilder.buildRequestBody(geminiMessages, params); |
| |
| if (params.stream) { |
| const result = await this.handleStreamRequest(requestBody, params, requestId, res); |
| if (!result.success) { |
| res.status(result.status || 500).json({ error: result.error }); |
| } |
| } else { |
| const result = await this.executeNormalRequest(requestBody, params, requestId); |
| if (result.success) { |
| res.json(result.data); |
| } else { |
| res.status(result.status || 500).json({ error: result.error }); |
| } |
| } |
| } catch (error) { |
| console.error('处理聊天请求错误:', error); |
| res.status(500).json({ |
| error: { |
| message: '内部服务器错误: ' + error.message, |
| type: 'internal_server_error', |
| code: 'server_error' |
| } |
| }); |
| } |
| } |
| |
| |
| |
| |
| async handleFakeStreamChatRequest(req, res) { |
| try { |
| const requestId = `chatcmpl-${uuidv4()}`; |
| |
| const params = MessageConverter.extractParams(req.body); |
| |
| |
| const geminiMessages = await MessageConverter.convertMessages(params.messages); |
| |
| if (!geminiMessages || geminiMessages.length === 0) { |
| return res.status(400).json({ |
| error: { |
| message: '无效的消息格式或消息为空', |
| type: 'invalid_request_error', |
| code: 'invalid_messages' |
| } |
| }); |
| } |
| console.log("请求中") |
| const requestBody = this.requestBuilder.buildRequestBody(geminiMessages, params); |
| |
| if (params.stream) { |
| |
| const result = await this.handleFakeStreamRequest(requestBody, params, requestId, res); |
| if (!result.success) { |
| res.status(result.status || 500).json({ error: result.error }); |
| } |
| } else { |
| |
| const result = await this.executeNormalRequest(requestBody, params, requestId); |
| if (result.success) { |
| res.json(result.data); |
| } else { |
| res.status(result.status || 500).json({ error: result.error }); |
| } |
| } |
| } catch (error) { |
| console.error('处理假流式聊天请求错误:', error); |
| res.status(500).json({ |
| error: { |
| message: '内部服务器错误: ' + error.message, |
| type: 'internal_server_error', |
| code: 'server_error' |
| } |
| }); |
| } |
| } |
| |
| |
| |
| |
| async handleFakeStreamRequest(requestBody, params, requestId, res) { |
| try { |
| |
| res.writeHead(200, { |
| 'Content-Type': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive', |
| 'Access-Control-Allow-Origin': '*' |
| }); |
| |
| |
| const pingInterval = setInterval(() => { |
| try { |
| res.write(': ping\n\n'); |
| } catch (error) { |
| clearInterval(pingInterval); |
| } |
| }, 1000); |
| |
| |
| const result = await this.executeNormalRequest(requestBody, params, requestId); |
| |
| |
| clearInterval(pingInterval); |
| |
| if (!result.success) { |
| res.write(`data: ${JSON.stringify({ error: result.error })}\n\n`); |
| res.write('data: [DONE]\n\n'); |
| res.end(); |
| return { success: false }; |
| } |
| |
| |
| const responseText = result.data.choices[0]?.message?.content || ''; |
| |
| if (responseText) { |
| |
| const chunks = ResponseConverter.splitTextToFakeStream(responseText, requestId, params.model); |
| |
| |
| for (const chunk of chunks) { |
| res.write(chunk); |
| |
| await new Promise(resolve => setTimeout(resolve, 50)); |
| } |
| } |
| |
| res.write('data: [DONE]\n\n'); |
| res.end(); |
| |
| return { success: true }; |
| |
| } catch (error) { |
| console.error('处理假流式请求错误:', error); |
| try { |
| res.write(`data: ${JSON.stringify({ error: '内部服务器错误: ' + error.message })}\n\n`); |
| res.write('data: [DONE]\n\n'); |
| res.end(); |
| } catch (writeError) { |
| console.error('写入错误响应失败:', writeError); |
| } |
| return { success: false, error: error.message }; |
| } |
| } |
| |
| |
| |
| |
| async handleStreamRequest(requestBody, params, requestId, res, retryCount = 0) { |
| const maxRetries = 3; |
| |
| const apiKey = this.config.getApiKey(); |
| if (!apiKey) { |
| return { success: false, error: '目前暂无可用的API Key', status: 503 }; |
| } |
| |
| try { |
| const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, true); |
| |
| const response = await fetch(apiUrl, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| if (response.status === 403) { |
| this.config.markKeyAsInvalid(apiKey); |
| if (retryCount < maxRetries) { |
| return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
| } |
| return { success: false, error: 'API Key 无效', status: 403 }; |
| } |
| |
| if (response.status === 429) { |
| this.config.moveToUsed(apiKey); |
| if (retryCount < maxRetries) { |
| return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
| } |
| return { success: false, error: '请求频率过高,请稍后重试', status: 429 }; |
| } |
| |
| if (response.status === 500) { |
| this.config.moveToUsed(apiKey); |
| return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 }; |
| } |
| |
| if (!response.ok) { |
| const errorText = await response.text(); |
| console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`); |
| return { success: false, error: `API请求失败: ${response.status}`, status: response.status }; |
| } |
| |
| res.writeHead(200, { |
| 'Content-Type': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive', |
| 'Access-Control-Allow-Origin': '*' |
| }); |
| |
| const parser = new GeminiRealtimeStreamParser(response, async (geminiData) => { |
| const convertedChunk = ResponseConverter.convertStreamChunk(geminiData, requestId, params.model); |
| if (convertedChunk) { |
| res.write(convertedChunk); |
| } |
| }); |
| |
| await parser.start(); |
| res.write('data: [DONE]\n\n'); |
| res.end(); |
| |
| return { success: true }; |
| |
| } catch (error) { |
| console.error('执行流式请求错误:', error); |
| this.config.moveToUsed(apiKey); |
| |
| if (retryCount < maxRetries) { |
| return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
| } |
| |
| return { success: false, error: '网络请求失败: ' + error.message, status: 500 }; |
| } |
| } |
| |
| |
| |
| |
| async executeNormalRequest(requestBody, params, requestId, retryCount = 0) { |
| const maxRetries = 3; |
| |
| const apiKey = this.config.getApiKey(); |
| if (!apiKey) { |
| return { success: false, error: '目前暂无可用的API Key', status: 503 }; |
| } |
| |
| try { |
| const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, false); |
| |
| const response = await fetch(apiUrl, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| if (response.status === 403) { |
| this.config.markKeyAsInvalid(apiKey); |
| if (retryCount < maxRetries) { |
| return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
| } |
| return { success: false, error: 'API Key 无效', status: 403 }; |
| } |
| |
| if (response.status === 429) { |
| this.config.moveToUsed(apiKey); |
| if (retryCount < maxRetries) { |
| return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
| } |
| return { success: false, error: '请求频率过高,请稍后重试', status: 429 }; |
| } |
| |
| if (response.status === 500) { |
| this.config.moveToUsed(apiKey); |
| return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 }; |
| } |
| |
| if (!response.ok) { |
| const errorText = await response.text(); |
| console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`); |
| return { success: false, error: `API请求失败: ${response.status}`, status: response.status }; |
| } |
| |
| const geminiResponse = await response.json(); |
| const openaiResponse = ResponseConverter.convertNormalResponse(geminiResponse, requestId, params.model); |
| return { success: true, data: openaiResponse }; |
| |
| } catch (error) { |
| console.error('执行非流式请求错误:', error); |
| this.config.moveToUsed(apiKey); |
| |
| if (retryCount < maxRetries) { |
| return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
| } |
| |
| return { success: false, error: '网络请求失败: ' + error.message, status: 500 }; |
| } |
| } |
| |
| |
| |
| |
| async handleModelsRequest(req, res) { |
| try { |
| const result = await this.modelManager.getModels(); |
| |
| if (result.success) { |
| res.json(result.data); |
| } else { |
| res.status(result.status || 500).json({ error: result.error }); |
| } |
| } catch (error) { |
| console.error('处理模型列表请求错误:', error); |
| res.status(500).json({ error: '内部服务器错误' }); |
| } |
| } |
| } |
|
|
| |
| |
| |
| class Server { |
| constructor() { |
| this.app = express(); |
| this.apiProxy = new ApiProxyService(); |
| this.setupMiddleware(); |
| this.setupRoutes(); |
| } |
| |
| setupMiddleware() { |
| |
| this.app.use(cors({ |
| origin: '*', |
| credentials: true, |
| optionsSuccessStatus: 200 |
| })); |
| |
| |
| this.app.use(express.json({ limit: '50mb' })); |
| this.app.use(express.urlencoded({ limit: '50mb', extended: true })); |
| |
| |
| this.app.use(this.apiProxy.authMiddleware.middleware()); |
| |
| |
| this.app.use((req, res, next) => { |
| const start = Date.now(); |
| res.on('finish', () => { |
| const duration = Date.now() - start; |
| console.log(`${req.method} ${req.path} - ${res.statusCode} [${duration}ms]`); |
| }); |
| next(); |
| }); |
| } |
| |
| setupRoutes() { |
| |
| this.app.post('/v1/chat/completions', (req, res) => { |
| this.apiProxy.handleChatRequest(req, res); |
| }); |
| |
| |
| this.app.post('/fakestream/v1/chat/completions', (req, res) => { |
| this.apiProxy.handleFakeStreamChatRequest(req, res); |
| }); |
| |
| |
| this.app.get('/v1/models', (req, res) => { |
| this.apiProxy.handleModelsRequest(req, res); |
| }); |
| |
| |
| this.app.get('/fakestream/v1/models', (req, res) => { |
| this.apiProxy.handleModelsRequest(req, res); |
| }); |
| |
| |
| this.app.get('/health', (req, res) => { |
| res.json({ |
| status: 'healthy', |
| timestamp: new Date().toISOString(), |
| availableKeys: this.apiProxy.config.apiKeys.length, |
| usedKeys: this.apiProxy.config.usedApiKeys.length, |
| invalidKeys: this.apiProxy.config.invalidApiKeys.length, |
| version: '2.0.0', |
| features: ['text', 'vision', 'stream', 'fake_stream', 'load_balancing'] |
| }); |
| }); |
| |
| |
| this.app.use('*', (req, res) => { |
| res.status(404).json({ |
| error: { |
| message: 'Not Found', |
| type: 'invalid_request_error', |
| code: 'not_found' |
| } |
| }); |
| }); |
| |
| |
| this.app.use((err, req, res, next) => { |
| console.error('服务器错误:', err); |
| res.status(500).json({ |
| error: { |
| message: '内部服务器错误', |
| type: 'internal_server_error', |
| code: 'server_error' |
| } |
| }); |
| }); |
| } |
| |
| start(port = 3000) { |
| this.app.listen(port, () => { |
| console.log(`🚀 OpenAI to Gemini Proxy Server (Enhanced) 启动在端口 ${port}`); |
| console.log(`📍 聊天API: http://localhost:${port}/v1/chat/completions`); |
| console.log(`📍 假流式聊天API: http://localhost:${port}/fakestream/v1/chat/completions`); |
| console.log(`📋 模型列表: http://localhost:${port}/v1/models`); |
| console.log(`📋 假流式模型列表: http://localhost:${port}/fakestream/v1/models`); |
| console.log(`🔍 健康检查: http://localhost:${port}/health`); |
| }); |
| } |
| } |
|
|
| |
| const server = new Server(); |
| const port = process.env.PORT || 3000; |
| server.start(port); |
|
|