tanbushi commited on
Commit
ab3f32d
·
1 Parent(s): 1911aee
Files changed (2) hide show
  1. app.py +170 -1
  2. worker.mjs +452 -0
app.py CHANGED
@@ -1,5 +1,173 @@
1
  # uvicorn app:app --host 0.0.0.0 --port 7860 --reload
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi import FastAPI, Request, HTTPException, Response
4
  import re, json
5
  import httpx
@@ -32,9 +200,10 @@ async def proxy_url(dest_url: str, request: Request):
32
 
33
  async with httpx.AsyncClient() as client:
34
  try:
 
35
  # 向目标 URL 发送 POST 请求
36
  response = await client.post(dest_url, content=body, headers=headers)
37
-
38
  # 检查响应状态码
39
  if response.status_code == 200:
40
  # 检查响应内容类型是否为 JSON
 
1
  # uvicorn app:app --host 0.0.0.0 --port 7860 --reload
2
 
3
+ from fastapi import FastAPI, Request, HTTPException, Response
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import re, json
6
+ import httpx
7
+ import uuid
8
+
9
+ app = FastAPI()
10
+
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["*"],
14
+ allow_credentials=True,
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ @app.get("/")
20
+ def greet_json():
21
+ return {"Hello": "World!"}
22
+
23
+ @app.get("/v1/{dest_url:path}")
24
+ async def gre_dest_url(dest_url: str):
25
+ return dest_url
26
+
27
+ API_CLIENT = "genai-js/0.21.0"
28
+ DEFAULT_MODEL = "gemini-1.5-pro-latest"
29
+
30
+ async def transform_request(req: dict):
31
+ # This is a placeholder, implement the transformation logic as needed
32
+ return req
33
+
34
+ async def process_completions_response(data: dict, model: str, id: str):
35
+ # Process the response to match the OpenAI format
36
+ choices = []
37
+ if "candidates" in data:
38
+ for i, candidate in enumerate(data["candidates"]):
39
+ message = {}
40
+ if "content" in candidate:
41
+ message["content"] = candidate["content"]
42
+ else:
43
+ message["content"] = ""
44
+ message["role"] = "assistant"
45
+ choices.append({
46
+ "finish_reason": candidate.get("finishReason", "stop"),
47
+ "index": i,
48
+ "message": message
49
+ })
50
+
51
+ usage = {}
52
+ if "usageMetadata" in data:
53
+ usage = {
54
+ "completion_tokens": data["usageMetadata"].get("tokenCount", 0),
55
+ "prompt_tokens": 0, # This value is not available in the response
56
+ "total_tokens": data["usageMetadata"].get("tokenCount", 0)
57
+ }
58
+
59
+ response_data = {
60
+ "id": id,
61
+ "choices": choices,
62
+ "created": 1678787675, # Replace with actual timestamp if available
63
+ "model": model,
64
+ "object": "chat.completion",
65
+ "usage": usage
66
+ }
67
+ return json.dumps(response_data, ensure_ascii=False)
68
+
69
+ @app.post("/v1/{dest_url:path}")
70
+ async def proxy_url(dest_url: str, request: Request):
71
+ body = await request.body()
72
+ headers = dict(request.headers)
73
+
74
+ # Remove Content-Length and Host headers
75
+ if 'content-length' in headers:
76
+ del headers['content-length']
77
+ if 'host' in headers:
78
+ del headers['host']
79
+
80
+ # Extract API key from Authorization header
81
+ auth = headers.get("Authorization")
82
+ api_key = auth.split(" ")[1] if auth else None
83
+
84
+ # Set required headers
85
+ headers["x-goog-api-client"] = API_CLIENT
86
+ if api_key:
87
+ headers["x-goog-api-key"] = api_key
88
+ headers['Content-Type'] = 'application/json'
89
+ #if 'user-agent' in headers:
90
+ # del headers['user-agent']
91
+
92
+ dest_url = re.sub('/', '://', dest_url, count=1)
93
+
94
+ # Modify dest_url based on the endpoint
95
+ if dest_url.endswith("/chat/completions"):
96
+ model = DEFAULT_MODEL
97
+ req_body = json.loads(body.decode('utf-8'))
98
+ if 'model' in req_body:
99
+ model = req_body['model']
100
+ if model.startswith("models/"):
101
+ model = model[7:]
102
+ TASK = "generateContent"
103
+ url = f"{dest_url.rsplit('/', 1)[0]}/{model}:{TASK}"
104
+
105
+ async with httpx.AsyncClient() as client:
106
+ try:
107
+ # Forward the modified request
108
+ response = await client.post(url, content=body, headers=headers)
109
+
110
+ # Check response status code
111
+ if response.status_code == 200:
112
+ # Process JSON response
113
+ if 'application/json' in response.headers.get('content-type', ''):
114
+ json_response = response.json()
115
+ json_response['id'] = f"chatcmpl-{uuid.uuid4()}"
116
+ processed_response = await process_completions_response(json_response, model, json_response['id'])
117
+ resp = Response(content=processed_response, media_type="application/json")
118
+ resp.headers["Access-Control-Allow-Origin"] = "*"
119
+ return resp
120
+ else:
121
+ return {"error": "Response is not in JSON format", "id": f"chatcmpl-{uuid.uuid4()}"}
122
+ else:
123
+ # Convert error response to JSON format and return to the client
124
+ try:
125
+ error_data = response.json()
126
+ error_data['id'] = f"chatcmpl-{uuid.uuid4()}"
127
+ except ValueError:
128
+ error_data = {"status_code": response.status_code, "detail": response.text, "id": f"chatcmpl-{uuid.uuid4()}"}
129
+ print(f"Error response: {error_data}")
130
+ resp = Response(content=json.dumps(error_data, ensure_ascii=False), media_type="application/json")
131
+ resp.headers["Access-Control-Allow-Origin"] = "*"
132
+ return resp
133
+
134
+ except httpx.RequestError as e:
135
+ # Handle request errors
136
+ print(f"Request error: {e}")
137
+ raise HTTPException(status_code=500, detail=str(e))
138
+ else:
139
+ async with httpx.AsyncClient() as client:
140
+ try:
141
+ # Forward the original request
142
+ response = await client.post(dest_url, content=body, headers=headers)
143
+
144
+ # Check response status code
145
+ if response.status_code == 200:
146
+ # Process JSON response
147
+ if 'application/json' in response.headers.get('content-type', ''):
148
+ json_response = response.json()
149
+ json_response['id'] = f"chatcmpl-{uuid.uuid4()}"
150
+ resp = Response(content=json.dumps(json_response), media_type="application/json")
151
+ resp.headers["Access-Control-Allow-Origin"] = "*"
152
+ return resp
153
+ else:
154
+ return {"error": "Response is not in JSON format", "id": f"chatcmpl-{uuid.uuid4()}"}
155
+ else:
156
+ # Convert error response to JSON format and return to the client
157
+ try:
158
+ error_data = response.json()
159
+ error_data['id'] = f"chatcmpl-{uuid.uuid4()}"
160
+ except ValueError:
161
+ error_data = {"status_code": response.status_code, "detail": response.text, "id": f"chatcmpl-{uuid.uuid4()}"}
162
+ resp = Response(content=json.dumps(error_data), media_type="application/json")
163
+ resp.headers["Access-Control-Allow-Origin"] = "*"
164
+ return resp
165
+
166
+ except httpx.RequestError as e:
167
+ # Handle request errors
168
+ raise HTTPException(status_code=500, detail=str(e))
169
+ # uvicorn app:app --host 0.0.0.0 --port 7860 --reload
170
+
171
  from fastapi import FastAPI, Request, HTTPException, Response
172
  import re, json
173
  import httpx
 
200
 
201
  async with httpx.AsyncClient() as client:
202
  try:
203
+ print(f"Request Headers: {headers}")
204
  # 向目标 URL 发送 POST 请求
205
  response = await client.post(dest_url, content=body, headers=headers)
206
+
207
  # 检查响应状态码
208
  if response.status_code == 200:
209
  # 检查响应内容类型是否为 JSON
worker.mjs ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Buffer } from "node:buffer";
2
+
3
+ export default {
4
+ async fetch (request) {
5
+ if (request.method === "OPTIONS") {
6
+ return handleOPTIONS();
7
+ }
8
+ const errHandler = (err) => {
9
+ console.error(err);
10
+ return new Response(err.message, fixCors({ status: err.status ?? 500 }));
11
+ };
12
+ try {
13
+ const auth = request.headers.get("Authorization");
14
+ const apiKey = auth?.split(" ")[1];
15
+ const assert = (success) => {
16
+ if (!success) {
17
+ throw new HttpError("The specified HTTP method is not allowed for the requested resource", 400);
18
+ }
19
+ };
20
+ const { pathname } = new URL(request.url);
21
+ switch (true) {
22
+ case pathname.endsWith("/chat/completions"):
23
+ assert(request.method === "POST");
24
+ return handleCompletions(await request.json(), apiKey)
25
+ .catch(errHandler);
26
+ case pathname.endsWith("/embeddings"):
27
+ assert(request.method === "POST");
28
+ return handleEmbeddings(await request.json(), apiKey)
29
+ .catch(errHandler);
30
+ case pathname.endsWith("/models"):
31
+ assert(request.method === "GET");
32
+ return handleModels(apiKey)
33
+ .catch(errHandler);
34
+ default:
35
+ throw new HttpError("404 Not Found", 404);
36
+ }
37
+ } catch (err) {
38
+ return errHandler(err);
39
+ }
40
+ }
41
+ };
42
+
43
+ class HttpError extends Error {
44
+ constructor(message, status) {
45
+ super(message);
46
+ this.name = this.constructor.name;
47
+ this.status = status;
48
+ }
49
+ }
50
+
51
+ const fixCors = ({ headers, status, statusText }) => {
52
+ headers = new Headers(headers);
53
+ headers.set("Access-Control-Allow-Origin", "*");
54
+ return { headers, status, statusText };
55
+ };
56
+
57
+ const handleOPTIONS = async () => {
58
+ return new Response(null, {
59
+ headers: {
60
+ "Access-Control-Allow-Origin": "*",
61
+ "Access-Control-Allow-Methods": "*",
62
+ "Access-Control-Allow-Headers": "*",
63
+ }
64
+ });
65
+ };
66
+
67
+ const BASE_URL = "https://generativelanguage.googleapis.com";
68
+ const API_VERSION = "v1beta";
69
+
70
+ // https://github.com/google-gemini/generative-ai-js/blob/cf223ff4a1ee5a2d944c53cddb8976136382bee6/src/requests/request.ts#L71
71
+ const API_CLIENT = "genai-js/0.21.0"; // npm view @google/generative-ai version
72
+ const makeHeaders = (apiKey, more) => ({
73
+ "x-goog-api-client": API_CLIENT,
74
+ ...(apiKey && { "x-goog-api-key": apiKey }),
75
+ ...more
76
+ });
77
+
78
+ async function handleModels (apiKey) {
79
+ const response = await fetch(`${BASE_URL}/${API_VERSION}/models`, {
80
+ headers: makeHeaders(apiKey),
81
+ });
82
+ let { body } = response;
83
+ if (response.ok) {
84
+ const { models } = JSON.parse(await response.text());
85
+ body = JSON.stringify({
86
+ object: "list",
87
+ data: models.map(({ name }) => ({
88
+ id: name.replace("models/", ""),
89
+ object: "model",
90
+ created: 0,
91
+ owned_by: "",
92
+ })),
93
+ }, null, " ");
94
+ }
95
+ return new Response(body, fixCors(response));
96
+ }
97
+
98
+ const DEFAULT_EMBEDDINGS_MODEL = "text-embedding-004";
99
+ async function handleEmbeddings (req, apiKey) {
100
+ if (typeof req.model !== "string") {
101
+ throw new HttpError("model is not specified", 400);
102
+ }
103
+ if (!Array.isArray(req.input)) {
104
+ req.input = [ req.input ];
105
+ }
106
+ let model;
107
+ if (req.model.startsWith("models/")) {
108
+ model = req.model;
109
+ } else {
110
+ req.model = DEFAULT_EMBEDDINGS_MODEL;
111
+ model = "models/" + req.model;
112
+ }
113
+ const response = await fetch(`${BASE_URL}/${API_VERSION}/${model}:batchEmbedContents`, {
114
+ method: "POST",
115
+ headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
116
+ body: JSON.stringify({
117
+ "requests": req.input.map(text => ({
118
+ model,
119
+ content: { parts: { text } },
120
+ outputDimensionality: req.dimensions,
121
+ }))
122
+ })
123
+ });
124
+ let { body } = response;
125
+ if (response.ok) {
126
+ const { embeddings } = JSON.parse(await response.text());
127
+ body = JSON.stringify({
128
+ object: "list",
129
+ data: embeddings.map(({ values }, index) => ({
130
+ object: "embedding",
131
+ index,
132
+ embedding: values,
133
+ })),
134
+ model: req.model,
135
+ }, null, " ");
136
+ }
137
+ return new Response(body, fixCors(response));
138
+ }
139
+
140
+ const DEFAULT_MODEL = "gemini-1.5-pro-latest";
141
+ async function handleCompletions (req, apiKey) {
142
+ let model = DEFAULT_MODEL;
143
+ switch(true) {
144
+ case typeof req.model !== "string":
145
+ break;
146
+ case req.model.startsWith("models/"):
147
+ model = req.model.substring(7);
148
+ break;
149
+ case req.model.startsWith("gemini-"):
150
+ case req.model.startsWith("learnlm-"):
151
+ model = req.model;
152
+ }
153
+ const TASK = req.stream ? "streamGenerateContent" : "generateContent";
154
+ let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`;
155
+ if (req.stream) { url += "?alt=sse"; }
156
+ const response = await fetch(url, {
157
+ method: "POST",
158
+ headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
159
+ body: JSON.stringify(await transformRequest(req)), // try
160
+ });
161
+
162
+ let body = response.body;
163
+ if (response.ok) {
164
+ let id = generateChatcmplId(); //"chatcmpl-8pMMaqXMK68B3nyDBrapTDrhkHBQK";
165
+ if (req.stream) {
166
+ body = response.body
167
+ .pipeThrough(new TextDecoderStream())
168
+ .pipeThrough(new TransformStream({
169
+ transform: parseStream,
170
+ flush: parseStreamFlush,
171
+ buffer: "",
172
+ }))
173
+ .pipeThrough(new TransformStream({
174
+ transform: toOpenAiStream,
175
+ flush: toOpenAiStreamFlush,
176
+ streamIncludeUsage: req.stream_options?.include_usage,
177
+ model, id, last: [],
178
+ }))
179
+ .pipeThrough(new TextEncoderStream());
180
+ } else {
181
+ body = await response.text();
182
+ body = processCompletionsResponse(JSON.parse(body), model, id);
183
+ }
184
+ }
185
+ return new Response(body, fixCors(response));
186
+ }
187
+
188
+ const harmCategory = [
189
+ "HARM_CATEGORY_HATE_SPEECH",
190
+ "HARM_CATEGORY_SEXUALLY_EXPLICIT",
191
+ "HARM_CATEGORY_DANGEROUS_CONTENT",
192
+ "HARM_CATEGORY_HARASSMENT",
193
+ "HARM_CATEGORY_CIVIC_INTEGRITY",
194
+ ];
195
+ const safetySettings = harmCategory.map(category => ({
196
+ category,
197
+ threshold: "BLOCK_NONE",
198
+ }));
199
+ const fieldsMap = {
200
+ stop: "stopSequences",
201
+ n: "candidateCount", // not for streaming
202
+ max_tokens: "maxOutputTokens",
203
+ max_completion_tokens: "maxOutputTokens",
204
+ temperature: "temperature",
205
+ top_p: "topP",
206
+ top_k: "topK", // non-standard
207
+ frequency_penalty: "frequencyPenalty",
208
+ presence_penalty: "presencePenalty",
209
+ };
210
+ const transformConfig = (req) => {
211
+ let cfg = {};
212
+ //if (typeof req.stop === "string") { req.stop = [req.stop]; } // no need
213
+ for (let key in req) {
214
+ const matchedKey = fieldsMap[key];
215
+ if (matchedKey) {
216
+ cfg[matchedKey] = req[key];
217
+ }
218
+ }
219
+ if (req.response_format) {
220
+ switch(req.response_format.type) {
221
+ case "json_schema":
222
+ cfg.responseSchema = req.response_format.json_schema?.schema;
223
+ if (cfg.responseSchema && "enum" in cfg.responseSchema) {
224
+ cfg.responseMimeType = "text/x.enum";
225
+ break;
226
+ }
227
+ // eslint-disable-next-line no-fallthrough
228
+ case "json_object":
229
+ cfg.responseMimeType = "application/json";
230
+ break;
231
+ case "text":
232
+ cfg.responseMimeType = "text/plain";
233
+ break;
234
+ default:
235
+ throw new HttpError("Unsupported response_format.type", 400);
236
+ }
237
+ }
238
+ return cfg;
239
+ };
240
+
241
+ const parseImg = async (url) => {
242
+ let mimeType, data;
243
+ if (url.startsWith("http://") || url.startsWith("https://")) {
244
+ try {
245
+ const response = await fetch(url);
246
+ if (!response.ok) {
247
+ throw new Error(`${response.status} ${response.statusText} (${url})`);
248
+ }
249
+ mimeType = response.headers.get("content-type");
250
+ data = Buffer.from(await response.arrayBuffer()).toString("base64");
251
+ } catch (err) {
252
+ throw new Error("Error fetching image: " + err.toString());
253
+ }
254
+ } else {
255
+ const match = url.match(/^data:(?<mimeType>.*?)(;base64)?,(?<data>.*)$/);
256
+ if (!match) {
257
+ throw new Error("Invalid image data: " + url);
258
+ }
259
+ ({ mimeType, data } = match.groups);
260
+ }
261
+ return {
262
+ inlineData: {
263
+ mimeType,
264
+ data,
265
+ },
266
+ };
267
+ };
268
+
269
+ const transformMsg = async ({ role, content }) => {
270
+ const parts = [];
271
+ if (!Array.isArray(content)) {
272
+ // system, user: string
273
+ // assistant: string or null (Required unless tool_calls is specified.)
274
+ parts.push({ text: content });
275
+ return { role, parts };
276
+ }
277
+ // user:
278
+ // An array of content parts with a defined type.
279
+ // Supported options differ based on the model being used to generate the response.
280
+ // Can contain text, image, or audio inputs.
281
+ for (const item of content) {
282
+ switch (item.type) {
283
+ case "text":
284
+ parts.push({ text: item.text });
285
+ break;
286
+ case "image_url":
287
+ parts.push(await parseImg(item.image_url.url));
288
+ break;
289
+ case "input_audio":
290
+ parts.push({
291
+ inlineData: {
292
+ mimeType: "audio/" + item.input_audio.format,
293
+ data: item.input_audio.data,
294
+ }
295
+ });
296
+ break;
297
+ default:
298
+ throw new TypeError(`Unknown "content" item type: "${item.type}"`);
299
+ }
300
+ }
301
+ if (content.every(item => item.type === "image_url")) {
302
+ parts.push({ text: "" }); // to avoid "Unable to submit request because it must have a text parameter"
303
+ }
304
+ return { role, parts };
305
+ };
306
+
307
+ const transformMessages = async (messages) => {
308
+ if (!messages) { return; }
309
+ const contents = [];
310
+ let system_instruction;
311
+ for (const item of messages) {
312
+ if (item.role === "system") {
313
+ delete item.role;
314
+ system_instruction = await transformMsg(item);
315
+ } else {
316
+ item.role = item.role === "assistant" ? "model" : "user";
317
+ contents.push(await transformMsg(item));
318
+ }
319
+ }
320
+ if (system_instruction && contents.length === 0) {
321
+ contents.push({ role: "model", parts: { text: " " } });
322
+ }
323
+ //console.info(JSON.stringify(contents, 2));
324
+ return { system_instruction, contents };
325
+ };
326
+
327
+ const transformRequest = async (req) => ({
328
+ ...await transformMessages(req.messages),
329
+ safetySettings,
330
+ generationConfig: transformConfig(req),
331
+ });
332
+
333
+ const generateChatcmplId = () => {
334
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
335
+ const randomChar = () => characters[Math.floor(Math.random() * characters.length)];
336
+ return "chatcmpl-" + Array.from({ length: 29 }, randomChar).join("");
337
+ };
338
+
339
+ const reasonsMap = { //https://ai.google.dev/api/rest/v1/GenerateContentResponse#finishreason
340
+ //"FINISH_REASON_UNSPECIFIED": // Default value. This value is unused.
341
+ "STOP": "stop",
342
+ "MAX_TOKENS": "length",
343
+ "SAFETY": "content_filter",
344
+ "RECITATION": "content_filter",
345
+ //"OTHER": "OTHER",
346
+ // :"function_call",
347
+ };
348
+ const SEP = "\n\n|>";
349
+ const transformCandidates = (key, cand) => ({
350
+ index: cand.index || 0, // 0-index is absent in new -002 models response
351
+ [key]: {
352
+ role: "assistant",
353
+ content: cand.content?.parts.map(p => p.text).join(SEP) },
354
+ logprobs: null,
355
+ finish_reason: reasonsMap[cand.finishReason] || cand.finishReason,
356
+ });
357
+ const transformCandidatesMessage = transformCandidates.bind(null, "message");
358
+ const transformCandidatesDelta = transformCandidates.bind(null, "delta");
359
+
360
+ const transformUsage = (data) => ({
361
+ completion_tokens: data.candidatesTokenCount,
362
+ prompt_tokens: data.promptTokenCount,
363
+ total_tokens: data.totalTokenCount
364
+ });
365
+
366
+ const processCompletionsResponse = (data, model, id) => {
367
+ return JSON.stringify({
368
+ id,
369
+ choices: data.candidates.map(transformCandidatesMessage),
370
+ created: Math.floor(Date.now()/1000),
371
+ model,
372
+ //system_fingerprint: "fp_69829325d0",
373
+ object: "chat.completion",
374
+ usage: transformUsage(data.usageMetadata),
375
+ });
376
+ };
377
+
378
+ const responseLineRE = /^data: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
379
+ async function parseStream (chunk, controller) {
380
+ chunk = await chunk;
381
+ if (!chunk) { return; }
382
+ this.buffer += chunk;
383
+ do {
384
+ const match = this.buffer.match(responseLineRE);
385
+ if (!match) { break; }
386
+ controller.enqueue(match[1]);
387
+ this.buffer = this.buffer.substring(match[0].length);
388
+ } while (true); // eslint-disable-line no-constant-condition
389
+ }
390
+ async function parseStreamFlush (controller) {
391
+ if (this.buffer) {
392
+ console.error("Invalid data:", this.buffer);
393
+ controller.enqueue(this.buffer);
394
+ }
395
+ }
396
+
397
+ function transformResponseStream (data, stop, first) {
398
+ const item = transformCandidatesDelta(data.candidates[0]);
399
+ if (stop) { item.delta = {}; } else { item.finish_reason = null; }
400
+ if (first) { item.delta.content = ""; } else { delete item.delta.role; }
401
+ const output = {
402
+ id: this.id,
403
+ choices: [item],
404
+ created: Math.floor(Date.now()/1000),
405
+ model: this.model,
406
+ //system_fingerprint: "fp_69829325d0",
407
+ object: "chat.completion.chunk",
408
+ };
409
+ if (data.usageMetadata && this.streamIncludeUsage) {
410
+ output.usage = stop ? transformUsage(data.usageMetadata) : null;
411
+ }
412
+ return "data: " + JSON.stringify(output) + delimiter;
413
+ }
414
+ const delimiter = "\n\n";
415
+ async function toOpenAiStream (chunk, controller) {
416
+ const transform = transformResponseStream.bind(this);
417
+ const line = await chunk;
418
+ if (!line) { return; }
419
+ let data;
420
+ try {
421
+ data = JSON.parse(line);
422
+ } catch (err) {
423
+ console.error(line);
424
+ console.error(err);
425
+ const length = this.last.length || 1; // at least 1 error msg
426
+ const candidates = Array.from({ length }, (_, index) => ({
427
+ finishReason: "error",
428
+ content: { parts: [{ text: err }] },
429
+ index,
430
+ }));
431
+ data = { candidates };
432
+ }
433
+ const cand = data.candidates[0];
434
+ console.assert(data.candidates.length === 1, "Unexpected candidates count: %d", data.candidates.length);
435
+ cand.index = cand.index || 0; // absent in new -002 models response
436
+ if (!this.last[cand.index]) {
437
+ controller.enqueue(transform(data, false, "first"));
438
+ }
439
+ this.last[cand.index] = data;
440
+ if (cand.content) { // prevent empty data (e.g. when MAX_TOKENS)
441
+ controller.enqueue(transform(data));
442
+ }
443
+ }
444
+ async function toOpenAiStreamFlush (controller) {
445
+ const transform = transformResponseStream.bind(this);
446
+ if (this.last.length > 0) {
447
+ for (const data of this.last) {
448
+ controller.enqueue(transform(data, "stop"));
449
+ }
450
+ controller.enqueue("data: [DONE]" + delimiter);
451
+ }
452
+ }