kuefr commited on
Commit
f0c76ab
·
verified ·
1 Parent(s): 22a4947

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +12 -0
  2. index.js +1097 -0
  3. package.json +17 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm install
7
+ COPY . .
8
+
9
+ ENV PORT=7860
10
+ EXPOSE 7860
11
+
12
+ CMD ["npm", "start"]
index.js ADDED
@@ -0,0 +1,1097 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import fetch from 'node-fetch';
3
+ import dotenv from 'dotenv';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import cors from 'cors';
6
+
7
+ // 初始化环境变量
8
+ dotenv.config();
9
+
10
+ /**
11
+ * 配置管理类
12
+ */
13
+ class Config {
14
+ constructor() {
15
+ this.initializeApiKeys();
16
+ this.initializeAuth();
17
+
18
+ // 已使用的API Key池
19
+ this.usedApiKeys = [];
20
+
21
+ // 失效的API Key池
22
+ this.invalidApiKeys = [];
23
+
24
+ // Gemini安全设置
25
+ this.geminiSafety = [
26
+ {
27
+ category: 'HARM_CATEGORY_HARASSMENT',
28
+ threshold: 'OFF',
29
+ },
30
+ {
31
+ category: 'HARM_CATEGORY_HATE_SPEECH',
32
+ threshold: 'OFF',
33
+ },
34
+ {
35
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
36
+ threshold: 'OFF',
37
+ },
38
+ {
39
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
40
+ threshold: 'OFF',
41
+ },
42
+ {
43
+ category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
44
+ threshold: 'OFF',
45
+ },
46
+ ];
47
+ }
48
+
49
+ /**
50
+ * 初始化API Keys
51
+ */
52
+ initializeApiKeys() {
53
+ const apiKeysEnv = process.env.GEMINI_API_KEYS;
54
+ if (!apiKeysEnv) {
55
+ console.error('❌ 错误: 未找到 GEMINI_API_KEYS 环境变量');
56
+ process.exit(1);
57
+ }
58
+
59
+ // 通过换行符分割API Keys
60
+ this.apiKeys = apiKeysEnv
61
+ .split('\n')
62
+ .map(key => key.trim())
63
+ .filter(key => key.length > 0);
64
+
65
+ if (this.apiKeys.length === 0) {
66
+ console.error('❌ 错误: 没有找到有效的API Keys');
67
+ process.exit(1);
68
+ }
69
+
70
+ console.log(`✅ 成功加载 ${this.apiKeys.length} 个API Keys`);
71
+ }
72
+
73
+ /**
74
+ * 初始化认证配置
75
+ */
76
+ initializeAuth() {
77
+ this.authToken = process.env.AUTH_TOKEN || 'sk-123456';
78
+ }
79
+
80
+ /**
81
+ * 获取可用的API Key(负载均衡)
82
+ */
83
+ getApiKey() {
84
+ if (this.apiKeys.length === 0) {
85
+ if (this.usedApiKeys.length > 0) {
86
+ this.apiKeys.push(...this.usedApiKeys);
87
+ this.usedApiKeys = [];
88
+ } else {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ const apiKey = this.apiKeys.shift();
94
+ this.usedApiKeys.push(apiKey);
95
+ return apiKey;
96
+ }
97
+
98
+ /**
99
+ * 获取第一个可用的API Key(用于模型列表请求)
100
+ */
101
+ getFirstAvailableApiKey() {
102
+ if (this.apiKeys.length > 0) {
103
+ return this.apiKeys[0];
104
+ }
105
+ if (this.usedApiKeys.length > 0) {
106
+ return this.usedApiKeys[0];
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * 将API Key标记为失效
113
+ */
114
+ markKeyAsInvalid(apiKey) {
115
+ const usedIndex = this.usedApiKeys.indexOf(apiKey);
116
+ if (usedIndex !== -1) {
117
+ this.usedApiKeys.splice(usedIndex, 1);
118
+ }
119
+
120
+ const mainIndex = this.apiKeys.indexOf(apiKey);
121
+ if (mainIndex !== -1) {
122
+ this.apiKeys.splice(mainIndex, 1);
123
+ }
124
+
125
+ if (!this.invalidApiKeys.includes(apiKey)) {
126
+ this.invalidApiKeys.push(apiKey);
127
+ }
128
+
129
+ console.warn(`⚠️ API Key 已标记为失效: ${apiKey.substring(0, 10)}...`);
130
+ }
131
+
132
+ /**
133
+ * 将API Key移回已使用池
134
+ */
135
+ moveToUsed(apiKey) {
136
+ if (!this.usedApiKeys.includes(apiKey)) {
137
+ this.usedApiKeys.push(apiKey);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 验证授权头
143
+ */
144
+ validateAuth(authHeader) {
145
+ if (!authHeader) {
146
+ return false;
147
+ }
148
+
149
+ const token = authHeader.replace('Bearer ', '');
150
+ return token === this.authToken;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 图片处理器类
156
+ */
157
+ class ImageProcessor {
158
+ /**
159
+ * 从data URL中提取MIME类型和base64数据
160
+ */
161
+ static parseDataUrl(dataUrl) {
162
+ try {
163
+ // 匹配data:image/jpeg;base64,<base64data>格式
164
+ const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
165
+ if (!match) {
166
+ throw new Error('无效的data URL格式');
167
+ }
168
+
169
+ const mimeType = match[1];
170
+ const base64Data = match[2];
171
+
172
+ // 验证MIME类型是否为支持的图片格式
173
+ const supportedMimeTypes = [
174
+ 'image/jpeg',
175
+ 'image/jpg',
176
+ 'image/png',
177
+ 'image/gif',
178
+ 'image/webp',
179
+ 'image/bmp',
180
+ 'image/tiff'
181
+ ];
182
+
183
+ if (!supportedMimeTypes.includes(mimeType.toLowerCase())) {
184
+ throw new Error(`不支持的图片格式: ${mimeType}`);
185
+ }
186
+
187
+ return {
188
+ mimeType,
189
+ data: base64Data
190
+ };
191
+ } catch (error) {
192
+ console.error('解析图片data URL错误:', error);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 验证base64数据是否有效
199
+ */
200
+ static validateBase64(base64String) {
201
+ try {
202
+ // 基本格式检查
203
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) {
204
+ return false;
205
+ }
206
+
207
+ // 长度检查(base64编码长度应该是4的倍数)
208
+ return base64String.length % 4 === 0;
209
+ } catch (error) {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 从URL下载图片并转换为base64
216
+ */
217
+ static async fetchImageAsBase64(imageUrl) {
218
+ try {
219
+ const response = await fetch(imageUrl, {
220
+ timeout: 30000, // 30秒超时
221
+ headers: {
222
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
223
+ }
224
+ });
225
+
226
+ if (!response.ok) {
227
+ throw new Error(`获取图片失败: HTTP ${response.status}`);
228
+ }
229
+
230
+ const contentType = response.headers.get('content-type');
231
+ if (!contentType || !contentType.startsWith('image/')) {
232
+ throw new Error(`URL返回的不是图片类型: ${contentType}`);
233
+ }
234
+
235
+ const buffer = await response.buffer();
236
+ const base64Data = buffer.toString('base64');
237
+
238
+ return {
239
+ mimeType: contentType,
240
+ data: base64Data
241
+ };
242
+ } catch (error) {
243
+ console.error('下载图片错误:', error);
244
+ throw error;
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * 消息转换器类(增强版)
251
+ */
252
+ class MessageConverter {
253
+ /**
254
+ * 将OpenAI格式的消息转换为Gemini格式(支持图片)
255
+ */
256
+ static async convertMessages(openaiMessages) {
257
+ const geminiMessages = [];
258
+ let currentRole = null;
259
+ let currentParts = [];
260
+
261
+ for (const message of openaiMessages) {
262
+ let role = message.role;
263
+ let content = message.content;
264
+
265
+ // 角色转换
266
+ if (role === 'system') {
267
+ role = 'user';
268
+ }
269
+
270
+ if (role === 'assistant') {
271
+ role = 'model';
272
+ }
273
+
274
+ // 处理内容
275
+ let parts = [];
276
+
277
+ if (typeof content === 'string') {
278
+ // 简单文本消息
279
+ parts = [{ text: content }];
280
+ } else if (Array.isArray(content)) {
281
+ // 多模态消息(包含文本和图片)
282
+ parts = await this.convertContentArray(content);
283
+ } else {
284
+ // 其他格式,转为文本
285
+ parts = [{ text: String(content) }];
286
+ }
287
+
288
+ // 合并相同角色的连续消息
289
+ if (role === currentRole) {
290
+ currentParts.push(...parts);
291
+ } else {
292
+ // 保存上一个角色的消息
293
+ if (currentRole !== null && currentParts.length > 0) {
294
+ geminiMessages.push({
295
+ role: currentRole,
296
+ parts: currentParts
297
+ });
298
+ }
299
+
300
+ // 开始新角色
301
+ currentRole = role;
302
+ currentParts = [...parts];
303
+ }
304
+ }
305
+
306
+ // 添加最后一个消息
307
+ if (currentRole !== null && currentParts.length > 0) {
308
+ geminiMessages.push({
309
+ role: currentRole,
310
+ parts: currentParts
311
+ });
312
+ }
313
+
314
+ return geminiMessages;
315
+ }
316
+
317
+ /**
318
+ * 转换OpenAI的content数组为Gemini的parts格式
319
+ */
320
+ static async convertContentArray(contentArray) {
321
+ const parts = [];
322
+
323
+ for (const item of contentArray) {
324
+ try {
325
+ if (item.type === 'text') {
326
+ // 文本内容
327
+ parts.push({ text: item.text || '' });
328
+ } else if (item.type === 'image_url') {
329
+ // 图片内容
330
+ const imagePart = await this.convertImageContent(item);
331
+ if (imagePart) {
332
+ parts.push(imagePart);
333
+ }
334
+ } else {
335
+ // 其他类型,尝试转为文本
336
+ console.warn(`未知的内容类型: ${item.type},将转为文本处理`);
337
+ parts.push({ text: JSON.stringify(item) });
338
+ }
339
+ } catch (error) {
340
+ console.error('转换内容项错误:', error);
341
+ // 出错时跳过该项,避免整个请求失败
342
+ continue;
343
+ }
344
+ }
345
+
346
+ return parts;
347
+ }
348
+
349
+ /**
350
+ * 转换图片内容为Gemini格式
351
+ */
352
+ static async convertImageContent(imageItem) {
353
+ try {
354
+ const imageUrl = imageItem.image_url?.url;
355
+ if (!imageUrl) {
356
+ throw new Error('缺少图片URL');
357
+ }
358
+
359
+ let imageData;
360
+
361
+ if (imageUrl.startsWith('data:')) {
362
+ // 处理base64数据URL
363
+ imageData = ImageProcessor.parseDataUrl(imageUrl);
364
+
365
+ // 验证base64数据
366
+ if (!ImageProcessor.validateBase64(imageData.data)) {
367
+ throw new Error('无效的base64图片数据');
368
+ }
369
+ } else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
370
+ // 处理网络图片URL
371
+ imageData = await ImageProcessor.fetchImageAsBase64(imageUrl);
372
+ } else {
373
+ throw new Error(`不支持的图片URL格式: ${imageUrl}`);
374
+ }
375
+
376
+ // 返回Gemini格式的图片数据
377
+ return {
378
+ inlineData: {
379
+ mimeType: imageData.mimeType,
380
+ data: imageData.data
381
+ }
382
+ };
383
+ } catch (error) {
384
+ console.error('转换图片内容错误:', error);
385
+ // 返回错误信息文本,而不是抛出异常
386
+ return { text: `[图片处理失败: ${error.message}]` };
387
+ }
388
+ }
389
+
390
+ /**
391
+ * 从OpenAI请求中提取参数
392
+ */
393
+ static extractParams(openaiRequest) {
394
+ return {
395
+ model: openaiRequest.model || 'gemini-1.5-flash',
396
+ messages: openaiRequest.messages || [],
397
+ stream: openaiRequest.stream || false,
398
+ temperature: openaiRequest.temperature,
399
+ maxTokens: openaiRequest.max_tokens,
400
+ topP: openaiRequest.top_p
401
+ };
402
+ }
403
+ }
404
+
405
+ /**
406
+ * 模型管理类
407
+ */
408
+ class ModelManager {
409
+ constructor(config) {
410
+ this.config = config;
411
+ this.cachedModels = null;
412
+ this.cacheExpiry = null;
413
+ this.cacheTimeout = 5 * 60 * 1000; // 5分钟缓存
414
+ }
415
+
416
+ /**
417
+ * 获取模型列表
418
+ */
419
+ async getModels() {
420
+ if (this.cachedModels && this.cacheExpiry && Date.now() < this.cacheExpiry) {
421
+ return { success: true, data: this.cachedModels };
422
+ }
423
+
424
+ const apiKey = this.config.getFirstAvailableApiKey();
425
+ if (!apiKey) {
426
+ return {
427
+ success: false,
428
+ error: '没有可用的API Key',
429
+ status: 503
430
+ };
431
+ }
432
+
433
+ try {
434
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
435
+ method: 'GET',
436
+ headers: {
437
+ 'Content-Type': 'application/json'
438
+ }
439
+ });
440
+
441
+ if (!response.ok) {
442
+ return {
443
+ success: false,
444
+ error: `获取模型列表失败: ${response.status}`,
445
+ status: response.status
446
+ };
447
+ }
448
+
449
+ const geminiResponse = await response.json();
450
+ const filteredModels = this.filterModels(geminiResponse.models || []);
451
+
452
+ this.cachedModels = filteredModels;
453
+ this.cacheExpiry = Date.now() + this.cacheTimeout;
454
+
455
+ return { success: true, data: filteredModels };
456
+
457
+ } catch (error) {
458
+ console.error('获取模型列表错误:', error);
459
+ return {
460
+ success: false,
461
+ error: '网络请求失败',
462
+ status: 500
463
+ };
464
+ }
465
+ }
466
+
467
+ /**
468
+ * 过滤模型 - 保持原始Gemini模型名,并增加视觉模型支持
469
+ */
470
+ filterModels(models) {
471
+ const allowedPrefixes = [
472
+ 'models/gemini-2.5-flash',
473
+ 'models/gemini-2.0-flash',
474
+ 'models/gemini-1.5-flash'
475
+ ];
476
+
477
+ const excludedModels = [
478
+ 'models/gemini-1.5-flash-8b'
479
+ ];
480
+
481
+ const filteredModels = models.filter(model => {
482
+ const modelName = model.name;
483
+
484
+ if (excludedModels.some(excluded => modelName.startsWith(excluded))) {
485
+ return false;
486
+ }
487
+ if(modelName == "models/gemini-2.5-pro"){
488
+ return true;
489
+ }
490
+
491
+ return allowedPrefixes.some(prefix => modelName.startsWith(prefix));
492
+ });
493
+
494
+ // 转换为OpenAI格式但保持Gemini模型名
495
+ const processedModels = filteredModels.map(model => {
496
+ const modelId = model.name.replace('models/', '');
497
+
498
+ return {
499
+ id: modelId, // 直接使用Gemini模型名
500
+ object: 'model',
501
+ created: Math.floor(Date.now() / 1000),
502
+ owned_by: 'google',
503
+ permission: [
504
+ {
505
+ id: `modelperm-${modelId}`,
506
+ object: 'model_permission',
507
+ created: Math.floor(Date.now() / 1000),
508
+ allow_create_engine: false,
509
+ allow_sampling: true,
510
+ allow_logprobs: false,
511
+ allow_search_indices: false,
512
+ allow_view: true,
513
+ allow_fine_tuning: false,
514
+ organization: '*',
515
+ group: null,
516
+ is_blocking: false
517
+ }
518
+ ],
519
+ root: modelId,
520
+ parent: null
521
+ };
522
+ });
523
+
524
+ return {
525
+ object: 'list',
526
+ data: processedModels
527
+ };
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Gemini API 请求构建器类
533
+ */
534
+ class GeminiRequestBuilder {
535
+ constructor(config) {
536
+ this.config = config;
537
+ }
538
+
539
+ /**
540
+ * 构建Gemini API请求体
541
+ */
542
+ buildRequestBody(geminiMessages, params) {
543
+ const requestBody = {
544
+ contents: geminiMessages,
545
+ safetySettings: this.config.geminiSafety,
546
+ generationConfig: {}
547
+ };
548
+
549
+ if (params.temperature !== undefined) {
550
+ requestBody.generationConfig.temperature = params.temperature;
551
+ }
552
+
553
+ if (params.maxTokens !== undefined) {
554
+ requestBody.generationConfig.maxOutputTokens = params.maxTokens;
555
+ }
556
+
557
+ if (params.topP !== undefined) {
558
+ requestBody.generationConfig.topP = params.topP;
559
+ }
560
+
561
+ return requestBody;
562
+ }
563
+
564
+ /**
565
+ * 构建Gemini API URL
566
+ */
567
+ buildApiUrl(model, apiKey, isStream = false) {
568
+ const method = isStream ? 'streamGenerateContent' : 'generateContent';
569
+ return `https://generativelanguage.googleapis.com/v1beta/models/${model}:${method}?key=${apiKey}`;
570
+ }
571
+ }
572
+
573
+ /**
574
+ * 响应转换器类
575
+ */
576
+ class ResponseConverter {
577
+ /**
578
+ * 将Gemini流式响应块转换为OpenAI格式
579
+ */
580
+ static convertStreamChunk(geminiData, requestId, model) {
581
+ try {
582
+ if (geminiData.candidates && geminiData.candidates[0]) {
583
+ const candidate = geminiData.candidates[0];
584
+ if (candidate.content && candidate.content.parts) {
585
+ const text = candidate.content.parts[0]?.text || '';
586
+ const openaiChunk = {
587
+ id: requestId,
588
+ object: 'chat.completion.chunk',
589
+ created: Math.floor(Date.now() / 1000),
590
+ model: model, // 使用当前请求的模型名
591
+ choices: [{
592
+ index: 0,
593
+ delta: { content: text },
594
+ finish_reason: candidate.finishReason === 'STOP' ? 'stop' : null
595
+ }]
596
+ };
597
+ return `data: ${JSON.stringify(openaiChunk)}\n\n`;
598
+ }
599
+ }
600
+ return '';
601
+ } catch (error) {
602
+ console.error('转换流响应块错误:', error);
603
+ return '';
604
+ }
605
+ }
606
+
607
+ /**
608
+ * 将Gemini非流式响应转换为OpenAI格式
609
+ */
610
+ static convertNormalResponse(geminiResponse, requestId, model) {
611
+ const openaiResponse = {
612
+ id: requestId,
613
+ object: 'chat.completion',
614
+ created: Math.floor(Date.now() / 1000),
615
+ model: model, // 使用当前请求的模型名
616
+ choices: [],
617
+ usage: {
618
+ prompt_tokens: 0,
619
+ completion_tokens: 0,
620
+ total_tokens: 0
621
+ }
622
+ };
623
+
624
+ if (geminiResponse.candidates && geminiResponse.candidates[0]) {
625
+ const candidate = geminiResponse.candidates[0];
626
+ if (candidate.content && candidate.content.parts) {
627
+ const text = candidate.content.parts.map(part => part.text).join('');
628
+ openaiResponse.choices.push({
629
+ index: 0,
630
+ message: {
631
+ role: 'assistant',
632
+ content: text
633
+ },
634
+ finish_reason: candidate.finishReason === 'STOP' ? 'stop' : 'length'
635
+ });
636
+ }
637
+ }
638
+
639
+ // 尝试从usage信息中获取token使用量
640
+ if (geminiResponse.usageMetadata) {
641
+ openaiResponse.usage = {
642
+ prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
643
+ completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
644
+ total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0
645
+ };
646
+ }
647
+
648
+ return openaiResponse;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Gemini实时流式响应解析器
654
+ */
655
+ class GeminiRealtimeStreamParser {
656
+ constructor(response, onChunk) {
657
+ this.response = response;
658
+ this.onChunk = onChunk;
659
+ this.buffer = '';
660
+ this.bufferLv = 0;
661
+ this.inString = false;
662
+ this.escapeNext = false;
663
+ this.decoder = new TextDecoder();
664
+ }
665
+
666
+ async start() {
667
+ try {
668
+ for await (const chunk of this.response.body) {
669
+ const text = this.decoder.decode(chunk, { stream: true });
670
+ await this.processText(text);
671
+ }
672
+
673
+ await this.handleRemainingBuffer();
674
+ } catch (error) {
675
+ console.error('流式解析错误:', error);
676
+ throw error;
677
+ }
678
+ }
679
+
680
+ async processText(text) {
681
+ for (const char of text) {
682
+ if (this.escapeNext) {
683
+ if (this.bufferLv > 1) {
684
+ this.buffer += char;
685
+ }
686
+ this.escapeNext = false;
687
+ continue;
688
+ }
689
+
690
+ if (char === '\\' && this.inString) {
691
+ this.escapeNext = true;
692
+ if (this.bufferLv > 1) {
693
+ this.buffer += char;
694
+ }
695
+ continue;
696
+ }
697
+
698
+ if (char === '"') {
699
+ this.inString = !this.inString;
700
+ }
701
+
702
+ if (!this.inString) {
703
+ if (char === '{' || char === '[') {
704
+ this.bufferLv++;
705
+ } else if (char === '}' || char === ']') {
706
+ this.bufferLv--;
707
+ }
708
+ }
709
+
710
+ if (this.bufferLv > 1) {
711
+ if (this.inString && char === '\n') {
712
+ this.buffer += '\\n';
713
+ } else {
714
+ this.buffer += char;
715
+ }
716
+ } else if (this.bufferLv === 1 && this.buffer) {
717
+ this.buffer += '}';
718
+
719
+ try {
720
+ const bufferJson = JSON.parse(this.buffer);
721
+ await this.onChunk(bufferJson);
722
+ } catch (parseError) {
723
+ console.error('解析Gemini流数据错误:', parseError);
724
+ }
725
+
726
+ this.buffer = '';
727
+ }
728
+ }
729
+ }
730
+
731
+ async handleRemainingBuffer() {
732
+ if (this.buffer.trim() && this.bufferLv >= 1) {
733
+ try {
734
+ if (!this.buffer.endsWith('}')) {
735
+ this.buffer += '}';
736
+ }
737
+ const bufferJson = JSON.parse(this.buffer);
738
+ await this.onChunk(bufferJson);
739
+ } catch (parseError) {
740
+ console.error('解析最后的缓冲区数据错误:', parseError);
741
+ }
742
+ }
743
+ }
744
+ }
745
+
746
+ /**
747
+ * 认证中间件
748
+ */
749
+ class AuthMiddleware {
750
+ constructor(config) {
751
+ this.config = config;
752
+ }
753
+
754
+ middleware() {
755
+ return (req, res, next) => {
756
+ // 跳过健康检查和预检请求
757
+ if (req.path === '/health' || req.method === 'OPTIONS') {
758
+ return next();
759
+ }
760
+
761
+ const authHeader = req.headers.authorization;
762
+
763
+ if (!this.config.validateAuth(authHeader)) {
764
+ return res.status(401).json({
765
+ error: {
766
+ message: 'Invalid authentication credentials',
767
+ type: 'invalid_request_error',
768
+ code: 'invalid_api_key'
769
+ }
770
+ });
771
+ }
772
+
773
+ next();
774
+ };
775
+ }
776
+ }
777
+
778
+ /**
779
+ * API代理服务类
780
+ */
781
+ class ApiProxyService {
782
+ constructor() {
783
+ this.config = new Config();
784
+ this.requestBuilder = new GeminiRequestBuilder(this.config);
785
+ this.modelManager = new ModelManager(this.config);
786
+ this.authMiddleware = new AuthMiddleware(this.config);
787
+ }
788
+
789
+ /**
790
+ * 处理聊天API请求(支持图片)
791
+ */
792
+ async handleChatRequest(req, res) {
793
+ try {
794
+ const requestId = `chatcmpl-${uuidv4()}`;
795
+
796
+ const params = MessageConverter.extractParams(req.body);
797
+
798
+ // 异步转换消息(支持图片处理)
799
+ const geminiMessages = await MessageConverter.convertMessages(params.messages);
800
+
801
+ if (!geminiMessages || geminiMessages.length === 0) {
802
+ return res.status(400).json({
803
+ error: {
804
+ message: '无效的消息格式或消息为空',
805
+ type: 'invalid_request_error',
806
+ code: 'invalid_messages'
807
+ }
808
+ });
809
+ }
810
+
811
+ const requestBody = this.requestBuilder.buildRequestBody(geminiMessages, params);
812
+
813
+ if (params.stream) {
814
+ const result = await this.handleStreamRequest(requestBody, params, requestId, res);
815
+ if (!result.success) {
816
+ res.status(result.status || 500).json({ error: result.error });
817
+ }
818
+ } else {
819
+ const result = await this.executeNormalRequest(requestBody, params, requestId);
820
+ if (result.success) {
821
+ res.json(result.data);
822
+ } else {
823
+ res.status(result.status || 500).json({ error: result.error });
824
+ }
825
+ }
826
+ } catch (error) {
827
+ console.error('处理聊天请求错误:', error);
828
+ res.status(500).json({
829
+ error: {
830
+ message: '内部服务器错误: ' + error.message,
831
+ type: 'internal_server_error',
832
+ code: 'server_error'
833
+ }
834
+ });
835
+ }
836
+ }
837
+
838
+ /**
839
+ * 处理流式请求
840
+ */
841
+ async handleStreamRequest(requestBody, params, requestId, res, retryCount = 0) {
842
+ const maxRetries = 3;
843
+
844
+ const apiKey = this.config.getApiKey();
845
+ if (!apiKey) {
846
+ return { success: false, error: '目前暂无可用的API Key', status: 503 };
847
+ }
848
+
849
+ try {
850
+ const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, true);
851
+
852
+ const response = await fetch(apiUrl, {
853
+ method: 'POST',
854
+ headers: { 'Content-Type': 'application/json' },
855
+ body: JSON.stringify(requestBody)
856
+ });
857
+
858
+ if (response.status === 403) {
859
+ this.config.markKeyAsInvalid(apiKey);
860
+ if (retryCount < maxRetries) {
861
+ return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1);
862
+ }
863
+ return { success: false, error: 'API Key 无效', status: 403 };
864
+ }
865
+
866
+ if (response.status === 429) {
867
+ this.config.moveToUsed(apiKey);
868
+ if (retryCount < maxRetries) {
869
+ return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1);
870
+ }
871
+ return { success: false, error: '请求频率过高,请稍后重试', status: 429 };
872
+ }
873
+
874
+ if (response.status === 500) {
875
+ this.config.moveToUsed(apiKey);
876
+ return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 };
877
+ }
878
+
879
+ if (!response.ok) {
880
+ const errorText = await response.text();
881
+ console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`);
882
+ return { success: false, error: `API请求失败: ${response.status}`, status: response.status };
883
+ }
884
+
885
+ res.writeHead(200, {
886
+ 'Content-Type': 'text/event-stream',
887
+ 'Cache-Control': 'no-cache',
888
+ 'Connection': 'keep-alive',
889
+ 'Access-Control-Allow-Origin': '*'
890
+ });
891
+
892
+ const parser = new GeminiRealtimeStreamParser(response, async (geminiData) => {
893
+ const convertedChunk = ResponseConverter.convertStreamChunk(geminiData, requestId, params.model);
894
+ if (convertedChunk) {
895
+ res.write(convertedChunk);
896
+ }
897
+ });
898
+
899
+ await parser.start();
900
+ res.write('data: [DONE]\n\n');
901
+ res.end();
902
+
903
+ return { success: true };
904
+
905
+ } catch (error) {
906
+ console.error('执行流式请求错误:', error);
907
+ this.config.moveToUsed(apiKey);
908
+
909
+ if (retryCount < maxRetries) {
910
+ return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1);
911
+ }
912
+
913
+ return { success: false, error: '网络请求失败: ' + error.message, status: 500 };
914
+ }
915
+ }
916
+
917
+ /**
918
+ * 处理非流式请求
919
+ */
920
+ async executeNormalRequest(requestBody, params, requestId, retryCount = 0) {
921
+ const maxRetries = 3;
922
+
923
+ const apiKey = this.config.getApiKey();
924
+ if (!apiKey) {
925
+ return { success: false, error: '目前暂无可用的API Key', status: 503 };
926
+ }
927
+
928
+ try {
929
+ const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, false);
930
+
931
+ const response = await fetch(apiUrl, {
932
+ method: 'POST',
933
+ headers: { 'Content-Type': 'application/json' },
934
+ body: JSON.stringify(requestBody)
935
+ });
936
+
937
+ if (response.status === 403) {
938
+ this.config.markKeyAsInvalid(apiKey);
939
+ if (retryCount < maxRetries) {
940
+ return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1);
941
+ }
942
+ return { success: false, error: 'API Key 无效', status: 403 };
943
+ }
944
+
945
+ if (response.status === 429) {
946
+ this.config.moveToUsed(apiKey);
947
+ if (retryCount < maxRetries) {
948
+ return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1);
949
+ }
950
+ return { success: false, error: '请求频率过高,请稍后重试', status: 429 };
951
+ }
952
+
953
+ if (response.status === 500) {
954
+ this.config.moveToUsed(apiKey);
955
+ return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 };
956
+ }
957
+
958
+ if (!response.ok) {
959
+ const errorText = await response.text();
960
+ console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`);
961
+ return { success: false, error: `API请求失败: ${response.status}`, status: response.status };
962
+ }
963
+
964
+ const geminiResponse = await response.json();
965
+ const openaiResponse = ResponseConverter.convertNormalResponse(geminiResponse, requestId, params.model);
966
+ return { success: true, data: openaiResponse };
967
+
968
+ } catch (error) {
969
+ console.error('执行非流式请求错误:', error);
970
+ this.config.moveToUsed(apiKey);
971
+
972
+ if (retryCount < maxRetries) {
973
+ return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1);
974
+ }
975
+
976
+ return { success: false, error: '网络请求失败: ' + error.message, status: 500 };
977
+ }
978
+ }
979
+
980
+ /**
981
+ * 处理模型列表请求
982
+ */
983
+ async handleModelsRequest(req, res) {
984
+ try {
985
+ const result = await this.modelManager.getModels();
986
+
987
+ if (result.success) {
988
+ res.json(result.data);
989
+ } else {
990
+ res.status(result.status || 500).json({ error: result.error });
991
+ }
992
+ } catch (error) {
993
+ console.error('处理模型列表请求错误:', error);
994
+ res.status(500).json({ error: '内部服务器错误' });
995
+ }
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Express 服务器
1001
+ */
1002
+ class Server {
1003
+ constructor() {
1004
+ this.app = express();
1005
+ this.apiProxy = new ApiProxyService();
1006
+ this.setupMiddleware();
1007
+ this.setupRoutes();
1008
+ }
1009
+
1010
+ setupMiddleware() {
1011
+ // CORS配置
1012
+ this.app.use(cors({
1013
+ origin: '*',
1014
+ credentials: true,
1015
+ optionsSuccessStatus: 200
1016
+ }));
1017
+
1018
+ // JSON解析 - 增加大小限制以支持图片
1019
+ this.app.use(express.json({ limit: '50mb' }));
1020
+ this.app.use(express.urlencoded({ limit: '50mb', extended: true }));
1021
+
1022
+ // 认证中间件
1023
+ this.app.use(this.apiProxy.authMiddleware.middleware());
1024
+
1025
+ // 请求日志中间件
1026
+ this.app.use((req, res, next) => {
1027
+ const start = Date.now();
1028
+ res.on('finish', () => {
1029
+ const duration = Date.now() - start;
1030
+ console.log(`${req.method} ${req.path} - ${res.statusCode} [${duration}ms]`);
1031
+ });
1032
+ next();
1033
+ });
1034
+ }
1035
+
1036
+ setupRoutes() {
1037
+ // 聊天接口(支持图片)
1038
+ this.app.post('/v1/chat/completions', (req, res) => {
1039
+ this.apiProxy.handleChatRequest(req, res);
1040
+ });
1041
+
1042
+ // 模型列表接口
1043
+ this.app.get('/v1/models', (req, res) => {
1044
+ this.apiProxy.handleModelsRequest(req, res);
1045
+ });
1046
+
1047
+ // 健康检查接口
1048
+ this.app.get('/health', (req, res) => {
1049
+ res.json({
1050
+ status: 'healthy',
1051
+ timestamp: new Date().toISOString(),
1052
+ availableKeys: this.apiProxy.config.apiKeys.length,
1053
+ usedKeys: this.apiProxy.config.usedApiKeys.length,
1054
+ invalidKeys: this.apiProxy.config.invalidApiKeys.length,
1055
+ version: '2.0.0',
1056
+ features: ['text', 'vision', 'stream', 'load_balancing']
1057
+ });
1058
+ });
1059
+
1060
+ // 404处理
1061
+ this.app.use('*', (req, res) => {
1062
+ res.status(404).json({
1063
+ error: {
1064
+ message: 'Not Found',
1065
+ type: 'invalid_request_error',
1066
+ code: 'not_found'
1067
+ }
1068
+ });
1069
+ });
1070
+
1071
+ // 全局错误处理
1072
+ this.app.use((err, req, res, next) => {
1073
+ console.error('服务器错误:', err);
1074
+ res.status(500).json({
1075
+ error: {
1076
+ message: '内部服务器错误',
1077
+ type: 'internal_server_error',
1078
+ code: 'server_error'
1079
+ }
1080
+ });
1081
+ });
1082
+ }
1083
+
1084
+ start(port = 3000) {
1085
+ this.app.listen(port, () => {
1086
+ console.log(`🚀 OpenAI to Gemini Proxy Server (Enhanced) 启动在端口 ${port}`);
1087
+ console.log(`📍 聊天API: http://localhost:${port}/v1/chat/completions`);
1088
+ console.log(`📋 模型列表: http://localhost:${port}/v1/models`);
1089
+ console.log(`🔍 健康检查: http://localhost:${port}/health`);
1090
+ });
1091
+ }
1092
+ }
1093
+
1094
+ // 启动服务器
1095
+ const server = new Server();
1096
+ const port = process.env.PORT || 3000;
1097
+ server.start(port);
package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemini2openai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "author": "yxmiler",
10
+ "dependencies": {
11
+ "cors": "^2.8.5",
12
+ "dotenv": "^16.4.7",
13
+ "express": "^4.21.2",
14
+ "uuid": "^11.0.4",
15
+ "node-fetch": "^3.3.2"
16
+ }
17
+ }