xt8 commited on
Commit
9e247e3
·
verified ·
1 Parent(s): 80a6dd4

Update main.ts

Browse files
Files changed (1) hide show
  1. main.ts +264 -244
main.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
2
 
3
  // --- 常量定义 ---
@@ -12,7 +13,7 @@ interface OpenAIMessage {
12
  type: string;
13
  text?: string;
14
  image_url?: { url: string };
15
- document?: { url: string; type: string }; // 支持多种文档类型
16
  }>;
17
  }
18
 
@@ -25,8 +26,8 @@ interface OpenAIRequest {
25
  }
26
 
27
  interface OpenAITTSRequest {
28
- model: string;
29
- input: string;
30
  voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
31
  response_format?: 'mp3' | 'opus' | 'aac' | 'flac';
32
  speed?: number;
@@ -61,16 +62,10 @@ class GoogleAIService {
61
  return key;
62
  }
63
 
64
- // --- [新增] TTS 功能 ---
65
  private getGoogleVoice(openAIVoice: string): string {
66
  const voiceMap: { [key: string]: string } = {
67
- 'alloy': 'Kore',
68
- 'echo': 'Sal',
69
- 'fable': 'Polly',
70
- 'onyx': 'Onyx',
71
- 'nova': 'Sparkle',
72
- 'shimmer': 'Luna',
73
- 'default': 'Kore'
74
  };
75
  return voiceMap[openAIVoice] || voiceMap['default'];
76
  }
@@ -81,70 +76,43 @@ class GoogleAIService {
81
  const ttsModel = "gemini-2.5-flash-preview-tts";
82
 
83
  console.log(`Generating speech with model: ${ttsModel}, voice: ${googleVoice} (mapped from OpenAI's '${voice}')`);
84
-
85
  const requestBody = {
86
  "contents": [{"parts":[{"text": input}]}],
87
- "generationConfig": {
88
- "responseModalities": ["AUDIO"],
89
- "speechConfig": {"voiceConfig": {"prebuiltVoiceConfig": {"voiceName": googleVoice}}}
90
- },
91
  "model": ttsModel,
92
  };
93
-
94
- const response = await fetch(
95
- `https://generativelanguage.googleapis.com/v1beta/models/${ttsModel}:generateContent?key=${apiKey}`,
96
- { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }
97
- );
98
-
99
  if (!response.ok) {
100
  const errorBody = await response.json().catch(() => response.text());
101
  const errorMessage = errorBody?.error?.message || JSON.stringify(errorBody);
102
- console.error(`Google TTS API Error: ${response.status} - ${errorMessage}`);
103
  throw new Error(`Google TTS API request failed with status ${response.status}: ${errorMessage}`);
104
  }
105
-
106
  const data = await response.json();
107
  const audioContentBase64 = data.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
108
- if (!audioContentBase64) {
109
- throw new Error("No audio data returned from Google API. The response might be blocked or empty.");
110
- }
111
-
112
  const binaryString = atob(audioContentBase64);
113
- const len = binaryString.length;
114
- const bytes = new Uint8Array(len);
115
- for (let i = 0; i < len; i++) {
116
  bytes[i] = binaryString.charCodeAt(i);
117
  }
118
  return bytes.buffer;
119
  }
120
 
121
- // --- 模型处理等现有代码保持不变 ---
122
-
123
- async fetchOfficialModels(): Promise<any[]> { /* ... 保持不变 ... */
124
  const now = Date.now();
125
- if (this.cachedModels.length > 0 && (now - this.modelsLastFetch) < MODELS_CACHE_DURATION) {
126
- return this.cachedModels;
127
- }
128
-
129
  const apiKey = this.getNextApiKey();
130
  try {
131
- const response = await fetch(
132
- `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
133
- { method: "GET", headers: { "Content-Type": "application/json" } }
134
- );
135
-
136
  if (!response.ok) {
137
  console.warn(`Failed to fetch models from Google AI: ${response.status}. Using fallback models.`);
138
  return this.getFallbackModels();
139
  }
140
-
141
  const data = await response.json();
142
  if (data.models && Array.isArray(data.models)) {
143
- this.cachedModels = data.models.filter((model: any) =>
144
- model.supportedGenerationMethods?.includes('generateContent')
145
- );
146
  this.modelsLastFetch = now;
147
- console.log(`Fetched ${this.cachedModels.length} models from Google AI`);
148
  return this.cachedModels;
149
  }
150
  return this.getFallbackModels();
@@ -154,98 +122,120 @@ class GoogleAIService {
154
  }
155
  }
156
 
157
- private getFallbackModels(): any[] { /* ... 保持不变 ... */
158
  return [
159
- { name: "models/gemini-1.5-pro", displayName: "Gemini 1.5 Pro", description: "Mid-size multimodal model that supports up to 1 million tokens, images, and documents (PDF, TXT, MD)", supportedGenerationMethods: ["generateContent"], maxTokens: 1000000, supportsDocuments: true },
160
- { name: "models/gemini-1.5-flash", displayName: "Gemini 1.5 Flash", description: "Fast and versatile multimodal model for diverse tasks, supports images and documents (PDF, TXT, MD)", supportedGenerationMethods: ["generateContent"], maxTokens: 1000000, supportsDocuments: true },
161
- { name: "models/gemini-2.0-flash-preview-image-generation", displayName: "Gemini 2.0 Flash Image Generation", description: "Advanced model for generating and editing high-quality images with text and image outputs", supportedGenerationMethods: ["generateContent"], maxTokens: 100000, capabilities: ["text", "image_generation", "image_editing"] },
162
- { name: "models/gemini-2.5-flash-preview-tts", displayName: "Gemini 2.5 Flash TTS", description: "Text-to-speech model for generating high-quality audio.", supportedGenerationMethods: ["generateContent"], id: "gemini-2.5-flash-preview-tts" }
163
  ];
164
  }
165
 
166
  public isVisionModel = (modelName: string): boolean => modelName.toLowerCase().includes('vision') || modelName.toLowerCase().includes('pro');
167
- public isImageGenerationModel = (modelName: string): boolean => modelName.includes('image-generation') || modelName === 'gemini-2.0-flash-preview-image-generation';
168
- public isImageEditingModel = (modelName: string): boolean => modelName.includes('image-generation') || modelName === 'gemini-2.0-flash-preview-image-generation';
169
- public isDocumentModel = (modelName: string): boolean => modelName.toLowerCase().includes('gemini-1.5') || modelName.toLowerCase().includes('pro') || modelName.toLowerCase().includes('flash');
 
 
 
 
 
 
 
 
170
 
171
- // ... 省略 extractDocumentData, extractImageData 等辅助函数,它们保持不变 ...
172
- private getDocumentType(url: string): string { /* ... 保持不变 ... */ return ''; }
173
- private extractDocumentData(documentUrl: string): { mimeType: string; data: string; text?: string; docType: string } { /* ... 保持不变 ... */ return { mimeType: '', data: '', docType: ''}; }
174
- private extractImageData(imageUrl: string): { mimeType: string; data: string } { /* ... 保持不变 ... */ return {mimeType: '', data: ''}; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
- // --- 内容生成函数 (非流式) ---
178
- // ... generateContentWithDocument, generateContent 等保持不变 ...
179
- async generateContentWithDocument(messages: OpenAIMessage[], modelName: string): Promise<string> { /* ... 保持不变 ... */ return ''; }
180
- async generateContent(messages: OpenAIMessage[], modelName: string, enableSearch: boolean = false): Promise<string> { /* ... 保持不变 ... */
181
- // 这部分逻辑保持原样,用于非流式请求
182
  const apiKey = this.getNextApiKey();
183
  const fullModelName = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
184
- const contents = messages.map(msg => {
185
- // ... 消息转换逻辑 ...
186
- });
187
- const requestBody: any = { contents, generationConfig: { /*...*/ } };
188
- const response = await fetch(
189
- `https://generativelanguage.googleapis.com/v1beta/${fullModelName}:generateContent?key=${apiKey}`,
190
- { method: "POST", body: JSON.stringify(requestBody), headers: { "Content-Type": "application/json" } }
191
- );
192
- // ... 错误处理和结果解析 ...
 
193
  const data = await response.json();
194
- return data.candidates?.[0]?.content?.parts[0]?.text || "No response generated";
 
 
 
 
 
 
195
  }
196
 
197
-
198
- // --- [新增] 真正的流式内容生成函数 ---
199
  /**
200
- * 使用 Google 的 streamGenerateContent 端点进行真正的流式内容生成
201
- * 这个函数是一个异步生成器,会不断 yield API 收到的文本块。
202
- * @param messages OpenAI 格式的消息
203
- * @param modelName 请求的模型名称
204
- * @yields {string} 文本块
205
  */
206
  async * streamGenerateContent(messages: OpenAIMessage[], modelName: string): AsyncGenerator<string> {
207
  const apiKey = this.getNextApiKey();
208
  const fullModelName = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
 
 
209
 
210
- // 注意:文档/图片处理的逻辑需要与非流式版本保持一致
211
- const contents = messages.map(msg => {
212
- if (typeof msg.content === "string") {
213
- return { role: msg.role === "assistant" ? "model" : "user", parts: [{ text: msg.content }] };
214
- } else {
215
- const messageParts = msg.content.map(part => {
216
- if (part.type === "text") return { text: part.text };
217
- if (part.type === "image_url" && part.image_url) {
218
- const { mimeType, data } = this.extractImageData(part.image_url.url);
219
- return { inlineData: { mimeType, data } };
220
- }
221
- // 简单处理,可以根据需要扩展
222
- return { text: "" };
223
- });
224
- return { role: msg.role === "assistant" ? "model" : "user", parts: messageParts.filter(p => p.text || p.inlineData) };
225
- }
226
- });
227
-
228
- const requestBody = {
229
- contents,
230
- generationConfig: { temperature: 0.7, maxOutputTokens: 8192 }
231
- };
232
-
233
  // [关键] 使用 :streamGenerateContent 端点
234
- const response = await fetch(
235
- `https://generativelanguage.googleapis.com/v1beta/${fullModelName}:streamGenerateContent?key=${apiKey}`,
236
- {
237
- method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify(requestBody),
240
- }
241
- );
242
 
243
  if (!response.ok || !response.body) {
244
  const errorText = await response.text();
245
- throw new Error(`Google AI API streaming error: ${response.status} - ${errorText}`);
246
  }
247
-
248
- // [关键] 读取并解析流式响应
249
  const reader = response.body.getReader();
250
  const decoder = new TextDecoder();
251
  let buffer = "";
@@ -256,25 +246,21 @@ class GoogleAIService {
256
 
257
  buffer += decoder.decode(value, { stream: true });
258
 
259
- // Google 的流式响应可能在一个数据包里包含多个JSON对象,它们以 "data: " 开头
260
- // 我们需要处理这种情况
261
- while (buffer.includes('\n')) {
262
- const endOfLine = buffer.indexOf('\n');
263
- const line = buffer.substring(0, endOfLine).trim();
264
- buffer = buffer.substring(endOfLine + 1);
265
 
 
266
  if (line.startsWith('data: ')) {
267
  try {
268
- const jsonStr = line.substring(6); // 去掉 'data: '
269
  const chunk = JSON.parse(jsonStr);
270
 
271
- if (chunk.error) {
272
- throw new Error(`Google API Error in stream: ${chunk.error.message}`);
273
- }
274
 
275
  const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
276
  if (text) {
277
- yield text;
278
  }
279
  } catch (e) {
280
  console.warn("Could not parse stream chunk:", line, e.message);
@@ -284,13 +270,44 @@ class GoogleAIService {
284
  }
285
  }
286
 
287
- // ... 其他辅助函数如 generateOrEditImage, generateContentWithGrounding 保持不变 ...
288
- async generateOrEditImageWithGemini(prompt: string, modelName: string = "gemini-2.0-flash-preview-image-generation", inputImage?: { mimeType: string; data: string }): Promise<{ text?: string; imageBase64?: string; imageUrl?: string }> { /* ... */ return {};}
289
- async generateContentWithGrounding(messages: OpenAIMessage[], modelName: string): Promise<string> { /* ... */ return ''; }
290
- async generateContentWithSearchPrompt(messages: OpenAIMessage[], modelName: string): Promise<string> { /* ... */ return ''; }
291
- async generateOrEditImage(prompt: string, modelName: string, inputImages?: any[]): Promise<string> { /* ... */ return ''; }
292
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
 
 
 
 
 
 
294
 
295
  class OpenAICompatibleServer {
296
  private googleAI: GoogleAIService;
@@ -301,91 +318,77 @@ class OpenAICompatibleServer {
301
  this.authKey = Deno.env.get("AUTH_KEY") || "";
302
  }
303
 
304
- private authenticate(request: Request): boolean { /* ... 保持不变 ... */ return true; }
305
- private isDocumentContent(url?: string): boolean { /* ... 保持不变 ... */ return false; }
306
- private async handleAudioSpeech(request: Request): Promise<Response> { /* ... 保持不变 ... */ return new Response(); }
 
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  private async handleChatCompletions(request: Request): Promise<Response> {
309
  try {
310
  const body: OpenAIRequest = await request.json();
311
  const requestedModel = body.model || "gemini-1.5-pro";
312
- const stream = body.stream || false;
313
- console.log(`Request for model: ${requestedModel}, stream: ${stream}`);
314
 
315
- if (stream) {
316
- // [修改] 调用新的流式处理逻辑
317
  const googleStream = this.googleAI.streamGenerateContent(body.messages, requestedModel);
318
  const openAIStream = this.streamGoogleResponseAsOpenAI(googleStream, requestedModel);
319
  return new Response(openAIStream, {
320
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" }
321
  });
322
- } else {
323
- // [保持不变] 非流式逻辑
324
- const lastMessage = body.messages[body.messages.length - 1];
325
- const content = typeof lastMessage.content === "string"
326
- ? lastMessage.content
327
- : (Array.isArray(lastMessage.content) ? lastMessage.content.map(p => p.text || "").join(" ") : "");
328
-
329
- const hasDocument = body.messages.some(msg =>
330
- Array.isArray(msg.content) &&
331
- msg.content.some(part => part.type === "document" || this.isDocumentContent(part.document?.url))
332
- );
333
-
334
- const hasImages = body.messages.some(msg => Array.isArray(msg.content) && msg.content.some(part => part.type === "image_url"));
335
-
336
- let inputImages: any[] = [];
337
- if (hasImages) {
338
- body.messages.forEach(msg => {
339
- if (Array.isArray(msg.content)) {
340
- msg.content.forEach(part => {
341
- if (part.type === "image_url" && part.image_url) inputImages.push({ url: part.image_url.url });
342
- });
343
- }
344
- });
345
- }
346
-
347
- let responseText: string;
348
-
349
- if (hasDocument) {
350
- responseText = await this.googleAI.generateContentWithDocument(body.messages, requestedModel);
351
- } else if (this.googleAI.isImageEditingModel(requestedModel) && hasImages) {
352
- responseText = await this.googleAI.generateOrEditImage(content, requestedModel, inputImages);
353
- } else if (this.googleAI.isImageGenerationModel(requestedModel)) {
354
- responseText = await this.googleAI.generateOrEditImage(content, requestedModel);
355
- } else if (content.toLowerCase().startsWith("/search:")) {
356
- const query = content.substring(8).trim();
357
- const searchMessages = [{ ...lastMessage, content: query }];
358
- responseText = await this.googleAI.generateContentWithGrounding(searchMessages, requestedModel);
359
- } else {
360
- responseText = await this.googleAI.generateContent(body.messages, requestedModel, false);
361
- }
362
-
363
- const responsePayload = {
364
- id: `chatcmpl-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1000), model: requestedModel,
365
- choices: [{ index: 0, message: { role: "assistant", content: responseText }, finish_reason: "stop" }],
366
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
367
- };
368
- return new Response(JSON.stringify(responsePayload), { headers: { "Content-Type": "application/json" } });
369
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  } catch (error) {
371
- console.error("Error in chat completions:", error.message);
372
- const status = error.message.includes("exceeds the limit") || error.message.includes("Invalid") ? 400 : 500;
373
- return new Response(
374
- JSON.stringify({
375
- error: {
376
- message: error.message,
377
- type: status === 400 ? "invalid_request_error" : "api_error",
378
- code: null
379
- }
380
- }),
381
- { status, headers: { "Content-Type": "application/json" } }
382
- );
383
  }
384
  }
385
 
386
- // [删除] 旧的伪流式函数 streamStringAsOpenAIResponse
387
-
388
- // [新增] 真正的流式响应转换函数
 
389
  private streamGoogleResponseAsOpenAI(googleStream: AsyncGenerator<string>, modelName: string): ReadableStream<Uint8Array> {
390
  const encoder = new TextEncoder();
391
  const streamId = `chatcmpl-${Date.now()}`;
@@ -393,29 +396,22 @@ class OpenAICompatibleServer {
393
 
394
  return new ReadableStream({
395
  async start(controller) {
396
- // 首先发送一个 assistant role ,这是 OpenAI 的惯例
397
  const initialChunk = { id: streamId, object: 'chat.completion.chunk', created: creationTime, model: modelName, choices: [{ index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null }] };
398
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(initialChunk)}\n\n`));
399
 
400
- // 迭代从 Google API 收到的文本块
401
  for await (const textChunk of googleStream) {
402
  if (textChunk) {
403
  const chunk = {
404
- id: streamId,
405
- object: 'chat.completion.chunk',
406
- created: creationTime,
407
- model: modelName,
408
- choices: [{
409
- index: 0,
410
- delta: { content: textChunk }, // 将收到的文本块放入 delta.content
411
- finish_reason: null
412
- }]
413
  };
414
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
415
  }
416
  }
417
 
418
- // 所有数据块发送完毕后,发送结束信号
419
  const finalChunk = { id: streamId, object: 'chat.completion.chunk', created: creationTime, model: modelName, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] };
420
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`));
421
  controller.enqueue(encoder.encode('data: [DONE]\n\n'));
@@ -424,8 +420,31 @@ class OpenAICompatibleServer {
424
  });
425
  }
426
 
427
- private async handleModels(): Promise<Response> { /* ... 保持不变 ... */ return new Response(); }
428
- private async handleStatus(): Promise<Response> { /* ... 保持不变 ... */ return new Response(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
  async handleRequest(request: Request): Promise<Response> {
431
  const corsHeaders = {
@@ -441,19 +460,23 @@ class OpenAICompatibleServer {
441
  const url = new URL(request.url);
442
  let response: Response;
443
 
444
- // 路由处理
445
- if (url.pathname === "/health" || url.pathname === "/status") {
446
- response = await this.handleStatus();
447
- } else if (!this.authenticate(request)) {
448
- response = new Response(JSON.stringify({ error: { message: "Unauthorized" } }), { status: 401 });
449
- } else if (url.pathname === "/v1/audio/speech" && request.method === "POST") {
450
- response = await this.handleAudioSpeech(request);
451
- } else if (url.pathname === "/v1/chat/completions" && request.method === "POST") {
452
- response = await this.handleChatCompletions(request);
453
- } else if (url.pathname === "/v1/models" && request.method === "GET") {
454
- response = await this.handleModels();
455
- } else {
456
- response = new Response("Not Found", { status: 404 });
 
 
 
 
457
  }
458
 
459
  // 为所有响应添加CORS头
@@ -461,32 +484,29 @@ class OpenAICompatibleServer {
461
  for (const [key, value] of Object.entries(corsHeaders)) {
462
  finalHeaders.set(key, value);
463
  }
464
-
465
- return new Response(response.body, { status: response.status, headers: finalHeaders });
466
  }
467
  }
468
 
469
  // --- 服务器启动 ---
470
  const server = new OpenAICompatibleServer();
471
 
472
- console.log("🚀 OpenAI Compatible Server with Google AI starting on port 8000...");
473
  console.log(`✅ Loaded ${server['googleAI'].apiKeys.length} API key(s).`);
474
- console.log(`📄 Max document size set to ${MAX_DOCUMENT_SIZE_MB}MB.`);
475
 
476
- // 启动时预取模型
477
- server['googleAI'].fetchOfficialModels().then(models => {
478
- console.log(`✅ Successfully fetched ${models.length} models from Google AI.`);
479
- }).catch(error => {
480
- console.warn(`⚠️ Could not pre-fetch models: ${error.message}. Will use fallbacks or fetch on first request.`);
481
  });
482
 
 
 
483
  console.log("\n🔗 Endpoints:");
484
- console.log(" POST /v1/chat/completions");
485
- console.log(" POST /v1/audio/speech");
486
- console.log(" GET /v1/models");
487
- console.log(" GET /status");
488
 
489
  await serve(
490
  (request: Request) => server.handleRequest(request),
491
- { port: 7860 }
492
  );
 
1
+ // main.ts
2
  import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
3
 
4
  // --- 常量定义 ---
 
13
  type: string;
14
  text?: string;
15
  image_url?: { url: string };
16
+ document?: { url: string; type: string };
17
  }>;
18
  }
19
 
 
26
  }
27
 
28
  interface OpenAITTSRequest {
29
+ model: string;
30
+ input: string;
31
  voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
32
  response_format?: 'mp3' | 'opus' | 'aac' | 'flac';
33
  speed?: number;
 
62
  return key;
63
  }
64
 
 
65
  private getGoogleVoice(openAIVoice: string): string {
66
  const voiceMap: { [key: string]: string } = {
67
+ 'alloy': 'Kore', 'echo': 'Sal', 'fable': 'Polly', 'onyx': 'Onyx',
68
+ 'nova': 'Sparkle', 'shimmer': 'Luna', 'default': 'Kore'
 
 
 
 
 
69
  };
70
  return voiceMap[openAIVoice] || voiceMap['default'];
71
  }
 
76
  const ttsModel = "gemini-2.5-flash-preview-tts";
77
 
78
  console.log(`Generating speech with model: ${ttsModel}, voice: ${googleVoice} (mapped from OpenAI's '${voice}')`);
 
79
  const requestBody = {
80
  "contents": [{"parts":[{"text": input}]}],
81
+ "generationConfig": {"responseModalities": ["AUDIO"],"speechConfig": {"voiceConfig": {"prebuiltVoiceConfig": {"voiceName": googleVoice}}}},
 
 
 
82
  "model": ttsModel,
83
  };
84
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${ttsModel}:generateContent?key=${apiKey}`,
85
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) });
 
 
 
 
86
  if (!response.ok) {
87
  const errorBody = await response.json().catch(() => response.text());
88
  const errorMessage = errorBody?.error?.message || JSON.stringify(errorBody);
 
89
  throw new Error(`Google TTS API request failed with status ${response.status}: ${errorMessage}`);
90
  }
 
91
  const data = await response.json();
92
  const audioContentBase64 = data.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
93
+ if (!audioContentBase64) throw new Error("No audio data returned from Google API.");
 
 
 
94
  const binaryString = atob(audioContentBase64);
95
+ const bytes = new Uint8Array(binaryString.length);
96
+ for (let i = 0; i < binaryString.length; i++) {
 
97
  bytes[i] = binaryString.charCodeAt(i);
98
  }
99
  return bytes.buffer;
100
  }
101
 
102
+ async fetchOfficialModels(): Promise<any[]> {
 
 
103
  const now = Date.now();
104
+ if (this.cachedModels.length > 0 && (now - this.modelsLastFetch) < MODELS_CACHE_DURATION) return this.cachedModels;
 
 
 
105
  const apiKey = this.getNextApiKey();
106
  try {
107
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
 
 
 
 
108
  if (!response.ok) {
109
  console.warn(`Failed to fetch models from Google AI: ${response.status}. Using fallback models.`);
110
  return this.getFallbackModels();
111
  }
 
112
  const data = await response.json();
113
  if (data.models && Array.isArray(data.models)) {
114
+ this.cachedModels = data.models.filter((model: any) => model.supportedGenerationMethods?.includes('generateContent'));
 
 
115
  this.modelsLastFetch = now;
 
116
  return this.cachedModels;
117
  }
118
  return this.getFallbackModels();
 
122
  }
123
  }
124
 
125
+ private getFallbackModels(): any[] {
126
  return [
127
+ { name: "models/gemini-1.5-pro", displayName: "Gemini 1.5 Pro", description: "Mid-size multimodal model that supports up to 1 million tokens.", supportedGenerationMethods: ["generateContent"], maxTokens: 1000000 },
128
+ { name: "models/gemini-1.5-flash", displayName: "Gemini 1.5 Flash", description: "Fast and versatile multimodal model.", supportedGenerationMethods: ["generateContent"], maxTokens: 1000000 },
129
+ { name: "models/gemini-2.0-flash-preview-image-generation", displayName: "Gemini 2.0 Flash Image Generation", description: "Advanced model for generating images.", supportedGenerationMethods: ["generateContent"], maxTokens: 100000 },
130
+ { name: "models/gemini-2.5-flash-preview-tts", displayName: "Gemini 2.5 Flash TTS", description: "Text-to-speech model.", id: "gemini-2.5-flash-preview-tts" }
131
  ];
132
  }
133
 
134
  public isVisionModel = (modelName: string): boolean => modelName.toLowerCase().includes('vision') || modelName.toLowerCase().includes('pro');
135
+ public isImageGenerationModel = (modelName: string): boolean => modelName.includes('image-generation');
136
+ public isImageEditingModel = (modelName: string): boolean => modelName.includes('image-generation');
137
+ public isDocumentModel = (modelName: string): boolean => modelName.toLowerCase().includes('gemini-1.5');
138
+
139
+ private getDocumentType(url: string): string {
140
+ const lowerUrl = url.toLowerCase();
141
+ if (lowerUrl.startsWith('data:application/pdf') || lowerUrl.includes('.pdf')) return 'pdf';
142
+ if (lowerUrl.startsWith('data:text/plain') || lowerUrl.includes('.txt')) return 'txt';
143
+ if (lowerUrl.startsWith('data:text/markdown') || lowerUrl.includes('.md')) return 'md';
144
+ return 'unknown';
145
+ }
146
 
147
+ private extractDocumentData(documentUrl: string): { mimeType: string; data: string; text?: string; docType: string } {
148
+ const docType = this.getDocumentType(documentUrl);
149
+ if (!documentUrl.startsWith("data:")) throw new Error("Document must be a base64 data URL.");
150
+ const [mimeInfo, base64Data] = documentUrl.split(",");
151
+ if (base64Data.length * 0.75 > MAX_DOCUMENT_SIZE_BYTES) throw new Error(`Document size exceeds ${MAX_DOCUMENT_SIZE_MB}MB.`);
152
+ const mimeType = mimeInfo.split(":")[1]?.split(";")[0] || 'application/octet-stream';
153
+ if (docType === 'txt' || docType === 'md') {
154
+ return { mimeType, data: base64Data, text: atob(base64Data), docType };
155
+ }
156
+ return { mimeType: docType === 'pdf' ? 'application/pdf' : mimeType, data: base64Data, docType };
157
+ }
158
+
159
+ private extractImageData(imageUrl: string): { mimeType: string; data: string } {
160
+ if (imageUrl.startsWith("data:image/")) {
161
+ const [mimeInfo, base64Data] = imageUrl.split(",");
162
+ return { mimeType: mimeInfo.split(":")[1].split(";")[0], data: base64Data };
163
+ } else if (imageUrl.startsWith("http")) {
164
+ throw new Error("URL images are not supported. Please use base64 data URLs.");
165
+ }
166
+ return { mimeType: "image/jpeg", data: imageUrl };
167
+ }
168
 
169
+ private buildGoogleContent(messages: OpenAIMessage[]) {
170
+ return messages.map(msg => {
171
+ const role = msg.role === "assistant" ? "model" : "user";
172
+ if (typeof msg.content === "string") {
173
+ return { role, parts: [{ text: msg.content }] };
174
+ }
175
+ const parts = msg.content.map(part => {
176
+ if (part.type === "text") return { text: part.text };
177
+ if (part.type === "image_url" && part.image_url) {
178
+ const { mimeType, data } = this.extractImageData(part.image_url.url);
179
+ return { inlineData: { mimeType, data } };
180
+ }
181
+ if (part.type === "document" && part.document) {
182
+ const docData = this.extractDocumentData(part.document.url);
183
+ if (docData.docType === 'txt' || docData.docType === 'md') {
184
+ return { text: `${docData.docType === 'md' ? 'Markdown' : 'Text'} document content:\n${docData.text}` };
185
+ }
186
+ if (docData.docType === 'pdf') {
187
+ return { inlineData: { mimeType: docData.mimeType, data: docData.data } };
188
+ }
189
+ }
190
+ return { text: "" };
191
+ });
192
+ return { role, parts: parts.filter(p => p && (p.text || p.inlineData)) };
193
+ });
194
+ }
195
 
196
+ async generateContent(messages: OpenAIMessage[], modelName: string): Promise<string> {
 
 
 
 
197
  const apiKey = this.getNextApiKey();
198
  const fullModelName = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
199
+ const contents = this.buildGoogleContent(messages);
200
+ const requestBody = { contents, generationConfig: { temperature: 0.7, maxOutputTokens: 8192 } };
201
+
202
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/${fullModelName}:generateContent?key=${apiKey}`,
203
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) });
204
+
205
+ if (!response.ok) {
206
+ const errorText = await response.text();
207
+ throw new Error(`Google AI API error: ${response.status} - ${errorText}`);
208
+ }
209
  const data = await response.json();
210
+ if (data.promptFeedback?.blockReason) {
211
+ throw new Error(`Request blocked by Google. Reason: ${data.promptFeedback.blockReason}`);
212
+ }
213
+ const candidate = data.candidates?.[0];
214
+ if (!candidate) throw new Error("No response generated from Google AI.");
215
+ if (candidate.finishReason === "SAFETY") throw new Error("Response blocked for safety reasons.");
216
+ return candidate.content?.parts?.[0]?.text || "No text response generated.";
217
  }
218
 
 
 
219
  /**
220
+ * [新增] 真正的流式内容生成函数
221
+ * 使用 Google streamGenerateContent 端点进行流式响应处理
 
 
 
222
  */
223
  async * streamGenerateContent(messages: OpenAIMessage[], modelName: string): AsyncGenerator<string> {
224
  const apiKey = this.getNextApiKey();
225
  const fullModelName = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
226
+ const contents = this.buildGoogleContent(messages);
227
+ const requestBody = { contents, generationConfig: { temperature: 0.7, maxOutputTokens: 8192 } };
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  // [关键] 使用 :streamGenerateContent 端点
230
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/${fullModelName}:streamGenerateContent?key=${apiKey}`,
231
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) });
 
 
 
 
 
 
232
 
233
  if (!response.ok || !response.body) {
234
  const errorText = await response.text();
235
+ throw new Error(`Google AI streaming API error: ${response.status} - ${errorText}`);
236
  }
237
+
238
+ // [关键] 读取并解析流
239
  const reader = response.body.getReader();
240
  const decoder = new TextDecoder();
241
  let buffer = "";
 
246
 
247
  buffer += decoder.decode(value, { stream: true });
248
 
249
+ // Google 的流式响应可能在一个数据包里包含多个以 "data: " 开头的 JSON 对象
250
+ const lines = buffer.split('\n');
251
+ buffer = lines.pop() || ''; // Keep the last, possibly incomplete, line in buffer
 
 
 
252
 
253
+ for (const line of lines) {
254
  if (line.startsWith('data: ')) {
255
  try {
256
+ const jsonStr = line.substring(6);
257
  const chunk = JSON.parse(jsonStr);
258
 
259
+ if (chunk.error) throw new Error(`Google API stream error: ${chunk.error.message}`);
 
 
260
 
261
  const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
262
  if (text) {
263
+ yield text; // 产生一个文本块
264
  }
265
  } catch (e) {
266
  console.warn("Could not parse stream chunk:", line, e.message);
 
270
  }
271
  }
272
 
273
+ async generateOrEditImage(prompt: string, modelName: string, inputImages?: any[]): Promise<string> {
274
+ const apiKey = this.getNextApiKey();
275
+ const fullModelName = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
276
+ const requestParts: any[] = [{ text: prompt }];
277
+ let inputImage;
278
+ if (inputImages && inputImages.length > 0) {
279
+ inputImage = this.extractImageData(inputImages[0].url);
280
+ requestParts.push({ inline_data: { mime_type: inputImage.mimeType, data: inputImage.data } });
281
+ }
282
+
283
+ const requestBody = {
284
+ contents: [{ parts: requestParts }],
285
+ generationConfig: { responseModalities: ["TEXT", "IMAGE"], temperature: 0.7 }
286
+ };
287
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/${fullModelName}:generateContent?key=${apiKey}`,
288
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) });
289
+
290
+ if (!response.ok) throw new Error(`Image processing failed: ${response.status} - ${await response.text()}`);
291
+ const data = await response.json();
292
+ const candidate = data.candidates?.[0];
293
+ if (!candidate) throw new Error("No image response from Google.");
294
+ if (candidate.finishReason === "SAFETY") throw new Error("Image request blocked for safety reasons.");
295
+
296
+ let textResponse = "";
297
+ let imageBase64 = "";
298
+ (candidate.content?.parts || []).forEach((part: any) => {
299
+ if (part.text) textResponse += part.text;
300
+ if (part.inlineData?.data || part.inline_data?.data) {
301
+ imageBase64 = part.inlineData?.data || part.inline_data.data;
302
+ }
303
+ });
304
 
305
+ let result = "";
306
+ if (textResponse) result += textResponse + "\\\\n\\\\n";
307
+ if (imageBase64) result += `${inputImage ? 'Edited' : 'Generated'} image:\\\\n"data:image/png;base64,${imageBase64}"`;
308
+ return result || "Image processing complete.";
309
+ }
310
+ }
311
 
312
  class OpenAICompatibleServer {
313
  private googleAI: GoogleAIService;
 
318
  this.authKey = Deno.env.get("AUTH_KEY") || "";
319
  }
320
 
321
+ private authenticate(request: Request): boolean {
322
+ if (!this.authKey) return true;
323
+ return request.headers.get("Authorization")?.replace("Bearer ", "") === this.authKey;
324
+ }
325
 
326
+ private isDocumentContent(url?: string): boolean {
327
+ if (!url) return false;
328
+ const lowerUrl = url.toLowerCase();
329
+ return lowerUrl.includes('.pdf') || lowerUrl.startsWith('data:application/pdf') ||
330
+ lowerUrl.includes('.txt') || lowerUrl.startsWith('data:text/plain') ||
331
+ lowerUrl.includes('.md') || lowerUrl.startsWith('data:text/markdown');
332
+ }
333
+
334
+ private async handleAudioSpeech(request: Request): Promise<Response> {
335
+ const body: OpenAITTSRequest = await request.json();
336
+ if (!body.input || !body.voice || !body.model) {
337
+ return new Response(JSON.stringify({ error: "Missing required fields: input, voice, model." }), { status: 400 });
338
+ }
339
+ const audioBuffer = await this.googleAI.generateSpeech(body.input, body.model, body.voice);
340
+ return new Response(audioBuffer, { headers: { "Content-Type": "audio/mpeg" } });
341
+ }
342
+
343
  private async handleChatCompletions(request: Request): Promise<Response> {
344
  try {
345
  const body: OpenAIRequest = await request.json();
346
  const requestedModel = body.model || "gemini-1.5-pro";
 
 
347
 
348
+ // [修改] 流式请求处理
349
+ if (body.stream) {
350
  const googleStream = this.googleAI.streamGenerateContent(body.messages, requestedModel);
351
  const openAIStream = this.streamGoogleResponseAsOpenAI(googleStream, requestedModel);
352
  return new Response(openAIStream, {
353
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }
354
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
+
357
+ // [保持不变] 非流式请求处理
358
+ const lastMessage = body.messages[body.messages.length - 1];
359
+ const content = typeof lastMessage.content === "string" ? lastMessage.content : (Array.isArray(lastMessage.content) ? lastMessage.content.map(p => p.text || "").join(" ") : "");
360
+ const hasDocument = body.messages.some(msg => Array.isArray(msg.content) && msg.content.some(part => part.type === "document" || this.isDocumentContent(part.document?.url)));
361
+ const hasImages = body.messages.some(msg => Array.isArray(msg.content) && msg.content.some(part => part.type === "image_url"));
362
+
363
+ let responseText: string;
364
+ if (this.googleAI.isImageGenerationModel(requestedModel)) {
365
+ const inputImages = hasImages ? body.messages.flatMap(msg => Array.isArray(msg.content) ? msg.content.filter(p => p.type === 'image_url' && p.image_url).map(p => ({ url: p.image_url!.url })) : []) : undefined;
366
+ responseText = await this.googleAI.generateOrEditImage(content, requestedModel, inputImages);
367
+ } else if(hasDocument || hasImages) { // Vision/Document models
368
+ responseText = await this.googleAI.generateContent(body.messages, requestedModel);
369
+ } else { // Standard text
370
+ responseText = await this.googleAI.generateContent(body.messages, requestedModel);
371
+ }
372
+
373
+ const responsePayload = {
374
+ id: `chatcmpl-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1000), model: requestedModel,
375
+ choices: [{ index: 0, message: { role: "assistant", content: responseText }, finish_reason: "stop" }],
376
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
377
+ };
378
+ return new Response(JSON.stringify(responsePayload), { headers: { "Content-Type": "application/json" } });
379
+
380
  } catch (error) {
381
+ console.error("Error in chat completions:", error.message);
382
+ const status = error.message.includes("exceeds") || error.message.includes("Invalid") ? 400 : 500;
383
+ return new Response(JSON.stringify({ error: { message: error.message, type: "api_error" } }),
384
+ { status, headers: { "Content-Type": "application/json" } });
 
 
 
 
 
 
 
 
385
  }
386
  }
387
 
388
+ /**
389
+ * [新增] 真正的流式响应转换函数
390
+ * Google 数据流转换为 OpenAI 兼容的 Server-Sent Events 流
391
+ */
392
  private streamGoogleResponseAsOpenAI(googleStream: AsyncGenerator<string>, modelName: string): ReadableStream<Uint8Array> {
393
  const encoder = new TextEncoder();
394
  const streamId = `chatcmpl-${Date.now()}`;
 
396
 
397
  return new ReadableStream({
398
  async start(controller) {
399
+ // 发送一个包含角色数据
400
  const initialChunk = { id: streamId, object: 'chat.completion.chunk', created: creationTime, model: modelName, choices: [{ index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null }] };
401
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(initialChunk)}\n\n`));
402
 
403
+ // 迭代从 Google API 收到的文本块并转发
404
  for await (const textChunk of googleStream) {
405
  if (textChunk) {
406
  const chunk = {
407
+ id: streamId, object: 'chat.completion.chunk', created: creationTime, model: modelName,
408
+ choices: [{ index: 0, delta: { content: textChunk }, finish_reason: null }]
 
 
 
 
 
 
 
409
  };
410
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
411
  }
412
  }
413
 
414
+ // 发送结束信号
415
  const finalChunk = { id: streamId, object: 'chat.completion.chunk', created: creationTime, model: modelName, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] };
416
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`));
417
  controller.enqueue(encoder.encode('data: [DONE]\n\n'));
 
420
  });
421
  }
422
 
423
+ private async handleModels(): Promise<Response> {
424
+ const googleModels = await this.googleAI.fetchOfficialModels();
425
+ const fallbackModels = this.googleAI['getFallbackModels']();
426
+
427
+ const uniqueModelMap = new Map();
428
+ [...googleModels, ...fallbackModels].forEach(model => {
429
+ const modelId = model.id || model.name.replace('models/', '');
430
+ if (!uniqueModelMap.has(modelId)) {
431
+ uniqueModelMap.set(modelId, {
432
+ id: modelId, object: "model", created: Math.floor(Date.now() / 1000), owned_by: "google"
433
+ });
434
+ }
435
+ });
436
+ return new Response(JSON.stringify({ object: "list", data: Array.from(uniqueModelMap.values()) }), { headers: { "Content-Type": "application/json" } });
437
+ }
438
+
439
+ private async handleStatus(): Promise<Response> {
440
+ const status = {
441
+ status: "healthy", timestamp: new Date().toISOString(), version: "2.5.1",
442
+ api_keys_loaded: this.googleAI.apiKeys.length,
443
+ models_in_cache: this.googleAI.cachedModels.length,
444
+ models_last_fetched: this.googleAI.modelsLastFetch > 0 ? new Date(this.googleAI.modelsLastFetch).toISOString() : "never"
445
+ };
446
+ return new Response(JSON.stringify(status), { headers: { "Content-Type": "application/json" } });
447
+ }
448
 
449
  async handleRequest(request: Request): Promise<Response> {
450
  const corsHeaders = {
 
460
  const url = new URL(request.url);
461
  let response: Response;
462
 
463
+ try {
464
+ if (url.pathname === "/health" || url.pathname === "/status") {
465
+ response = await this.handleStatus();
466
+ } else if (!this.authenticate(request)) {
467
+ response = new Response(JSON.stringify({ error: { message: "Unauthorized" } }), { status: 401 });
468
+ } else if (url.pathname === "/v1/audio/speech" && request.method === "POST") {
469
+ response = await this.handleAudioSpeech(request);
470
+ } else if (url.pathname === "/v1/chat/completions" && request.method === "POST") {
471
+ response = await this.handleChatCompletions(request);
472
+ } else if (url.pathname === "/v1/models" && request.method === "GET") {
473
+ response = await this.handleModels();
474
+ } else {
475
+ response = new Response("Not Found", { status: 404 });
476
+ }
477
+ } catch (error) {
478
+ console.error("Unhandled error:", error);
479
+ response = new Response(JSON.stringify({ error: { message: error.message || "An internal server error occurred." } }), { status: 500 });
480
  }
481
 
482
  // 为所有响应添加CORS头
 
484
  for (const [key, value] of Object.entries(corsHeaders)) {
485
  finalHeaders.set(key, value);
486
  }
487
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers: finalHeaders });
 
488
  }
489
  }
490
 
491
  // --- 服务器启动 ---
492
  const server = new OpenAICompatibleServer();
493
 
494
+ console.log("🚀 OpenAI Compatible Server with Google AI starting...");
495
  console.log(`✅ Loaded ${server['googleAI'].apiKeys.length} API key(s).`);
 
496
 
497
+ server['googleAI'].fetchOfficialModels().catch(error => {
498
+ console.warn(`⚠️ Could not pre-fetch models: ${error.message}. Will use fallbacks.`);
 
 
 
499
  });
500
 
501
+ const port = 7860;
502
+ console.log(`Server listening on http://localhost:${port}`);
503
  console.log("\n🔗 Endpoints:");
504
+ console.log(` POST /v1/chat/completions`);
505
+ console.log(` POST /v1/audio/speech`);
506
+ console.log(` GET /v1/models`);
507
+ console.log(` GET /status`);
508
 
509
  await serve(
510
  (request: Request) => server.handleRequest(request),
511
+ { port 7860 }
512
  );