ai-zero-token 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +23 -5
- package/README.zh-CN.md +24 -6
- package/admin-ui/dist/assets/StatCard-7TEzqn2i.js +1 -0
- package/admin-ui/dist/assets/accounts-bCDKXGg9.js +4 -0
- package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs--eK_2fzC.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-7wBZ1GhS.js} +1 -1
- package/admin-ui/dist/assets/index-C22_3Mxq.css +1 -0
- package/admin-ui/dist/assets/index-CdFYy5j6.js +10 -0
- package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-BiD1Khtg.js} +1 -1
- package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-BdoKDqh2.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-BvKns5nQ.js} +1 -1
- package/admin-ui/dist/assets/overview-wm6M45fu.js +1 -0
- package/admin-ui/dist/assets/settings-DOOu7Kd8.js +5 -0
- package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-NrARmlis.js} +1 -1
- package/admin-ui/dist/assets/usage-CdWRVMDV.js +1 -0
- package/admin-ui/dist/index.html +2 -2
- package/dist/core/context.js +3 -0
- package/dist/core/providers/http-client.js +247 -3
- package/dist/core/providers/openai-codex/chat.js +84 -23
- package/dist/core/providers/openai-codex/chatgpt-web-image.js +1404 -0
- package/dist/core/services/auth-service.js +64 -8
- package/dist/core/services/config-service.js +24 -5
- package/dist/core/services/image-service.js +31 -1
- package/dist/core/services/usage-service.js +349 -0
- package/dist/core/store/codex-auth-store.js +429 -4
- package/dist/core/store/settings-store.js +62 -26
- package/dist/core/store/state-paths.js +17 -1
- package/dist/server/app.js +1278 -119
- package/docs/API_USAGE.md +48 -1
- package/docs/DESKTOP_RELEASE.md +12 -1
- package/package.json +1 -1
- package/admin-ui/dist/assets/accounts-CTjk9c4F.js +0 -4
- package/admin-ui/dist/assets/index-By4r-wy3.css +0 -1
- package/admin-ui/dist/assets/index-rgcJgVAu.js +0 -10
- package/admin-ui/dist/assets/overview-X_WodIqE.js +0 -1
- package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
package/dist/server/app.js
CHANGED
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { promisify } from "node:util";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
brotliDecompress,
|
|
10
|
+
gunzip,
|
|
11
|
+
inflate,
|
|
12
|
+
zstdDecompress
|
|
13
|
+
} from "node:zlib";
|
|
6
14
|
import Fastify from "fastify";
|
|
7
15
|
import cors from "@fastify/cors";
|
|
8
16
|
import { z } from "zod";
|
|
9
17
|
import { createGatewayContext } from "../core/context.js";
|
|
10
18
|
import { requestText } from "../core/providers/http-client.js";
|
|
19
|
+
import { streamOpenAICodex } from "../core/providers/openai-codex/chat.js";
|
|
20
|
+
import { generateChatGPTWebImage } from "../core/providers/openai-codex/chatgpt-web-image.js";
|
|
11
21
|
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
12
22
|
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
13
23
|
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
14
24
|
const MAX_GATEWAY_REQUEST_LOGS = 100;
|
|
25
|
+
const gunzipAsync = promisify(gunzip);
|
|
26
|
+
const inflateAsync = promisify(inflate);
|
|
27
|
+
const brotliDecompressAsync = promisify(brotliDecompress);
|
|
28
|
+
const zstdDecompressAsync = typeof zstdDecompress === "function" ? promisify(zstdDecompress) : null;
|
|
15
29
|
const assetContentTypes = {
|
|
16
30
|
".css": "text/css; charset=utf-8",
|
|
17
31
|
".gif": "image/gif",
|
|
@@ -29,6 +43,35 @@ const assetContentTypes = {
|
|
|
29
43
|
function getContentType(filePath) {
|
|
30
44
|
return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
31
45
|
}
|
|
46
|
+
async function decodeJsonRequestBody(body, contentEncoding) {
|
|
47
|
+
const encodings = (Array.isArray(contentEncoding) ? contentEncoding.join(",") : contentEncoding ?? "").split(",").map((item) => item.trim().toLowerCase()).filter((item) => item && item !== "identity");
|
|
48
|
+
let decoded = body;
|
|
49
|
+
for (const encoding of encodings.reverse()) {
|
|
50
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
51
|
+
decoded = await gunzipAsync(decoded);
|
|
52
|
+
} else if (encoding === "deflate") {
|
|
53
|
+
decoded = await inflateAsync(decoded);
|
|
54
|
+
} else if (encoding === "br") {
|
|
55
|
+
decoded = await brotliDecompressAsync(decoded);
|
|
56
|
+
} else if (encoding === "zstd") {
|
|
57
|
+
if (!zstdDecompressAsync) {
|
|
58
|
+
throw new Error("\u5F53\u524D Node.js \u8FD0\u884C\u65F6\u4E0D\u652F\u6301 zstd \u8BF7\u6C42\u4F53\u89E3\u538B\uFF0C\u8BF7\u5347\u7EA7\u8FD0\u884C\u65F6\u540E\u91CD\u8BD5\u3002");
|
|
59
|
+
}
|
|
60
|
+
decoded = await zstdDecompressAsync(decoded);
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u4F53\u538B\u7F29\u683C\u5F0F: ${encoding}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return decoded;
|
|
66
|
+
}
|
|
67
|
+
async function parseJsonRequestBody(request, body) {
|
|
68
|
+
const rawBody = typeof body === "string" ? Buffer.from(body) : body;
|
|
69
|
+
if (rawBody.length === 0) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
const decoded = await decodeJsonRequestBody(rawBody, request.headers["content-encoding"]);
|
|
73
|
+
return JSON.parse(decoded.toString("utf8"));
|
|
74
|
+
}
|
|
32
75
|
async function readAdminUiAsset(assetPath) {
|
|
33
76
|
const normalized = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
34
77
|
const filePath = path.resolve(adminUiDistDir, normalized);
|
|
@@ -45,17 +88,9 @@ async function readAdminUiAsset(assetPath) {
|
|
|
45
88
|
return null;
|
|
46
89
|
}
|
|
47
90
|
}
|
|
48
|
-
const inputPartSchema = z.object({
|
|
49
|
-
type: z.string().optional(),
|
|
50
|
-
text: z.string().optional()
|
|
51
|
-
}).passthrough();
|
|
52
|
-
const inputMessageSchema = z.object({
|
|
53
|
-
role: z.string().optional(),
|
|
54
|
-
content: z.array(inputPartSchema).optional()
|
|
55
|
-
}).passthrough();
|
|
56
91
|
const responsesBodySchema = z.object({
|
|
57
92
|
model: z.string().optional(),
|
|
58
|
-
input: z.
|
|
93
|
+
input: z.unknown().optional(),
|
|
59
94
|
instructions: z.string().optional(),
|
|
60
95
|
stream: z.boolean().optional(),
|
|
61
96
|
tools: z.array(z.unknown()).optional(),
|
|
@@ -69,7 +104,7 @@ const responsesBodySchema = z.object({
|
|
|
69
104
|
allow_unknown_model: z.boolean().optional(),
|
|
70
105
|
include_raw: z.boolean().optional()
|
|
71
106
|
}).passthrough().optional()
|
|
72
|
-
});
|
|
107
|
+
}).passthrough();
|
|
73
108
|
const chatCompletionContentPartSchema = z.object({
|
|
74
109
|
type: z.string().optional(),
|
|
75
110
|
text: z.string().optional(),
|
|
@@ -114,11 +149,15 @@ const settingsUpdateSchema = z.object({
|
|
|
114
149
|
noProxy: z.string().optional()
|
|
115
150
|
}).optional(),
|
|
116
151
|
autoSwitch: z.object({
|
|
117
|
-
enabled: z.boolean()
|
|
152
|
+
enabled: z.boolean().optional(),
|
|
153
|
+
excludedProfileIds: z.array(z.string()).optional()
|
|
118
154
|
}).optional(),
|
|
119
155
|
runtime: z.object({
|
|
120
156
|
quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
|
|
121
157
|
}).optional(),
|
|
158
|
+
image: z.object({
|
|
159
|
+
freeAccountWebGenerationEnabled: z.boolean().optional()
|
|
160
|
+
}).optional(),
|
|
122
161
|
server: z.object({
|
|
123
162
|
port: z.number().int().min(1).max(65535)
|
|
124
163
|
}).optional()
|
|
@@ -150,6 +189,10 @@ const profileExportSchema = z.object({
|
|
|
150
189
|
const codexApplySchema = z.object({
|
|
151
190
|
profileId: z.string().min(1)
|
|
152
191
|
});
|
|
192
|
+
const codexProviderConfigSchema = z.object({
|
|
193
|
+
baseUrl: z.string().min(1).optional(),
|
|
194
|
+
providerId: z.string().min(1).optional()
|
|
195
|
+
});
|
|
153
196
|
const githubImageBedConfigSchema = z.object({
|
|
154
197
|
token: z.string().min(1)
|
|
155
198
|
});
|
|
@@ -199,6 +242,97 @@ const imageEditsBodySchema = z.object({
|
|
|
199
242
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
200
243
|
user: z.string().optional()
|
|
201
244
|
}).passthrough();
|
|
245
|
+
function isObjectRecord(value) {
|
|
246
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
function tokenNumber(value) {
|
|
249
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
|
|
250
|
+
}
|
|
251
|
+
function normalizeTokenUsage(value) {
|
|
252
|
+
if (!isObjectRecord(value)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const inputTokens = tokenNumber(value.input_tokens ?? value.prompt_tokens);
|
|
256
|
+
const outputTokens = tokenNumber(value.output_tokens ?? value.completion_tokens);
|
|
257
|
+
const totalTokens = tokenNumber(value.total_tokens) ?? (inputTokens !== null || outputTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) : null);
|
|
258
|
+
if (inputTokens === null && outputTokens === null && totalTokens === null) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
inputTokens,
|
|
263
|
+
outputTokens,
|
|
264
|
+
totalTokens
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function extractTokenUsage(value, depth = 0) {
|
|
268
|
+
if (depth > 5 || !value) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(value)) {
|
|
272
|
+
for (const item of value) {
|
|
273
|
+
const usage = extractTokenUsage(item, depth + 1);
|
|
274
|
+
if (usage) {
|
|
275
|
+
return usage;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!isObjectRecord(value)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const direct = normalizeTokenUsage(value);
|
|
284
|
+
if (direct) {
|
|
285
|
+
return direct;
|
|
286
|
+
}
|
|
287
|
+
for (const key of ["usage", "response", "events"]) {
|
|
288
|
+
const usage = extractTokenUsage(value[key], depth + 1);
|
|
289
|
+
if (usage) {
|
|
290
|
+
return usage;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function imageUsageToTokenUsage(usage) {
|
|
296
|
+
if (!usage) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
inputTokens: usage.input_tokens,
|
|
301
|
+
outputTokens: usage.output_tokens,
|
|
302
|
+
totalTokens: usage.total_tokens
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function extractUsageErrorType(details, statusCode) {
|
|
306
|
+
const error = isObjectRecord(details?.error) ? details.error : null;
|
|
307
|
+
const upstreamErrorCode = error?.upstreamErrorCode;
|
|
308
|
+
const upstreamStatus = error?.upstreamStatus;
|
|
309
|
+
const type = error?.type;
|
|
310
|
+
if (typeof upstreamErrorCode === "string" && upstreamErrorCode.trim()) {
|
|
311
|
+
return upstreamErrorCode.trim();
|
|
312
|
+
}
|
|
313
|
+
if (typeof type === "string" && type.trim()) {
|
|
314
|
+
return type.trim();
|
|
315
|
+
}
|
|
316
|
+
if (typeof upstreamStatus === "number") {
|
|
317
|
+
return `HTTP ${upstreamStatus}`;
|
|
318
|
+
}
|
|
319
|
+
return statusCode >= 400 ? `HTTP ${statusCode}` : void 0;
|
|
320
|
+
}
|
|
321
|
+
function extractTextFromInputContent(content) {
|
|
322
|
+
if (typeof content === "string" && content.trim()) {
|
|
323
|
+
return [content.trim()];
|
|
324
|
+
}
|
|
325
|
+
if (!Array.isArray(content)) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return content.flatMap((part) => {
|
|
329
|
+
if (!part || typeof part !== "object") {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
const record = part;
|
|
333
|
+
return typeof record.text === "string" && record.text.trim() ? [record.text.trim()] : [];
|
|
334
|
+
});
|
|
335
|
+
}
|
|
202
336
|
function extractTextInput(input) {
|
|
203
337
|
if (typeof input === "undefined") {
|
|
204
338
|
return "";
|
|
@@ -207,15 +341,60 @@ function extractTextInput(input) {
|
|
|
207
341
|
return input;
|
|
208
342
|
}
|
|
209
343
|
const chunks = [];
|
|
344
|
+
if (!Array.isArray(input)) {
|
|
345
|
+
return "";
|
|
346
|
+
}
|
|
210
347
|
for (const item of input) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
chunks.push(part.text.trim());
|
|
214
|
-
}
|
|
348
|
+
if (!item || typeof item !== "object") {
|
|
349
|
+
continue;
|
|
215
350
|
}
|
|
351
|
+
chunks.push(...extractTextFromInputContent(item.content));
|
|
216
352
|
}
|
|
217
353
|
return chunks.join("\n").trim();
|
|
218
354
|
}
|
|
355
|
+
function extractImageUrlFromInputPart(part) {
|
|
356
|
+
if (!isObjectRecord(part)) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const imageUrl = part.image_url ?? part.imageUrl;
|
|
360
|
+
if (typeof imageUrl === "string" && imageUrl.trim()) {
|
|
361
|
+
return imageUrl.trim();
|
|
362
|
+
}
|
|
363
|
+
if (isObjectRecord(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.trim()) {
|
|
364
|
+
return imageUrl.url.trim();
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function extractImageInputs(input) {
|
|
369
|
+
const images = [];
|
|
370
|
+
const addImage = (imageUrl) => {
|
|
371
|
+
if (imageUrl && !images.some((item) => item.imageUrl === imageUrl)) {
|
|
372
|
+
images.push({ imageUrl });
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
if (!Array.isArray(input)) {
|
|
376
|
+
addImage(extractImageUrlFromInputPart(input));
|
|
377
|
+
return images;
|
|
378
|
+
}
|
|
379
|
+
for (const item of input) {
|
|
380
|
+
addImage(extractImageUrlFromInputPart(item));
|
|
381
|
+
if (!isObjectRecord(item)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const content = item.content;
|
|
385
|
+
if (Array.isArray(content)) {
|
|
386
|
+
for (const part of content) {
|
|
387
|
+
addImage(extractImageUrlFromInputPart(part));
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
addImage(extractImageUrlFromInputPart(content));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return images;
|
|
394
|
+
}
|
|
395
|
+
function isFreePlan(profile) {
|
|
396
|
+
return profile.quota?.planType?.toLowerCase() === "free";
|
|
397
|
+
}
|
|
219
398
|
function normalizeResponseInput(input) {
|
|
220
399
|
if (typeof input === "undefined") {
|
|
221
400
|
return void 0;
|
|
@@ -412,9 +591,162 @@ function summarizeToolNames(tools) {
|
|
|
412
591
|
}
|
|
413
592
|
const record = tool;
|
|
414
593
|
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
415
|
-
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : "";
|
|
594
|
+
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : typeof record.type === "string" ? record.type : "";
|
|
416
595
|
}).filter(Boolean);
|
|
417
596
|
}
|
|
597
|
+
function summarizeResponsesRequest(data, endpoint = "/v1/responses") {
|
|
598
|
+
const input = data.input;
|
|
599
|
+
const toolNames = summarizeToolNames(Array.isArray(data.tools) ? data.tools : void 0);
|
|
600
|
+
return {
|
|
601
|
+
endpoint,
|
|
602
|
+
model: data.model ?? "default",
|
|
603
|
+
stream: data.stream ?? false,
|
|
604
|
+
inputKind: typeof input === "string" ? "string" : Array.isArray(input) ? "array" : "override",
|
|
605
|
+
inputItems: Array.isArray(input) ? input.length : void 0,
|
|
606
|
+
inputTextPreview: typeof input === "string" ? truncateForLog(input) : "",
|
|
607
|
+
instructionsLength: typeof data.instructions === "string" ? data.instructions.length : void 0,
|
|
608
|
+
toolCount: Array.isArray(data.tools) ? data.tools.length : 0,
|
|
609
|
+
toolNames: toolNames.slice(0, 50),
|
|
610
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
611
|
+
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
612
|
+
parallelToolCalls: data.parallel_tool_calls,
|
|
613
|
+
hasReasoning: Boolean(data.reasoning)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function createResponsesCodexBody(data) {
|
|
617
|
+
const experimentalBody = data.experimental_codex?.body ?? {};
|
|
618
|
+
const body = {
|
|
619
|
+
...experimentalBody,
|
|
620
|
+
...data
|
|
621
|
+
};
|
|
622
|
+
delete body.experimental_codex;
|
|
623
|
+
const normalizedInput = normalizeResponseInput(data.input);
|
|
624
|
+
if (typeof normalizedInput !== "undefined") {
|
|
625
|
+
body.input = normalizedInput;
|
|
626
|
+
}
|
|
627
|
+
return body;
|
|
628
|
+
}
|
|
629
|
+
function createCodexPassthroughBody(data, model) {
|
|
630
|
+
const body = {
|
|
631
|
+
...data,
|
|
632
|
+
model
|
|
633
|
+
};
|
|
634
|
+
delete body.experimental_codex;
|
|
635
|
+
return body;
|
|
636
|
+
}
|
|
637
|
+
function getImageGenerationTool(body) {
|
|
638
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
639
|
+
for (const tool of tools) {
|
|
640
|
+
if (isObjectRecord(tool) && tool.type === "image_generation") {
|
|
641
|
+
return tool;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function hasImageGenerationToolChoice(body) {
|
|
647
|
+
const choice = body.tool_choice;
|
|
648
|
+
if (typeof choice === "string") {
|
|
649
|
+
return choice === "image_generation";
|
|
650
|
+
}
|
|
651
|
+
return isObjectRecord(choice) && choice.type === "image_generation";
|
|
652
|
+
}
|
|
653
|
+
function normalizeImageOutputFormat(value) {
|
|
654
|
+
return value === "png" || value === "webp" || value === "jpeg" ? value : void 0;
|
|
655
|
+
}
|
|
656
|
+
function extractCodexImageGenerationRequest(body) {
|
|
657
|
+
const imageTool = getImageGenerationTool(body);
|
|
658
|
+
if (!hasImageGenerationToolChoice(body)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
prompt: extractTextInput(body.input),
|
|
663
|
+
inputImages: extractImageInputs(body.input),
|
|
664
|
+
imageModel: typeof imageTool?.model === "string" && imageTool.model.trim() ? imageTool.model.trim() : "gpt-image-2",
|
|
665
|
+
size: typeof imageTool?.size === "string" && imageTool.size.trim() ? imageTool.size.trim() : void 0,
|
|
666
|
+
outputFormat: normalizeImageOutputFormat(imageTool?.output_format)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function writeResponsesSseBlock(reply, block) {
|
|
670
|
+
if (!reply.raw.write(block)) {
|
|
671
|
+
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
672
|
+
}
|
|
673
|
+
return Buffer.byteLength(block);
|
|
674
|
+
}
|
|
675
|
+
async function writeResponsesSseEvent(reply, eventName, payload) {
|
|
676
|
+
return writeResponsesSseBlock(reply, `event: ${eventName}
|
|
677
|
+
data: ${JSON.stringify(payload)}
|
|
678
|
+
|
|
679
|
+
`);
|
|
680
|
+
}
|
|
681
|
+
async function sendSyntheticCodexImageSse(params) {
|
|
682
|
+
const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
|
|
683
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
684
|
+
const outputFormat = params.result.output_format ?? params.requestedOutputFormat ?? "png";
|
|
685
|
+
const size = params.result.size ?? params.requestedSize;
|
|
686
|
+
const output = params.result.data.map((image, index) => ({
|
|
687
|
+
id: `ig_${randomUUID().replace(/-/g, "")}`,
|
|
688
|
+
type: "image_generation_call",
|
|
689
|
+
status: "completed",
|
|
690
|
+
result: image.b64_json,
|
|
691
|
+
revised_prompt: image.revised_prompt ?? params.prompt,
|
|
692
|
+
output_format: outputFormat,
|
|
693
|
+
...size ? { size } : {}
|
|
694
|
+
}));
|
|
695
|
+
let bytes = 0;
|
|
696
|
+
params.reply.raw.writeHead(200, {
|
|
697
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
698
|
+
"Cache-Control": "no-cache, no-transform",
|
|
699
|
+
Connection: "keep-alive",
|
|
700
|
+
"X-Accel-Buffering": "no"
|
|
701
|
+
});
|
|
702
|
+
params.reply.raw.flushHeaders?.();
|
|
703
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.created", {
|
|
704
|
+
type: "response.created",
|
|
705
|
+
response: {
|
|
706
|
+
id: responseId,
|
|
707
|
+
object: "response",
|
|
708
|
+
created_at: created,
|
|
709
|
+
model: params.model,
|
|
710
|
+
status: "in_progress",
|
|
711
|
+
output: []
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
715
|
+
const item = output[index];
|
|
716
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.output_item.added", {
|
|
717
|
+
type: "response.output_item.added",
|
|
718
|
+
output_index: index,
|
|
719
|
+
item: {
|
|
720
|
+
id: item.id,
|
|
721
|
+
type: item.type,
|
|
722
|
+
status: "in_progress"
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.output_item.done", {
|
|
726
|
+
type: "response.output_item.done",
|
|
727
|
+
output_index: index,
|
|
728
|
+
item
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.completed", {
|
|
732
|
+
type: "response.completed",
|
|
733
|
+
response: {
|
|
734
|
+
id: responseId,
|
|
735
|
+
object: "response",
|
|
736
|
+
created_at: created,
|
|
737
|
+
model: params.model,
|
|
738
|
+
status: "completed",
|
|
739
|
+
output,
|
|
740
|
+
usage: null
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
bytes += await writeResponsesSseBlock(params.reply, "data: [DONE]\n\n");
|
|
744
|
+
params.reply.raw.end();
|
|
745
|
+
return {
|
|
746
|
+
bytes,
|
|
747
|
+
imageCount: output.length
|
|
748
|
+
};
|
|
749
|
+
}
|
|
418
750
|
function summarizeChatCompletionsRequest(data) {
|
|
419
751
|
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
420
752
|
const toolNames = summarizeToolNames(data.tools);
|
|
@@ -451,11 +783,57 @@ function summarizeCodexChatBody(body) {
|
|
|
451
783
|
hasReasoning: Boolean(body.reasoning)
|
|
452
784
|
};
|
|
453
785
|
}
|
|
786
|
+
async function buildOpenAIModelsResponse(ctx) {
|
|
787
|
+
return {
|
|
788
|
+
object: "list",
|
|
789
|
+
data: (await ctx.modelService.listModels()).map((model) => ({
|
|
790
|
+
id: model.id,
|
|
791
|
+
object: "model",
|
|
792
|
+
owned_by: model.provider
|
|
793
|
+
}))
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function buildCodexModelsResponse(ctx) {
|
|
797
|
+
const [models, catalog] = await Promise.all([
|
|
798
|
+
ctx.modelService.listModels(),
|
|
799
|
+
ctx.modelService.getCatalog()
|
|
800
|
+
]);
|
|
801
|
+
return {
|
|
802
|
+
fetched_at: catalog.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
803
|
+
models: models.map((model, index) => ({
|
|
804
|
+
slug: model.id,
|
|
805
|
+
display_name: model.name,
|
|
806
|
+
description: model.name,
|
|
807
|
+
default_reasoning_level: "medium",
|
|
808
|
+
supported_reasoning_levels: [
|
|
809
|
+
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
810
|
+
{ effort: "medium", description: "Balanced speed and reasoning" },
|
|
811
|
+
{ effort: "high", description: "Deeper reasoning" },
|
|
812
|
+
{ effort: "xhigh", description: "Extra deep reasoning" }
|
|
813
|
+
],
|
|
814
|
+
shell_type: "shell_command",
|
|
815
|
+
visibility: "list",
|
|
816
|
+
supported_in_api: true,
|
|
817
|
+
priority: index,
|
|
818
|
+
input_modalities: model.input
|
|
819
|
+
}))
|
|
820
|
+
};
|
|
821
|
+
}
|
|
454
822
|
function profileLogLabel(profile) {
|
|
455
823
|
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
456
824
|
}
|
|
457
825
|
function requestSourceFromUserAgent(userAgent) {
|
|
458
|
-
|
|
826
|
+
if (typeof userAgent !== "string") {
|
|
827
|
+
return "API";
|
|
828
|
+
}
|
|
829
|
+
const normalized = userAgent.toLowerCase();
|
|
830
|
+
if (normalized.includes("codex")) {
|
|
831
|
+
return "Codex";
|
|
832
|
+
}
|
|
833
|
+
if (normalized.includes("openclaw")) {
|
|
834
|
+
return "OpenClaw";
|
|
835
|
+
}
|
|
836
|
+
return "API";
|
|
459
837
|
}
|
|
460
838
|
function createChatCompletionsCodexBody(data) {
|
|
461
839
|
const body = {
|
|
@@ -749,7 +1127,7 @@ function getErrorStatusCode(error) {
|
|
|
749
1127
|
return normalized.statusCode;
|
|
750
1128
|
}
|
|
751
1129
|
const upstreamStatus = normalized.upstreamStatus;
|
|
752
|
-
if (upstreamStatus ===
|
|
1130
|
+
if (typeof upstreamStatus === "number" && upstreamStatus >= 400 && upstreamStatus < 600) {
|
|
753
1131
|
return upstreamStatus;
|
|
754
1132
|
}
|
|
755
1133
|
const message = normalized.message;
|
|
@@ -761,15 +1139,67 @@ function getErrorStatusCode(error) {
|
|
|
761
1139
|
}
|
|
762
1140
|
return 500;
|
|
763
1141
|
}
|
|
1142
|
+
function isQuotaLimitError(error) {
|
|
1143
|
+
const normalized = normalizeError(error);
|
|
1144
|
+
const marker = `${normalized.upstreamErrorCode ?? ""} ${normalized.upstreamErrorType ?? ""} ${normalized.message}`.toLowerCase();
|
|
1145
|
+
return normalized.upstreamStatus === 429 || marker.includes("usage_limit_reached");
|
|
1146
|
+
}
|
|
1147
|
+
function createSseStreamStats() {
|
|
1148
|
+
return {
|
|
1149
|
+
buffer: "",
|
|
1150
|
+
bytes: 0,
|
|
1151
|
+
completed: false
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function trackSseChunk(stats, chunk) {
|
|
1155
|
+
const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : String(chunk);
|
|
1156
|
+
stats.bytes += Buffer.byteLength(text);
|
|
1157
|
+
stats.buffer += text.replace(/\r\n/g, "\n");
|
|
1158
|
+
let separatorIndex = stats.buffer.indexOf("\n\n");
|
|
1159
|
+
while (separatorIndex !== -1) {
|
|
1160
|
+
const block = stats.buffer.slice(0, separatorIndex);
|
|
1161
|
+
stats.buffer = stats.buffer.slice(separatorIndex + 2);
|
|
1162
|
+
const eventName = block.split("\n").find((line) => line.startsWith("event:"))?.slice("event:".length).trim();
|
|
1163
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trim()).join("\n");
|
|
1164
|
+
let eventType = eventName;
|
|
1165
|
+
if (data && data !== "[DONE]") {
|
|
1166
|
+
try {
|
|
1167
|
+
const parsed = JSON.parse(data);
|
|
1168
|
+
if (typeof parsed.type === "string") {
|
|
1169
|
+
eventType = parsed.type;
|
|
1170
|
+
}
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (eventType === "response.completed") {
|
|
1175
|
+
stats.completed = true;
|
|
1176
|
+
stats.terminalEvent = eventType;
|
|
1177
|
+
} else if (eventType === "response.failed" || eventType === "response.incomplete") {
|
|
1178
|
+
stats.terminalEvent = eventType;
|
|
1179
|
+
}
|
|
1180
|
+
separatorIndex = stats.buffer.indexOf("\n\n");
|
|
1181
|
+
}
|
|
1182
|
+
if (stats.buffer.length > 65536) {
|
|
1183
|
+
stats.buffer = stats.buffer.slice(-65536);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
764
1186
|
function createApp(params) {
|
|
765
1187
|
const app = Fastify({
|
|
766
1188
|
logger: false,
|
|
767
1189
|
bodyLimit: params?.bodyLimit
|
|
768
1190
|
});
|
|
1191
|
+
app.removeContentTypeParser("application/json");
|
|
1192
|
+
app.addContentTypeParser(
|
|
1193
|
+
/^application\/(?:[\w!#$&^.+-]+\+)?json(?:\s*;.*)?$/i,
|
|
1194
|
+
{ parseAs: "buffer" },
|
|
1195
|
+
(request, body, done) => {
|
|
1196
|
+
parseJsonRequestBody(request, Buffer.isBuffer(body) ? body : Buffer.from(body)).then((parsed) => done(null, parsed)).catch((error) => done(error));
|
|
1197
|
+
}
|
|
1198
|
+
);
|
|
769
1199
|
const ctx = createGatewayContext();
|
|
770
1200
|
const gatewayRequestLogs = [];
|
|
771
1201
|
function pushGatewayRequestLog(log) {
|
|
772
|
-
|
|
1202
|
+
const entry = {
|
|
773
1203
|
id: log.id ?? randomUUID(),
|
|
774
1204
|
time: log.time ?? Date.now(),
|
|
775
1205
|
method: log.method,
|
|
@@ -780,10 +1210,34 @@ function createApp(params) {
|
|
|
780
1210
|
durationMs: log.durationMs,
|
|
781
1211
|
source: log.source,
|
|
782
1212
|
details: log.details
|
|
783
|
-
}
|
|
1213
|
+
};
|
|
1214
|
+
gatewayRequestLogs.unshift(entry);
|
|
784
1215
|
if (gatewayRequestLogs.length > MAX_GATEWAY_REQUEST_LOGS) {
|
|
785
1216
|
gatewayRequestLogs.length = MAX_GATEWAY_REQUEST_LOGS;
|
|
786
1217
|
}
|
|
1218
|
+
const profile = log.usage?.profile ?? void 0;
|
|
1219
|
+
const usageEvent = {
|
|
1220
|
+
id: entry.id,
|
|
1221
|
+
timestamp: entry.time,
|
|
1222
|
+
method: entry.method,
|
|
1223
|
+
endpoint: entry.endpoint,
|
|
1224
|
+
model: entry.model,
|
|
1225
|
+
source: entry.source,
|
|
1226
|
+
statusCode: entry.statusCode,
|
|
1227
|
+
durationMs: entry.durationMs,
|
|
1228
|
+
success: entry.statusCode >= 200 && entry.statusCode < 400,
|
|
1229
|
+
profileId: profile?.profileId,
|
|
1230
|
+
accountId: profile?.accountId,
|
|
1231
|
+
accountLabel: entry.account,
|
|
1232
|
+
planType: profile?.quota?.planType,
|
|
1233
|
+
tokenUsage: log.usage?.tokenUsage,
|
|
1234
|
+
imageCount: log.usage?.imageCount,
|
|
1235
|
+
imageRoute: log.usage?.imageRoute ?? "none",
|
|
1236
|
+
errorType: log.usage?.errorType ?? extractUsageErrorType(log.details, entry.statusCode)
|
|
1237
|
+
};
|
|
1238
|
+
ctx.usageService.record(usageEvent).catch((error) => {
|
|
1239
|
+
console.warn("[gateway:usage] \u7EDF\u8BA1\u5199\u5165\u5931\u8D25", error);
|
|
1240
|
+
});
|
|
787
1241
|
}
|
|
788
1242
|
void app.register(cors, {
|
|
789
1243
|
origin: params?.corsOrigin ?? true,
|
|
@@ -810,8 +1264,9 @@ function createApp(params) {
|
|
|
810
1264
|
app.get("/_gateway/admin/request-logs", async () => ({
|
|
811
1265
|
data: gatewayRequestLogs
|
|
812
1266
|
}));
|
|
1267
|
+
app.get("/_gateway/admin/usage", async () => ctx.usageService.getSummary());
|
|
813
1268
|
async function buildAdminConfig(request) {
|
|
814
|
-
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
1269
|
+
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus, usage] = await Promise.all([
|
|
815
1270
|
ctx.authService.getStatus(),
|
|
816
1271
|
ctx.modelService.listModels(),
|
|
817
1272
|
ctx.modelService.getCatalog(),
|
|
@@ -819,7 +1274,8 @@ function createApp(params) {
|
|
|
819
1274
|
ctx.configService.getSettings(),
|
|
820
1275
|
ctx.authService.getActiveProfile(),
|
|
821
1276
|
ctx.authService.listProfiles(),
|
|
822
|
-
ctx.authService.getCodexStatus()
|
|
1277
|
+
ctx.authService.getCodexStatus(),
|
|
1278
|
+
ctx.usageService.getSummary()
|
|
823
1279
|
]);
|
|
824
1280
|
const origin = resolveOrigin(request);
|
|
825
1281
|
return {
|
|
@@ -831,8 +1287,10 @@ function createApp(params) {
|
|
|
831
1287
|
profile: serializeProfile(profile),
|
|
832
1288
|
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
833
1289
|
codex: codexStatus,
|
|
1290
|
+
usage,
|
|
834
1291
|
adminUrl: `${origin}/`,
|
|
835
1292
|
baseUrl: `${origin}/v1`,
|
|
1293
|
+
codexBaseUrl: `${origin}/codex/v1`,
|
|
836
1294
|
restartSupported: Boolean(params?.onRestart),
|
|
837
1295
|
codexRestartSupported: Boolean(params?.onRestartCodex),
|
|
838
1296
|
supportedEndpoints: [
|
|
@@ -846,6 +1304,16 @@ function createApp(params) {
|
|
|
846
1304
|
path: "/v1/responses",
|
|
847
1305
|
description: "OpenAI responses \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
848
1306
|
},
|
|
1307
|
+
{
|
|
1308
|
+
method: "POST",
|
|
1309
|
+
path: "/codex/v1/responses",
|
|
1310
|
+
description: "Codex custom provider \u4E13\u7528 Responses SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
method: "POST",
|
|
1314
|
+
path: "/codex/v1/responses/compact",
|
|
1315
|
+
description: "Codex custom provider \u4E13\u7528 Responses compact SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
1316
|
+
},
|
|
849
1317
|
{
|
|
850
1318
|
method: "POST",
|
|
851
1319
|
path: "/v1/chat/completions",
|
|
@@ -1092,6 +1560,45 @@ function createApp(params) {
|
|
|
1092
1560
|
config: await buildAdminConfig(request)
|
|
1093
1561
|
};
|
|
1094
1562
|
});
|
|
1563
|
+
app.post("/_gateway/admin/codex/configure-provider", async (request, reply) => {
|
|
1564
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1565
|
+
if (!parsed.success) {
|
|
1566
|
+
reply.code(400);
|
|
1567
|
+
return {
|
|
1568
|
+
error: {
|
|
1569
|
+
type: "validation_error",
|
|
1570
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const origin = resolveOrigin(request);
|
|
1575
|
+
const baseUrl = parsed.data.baseUrl ?? `${origin}/codex/v1`;
|
|
1576
|
+
return {
|
|
1577
|
+
codexProvider: await ctx.authService.applyGatewayToCodexProvider({
|
|
1578
|
+
baseUrl,
|
|
1579
|
+
providerId: parsed.data.providerId
|
|
1580
|
+
}),
|
|
1581
|
+
config: await buildAdminConfig(request)
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
app.post("/_gateway/admin/codex/remove-provider", async (request, reply) => {
|
|
1585
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1586
|
+
if (!parsed.success) {
|
|
1587
|
+
reply.code(400);
|
|
1588
|
+
return {
|
|
1589
|
+
error: {
|
|
1590
|
+
type: "validation_error",
|
|
1591
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
codexProvider: await ctx.authService.removeGatewayFromCodexProvider({
|
|
1597
|
+
providerId: parsed.data.providerId
|
|
1598
|
+
}),
|
|
1599
|
+
config: await buildAdminConfig(request)
|
|
1600
|
+
};
|
|
1601
|
+
});
|
|
1095
1602
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
1096
1603
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
1097
1604
|
if (!parsed.success) {
|
|
@@ -1254,83 +1761,470 @@ function createApp(params) {
|
|
|
1254
1761
|
return ctx.githubImageBedService.deleteHistoryItem(parsed.data.id);
|
|
1255
1762
|
});
|
|
1256
1763
|
app.delete("/_gateway/image-bed/history", async () => ctx.githubImageBedService.clearHistory());
|
|
1257
|
-
app.get("/v1/models", async () => (
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1764
|
+
app.get("/v1/models", async () => buildOpenAIModelsResponse(ctx));
|
|
1765
|
+
app.get("/codex/v1/models", async () => buildCodexModelsResponse(ctx));
|
|
1766
|
+
app.get("/codex/v1/responses", async (_request, reply) => {
|
|
1767
|
+
reply.code(426);
|
|
1768
|
+
return {
|
|
1769
|
+
error: {
|
|
1770
|
+
type: "websocket_unsupported",
|
|
1771
|
+
message: "AI Zero Token \u5F53\u524D\u901A\u8FC7 HTTP SSE \u8F6C\u53D1 Codex Responses \u8BF7\u6C42\u3002"
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
});
|
|
1775
|
+
async function handleCodexResponsesPassthrough(request, reply, data, startedAt, upstreamEndpoint = "responses") {
|
|
1776
|
+
const abortController = new AbortController();
|
|
1777
|
+
let streamFinished = false;
|
|
1778
|
+
let headersCommitted = false;
|
|
1779
|
+
let profile = null;
|
|
1780
|
+
let retryCount = 0;
|
|
1781
|
+
let failureRecorded = false;
|
|
1782
|
+
let codexImageRoute = "none";
|
|
1783
|
+
reply.raw.on("close", () => {
|
|
1784
|
+
if (!streamFinished) {
|
|
1785
|
+
abortController.abort();
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
try {
|
|
1789
|
+
const model = await ctx.modelService.resolveModel("openai-codex", data.model, {
|
|
1790
|
+
allowUnknown: data.experimental_codex?.allow_unknown_model
|
|
1791
|
+
});
|
|
1792
|
+
const codexBody = createCodexPassthroughBody(data, model);
|
|
1793
|
+
const imageRequest = upstreamEndpoint === "responses" ? extractCodexImageGenerationRequest(codexBody) : null;
|
|
1794
|
+
if (imageRequest) {
|
|
1795
|
+
codexImageRoute = "codex-tool";
|
|
1796
|
+
const settings = await ctx.configService.getSettings();
|
|
1797
|
+
if (settings.image.freeAccountWebGenerationEnabled) {
|
|
1798
|
+
profile = await ctx.authService.requireUsableProfile("openai-codex", {
|
|
1799
|
+
skipAutoSwitch: true
|
|
1800
|
+
});
|
|
1273
1801
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1802
|
+
if (profile && isFreePlan(profile)) {
|
|
1803
|
+
if (!imageRequest.prompt) {
|
|
1804
|
+
throw new Error("Codex \u751F\u56FE\u8BF7\u6C42\u7F3A\u5C11\u6587\u672C prompt\u3002");
|
|
1805
|
+
}
|
|
1806
|
+
console.info("[gateway:codex:image] using ChatGPT web image route for Free profile", {
|
|
1807
|
+
requestId: request.id,
|
|
1808
|
+
account: profileLogLabel(profile),
|
|
1809
|
+
model,
|
|
1810
|
+
imageModel: imageRequest.imageModel,
|
|
1811
|
+
promptLength: imageRequest.prompt.length,
|
|
1812
|
+
inputImageCount: imageRequest.inputImages.length,
|
|
1813
|
+
size: imageRequest.size ?? "default"
|
|
1814
|
+
});
|
|
1815
|
+
const imageResult = await generateChatGPTWebImage({
|
|
1816
|
+
profile,
|
|
1817
|
+
prompt: imageRequest.prompt,
|
|
1818
|
+
model: imageRequest.imageModel,
|
|
1819
|
+
inputImages: imageRequest.inputImages,
|
|
1820
|
+
size: imageRequest.size,
|
|
1821
|
+
responseFormat: "b64_json"
|
|
1822
|
+
});
|
|
1823
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, void 0, "openai-codex", {
|
|
1824
|
+
skipAutoSwitch: true
|
|
1825
|
+
});
|
|
1826
|
+
headersCommitted = true;
|
|
1827
|
+
const syntheticStats = await sendSyntheticCodexImageSse({
|
|
1828
|
+
reply,
|
|
1829
|
+
result: imageResult,
|
|
1830
|
+
model,
|
|
1831
|
+
prompt: imageRequest.prompt,
|
|
1832
|
+
requestedSize: imageRequest.size,
|
|
1833
|
+
requestedOutputFormat: imageRequest.outputFormat
|
|
1834
|
+
});
|
|
1835
|
+
streamFinished = true;
|
|
1836
|
+
pushGatewayRequestLog({
|
|
1837
|
+
method: request.method,
|
|
1838
|
+
endpoint: request.url,
|
|
1839
|
+
account: profileLogLabel(profile),
|
|
1840
|
+
model,
|
|
1841
|
+
statusCode: 200,
|
|
1842
|
+
durationMs: performance.now() - startedAt,
|
|
1843
|
+
source: "Codex",
|
|
1844
|
+
details: {
|
|
1845
|
+
requestId: request.id,
|
|
1846
|
+
remoteAddress: request.ip,
|
|
1847
|
+
userAgent: request.headers["user-agent"],
|
|
1848
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1849
|
+
response: {
|
|
1850
|
+
stream: true,
|
|
1851
|
+
passthrough: false,
|
|
1852
|
+
upstreamEndpoint,
|
|
1853
|
+
route: "chatgpt-web-image",
|
|
1854
|
+
imageModel: imageRequest.imageModel,
|
|
1855
|
+
imageCount: syntheticStats.imageCount,
|
|
1856
|
+
bytes: syntheticStats.bytes
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1859
|
+
usage: {
|
|
1860
|
+
profile,
|
|
1861
|
+
imageCount: syntheticStats.imageCount,
|
|
1862
|
+
imageRoute: "chatgpt-web"
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
return reply;
|
|
1282
1866
|
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1867
|
+
}
|
|
1868
|
+
let upstream = null;
|
|
1869
|
+
const maxProfileAttempts = 5;
|
|
1870
|
+
for (let attempt = 0; attempt < maxProfileAttempts; attempt += 1) {
|
|
1871
|
+
profile = await ctx.authService.requireUsableProfile("openai-codex");
|
|
1872
|
+
try {
|
|
1873
|
+
upstream = await streamOpenAICodex({
|
|
1874
|
+
profile,
|
|
1875
|
+
model,
|
|
1876
|
+
bodyOverride: codexBody,
|
|
1877
|
+
endpoint: upstreamEndpoint,
|
|
1878
|
+
passthroughBody: true,
|
|
1879
|
+
signal: abortController.signal
|
|
1880
|
+
});
|
|
1881
|
+
break;
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
const quota = error.quota;
|
|
1884
|
+
const switchedProfile = await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1885
|
+
failureRecorded = true;
|
|
1886
|
+
if (attempt < maxProfileAttempts - 1 && isQuotaLimitError(error) && switchedProfile && switchedProfile.profileId !== profile.profileId && !abortController.signal.aborted) {
|
|
1887
|
+
retryCount += 1;
|
|
1888
|
+
failureRecorded = false;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
throw error;
|
|
1293
1892
|
}
|
|
1893
|
+
}
|
|
1894
|
+
if (!upstream || !profile) {
|
|
1895
|
+
throw new Error("Codex stream \u672A\u80FD\u5EFA\u7ACB\u3002");
|
|
1896
|
+
}
|
|
1897
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, upstream.quota, "openai-codex");
|
|
1898
|
+
const headers = {
|
|
1899
|
+
"Content-Type": upstream.headers["content-type"] ?? "text/event-stream; charset=utf-8",
|
|
1900
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1901
|
+
Connection: "keep-alive",
|
|
1902
|
+
"X-Accel-Buffering": "no"
|
|
1294
1903
|
};
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
const normalizedInput = normalizeResponseInput(parsed.data.input);
|
|
1300
|
-
if (typeof normalizedInput !== "undefined") {
|
|
1301
|
-
codexBody.input = normalizedInput;
|
|
1302
|
-
}
|
|
1303
|
-
if (typeof parsed.data.instructions === "string") {
|
|
1304
|
-
codexBody.instructions = parsed.data.instructions;
|
|
1305
|
-
}
|
|
1306
|
-
if (parsed.data.tools) {
|
|
1307
|
-
codexBody.tools = parsed.data.tools;
|
|
1308
|
-
}
|
|
1309
|
-
if (typeof parsed.data.tool_choice !== "undefined") {
|
|
1310
|
-
codexBody.tool_choice = parsed.data.tool_choice;
|
|
1311
|
-
}
|
|
1312
|
-
if (parsed.data.include) {
|
|
1313
|
-
codexBody.include = parsed.data.include;
|
|
1314
|
-
}
|
|
1315
|
-
if (parsed.data.text) {
|
|
1316
|
-
codexBody.text = parsed.data.text;
|
|
1317
|
-
}
|
|
1318
|
-
if (typeof parsed.data.store === "boolean") {
|
|
1319
|
-
codexBody.store = parsed.data.store;
|
|
1320
|
-
}
|
|
1321
|
-
if (typeof parsed.data.parallel_tool_calls === "boolean") {
|
|
1322
|
-
codexBody.parallel_tool_calls = parsed.data.parallel_tool_calls;
|
|
1323
|
-
}
|
|
1324
|
-
const result = await ctx.chatService.chat({
|
|
1325
|
-
model: parsed.data.model,
|
|
1326
|
-
input: input || void 0,
|
|
1327
|
-
system: parsed.data.instructions,
|
|
1328
|
-
experimental: {
|
|
1329
|
-
codexBody,
|
|
1330
|
-
allowUnknownModel: parsed.data.experimental_codex?.allow_unknown_model
|
|
1904
|
+
for (const [key, value] of Object.entries(upstream.headers)) {
|
|
1905
|
+
if (key.startsWith("x-codex-") || key === "x-request-id") {
|
|
1906
|
+
headers[key] = value;
|
|
1907
|
+
}
|
|
1331
1908
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1909
|
+
reply.raw.writeHead(upstream.status, headers);
|
|
1910
|
+
headersCommitted = true;
|
|
1911
|
+
reply.raw.flushHeaders?.();
|
|
1912
|
+
const streamStats = createSseStreamStats();
|
|
1913
|
+
for await (const chunk of Readable.fromWeb(upstream.body)) {
|
|
1914
|
+
trackSseChunk(streamStats, chunk);
|
|
1915
|
+
if (!reply.raw.write(chunk)) {
|
|
1916
|
+
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
streamFinished = true;
|
|
1920
|
+
reply.raw.end();
|
|
1921
|
+
if (!streamStats.completed) {
|
|
1922
|
+
console.warn("[gateway:codex:stream] upstream stream ended without response.completed", {
|
|
1923
|
+
requestId: request.id,
|
|
1924
|
+
upstreamRequestId: upstream.requestId,
|
|
1925
|
+
account: profileLogLabel(profile),
|
|
1926
|
+
model,
|
|
1927
|
+
bytes: streamStats.bytes,
|
|
1928
|
+
terminalEvent: streamStats.terminalEvent
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
pushGatewayRequestLog({
|
|
1932
|
+
method: request.method,
|
|
1933
|
+
endpoint: request.url,
|
|
1934
|
+
account: profileLogLabel(profile),
|
|
1935
|
+
model,
|
|
1936
|
+
statusCode: upstream.status,
|
|
1937
|
+
durationMs: performance.now() - startedAt,
|
|
1938
|
+
source: "Codex",
|
|
1939
|
+
details: {
|
|
1940
|
+
requestId: request.id,
|
|
1941
|
+
upstreamRequestId: upstream.requestId,
|
|
1942
|
+
remoteAddress: request.ip,
|
|
1943
|
+
userAgent: request.headers["user-agent"],
|
|
1944
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1945
|
+
response: {
|
|
1946
|
+
stream: true,
|
|
1947
|
+
passthrough: true,
|
|
1948
|
+
upstreamEndpoint,
|
|
1949
|
+
retryCount,
|
|
1950
|
+
completed: streamStats.completed,
|
|
1951
|
+
terminalEvent: streamStats.terminalEvent,
|
|
1952
|
+
bytes: streamStats.bytes
|
|
1953
|
+
}
|
|
1954
|
+
},
|
|
1955
|
+
usage: {
|
|
1956
|
+
profile,
|
|
1957
|
+
imageRoute: codexImageRoute
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
return reply;
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
const quota = error.quota;
|
|
1963
|
+
if (profile && !failureRecorded) {
|
|
1964
|
+
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1965
|
+
}
|
|
1966
|
+
const normalized = normalizeError(error);
|
|
1967
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
1968
|
+
pushGatewayRequestLog({
|
|
1969
|
+
method: request.method,
|
|
1970
|
+
endpoint: request.url,
|
|
1971
|
+
account: profileLogLabel(profile),
|
|
1972
|
+
model: data.model ?? "default",
|
|
1973
|
+
statusCode,
|
|
1974
|
+
durationMs: performance.now() - startedAt,
|
|
1975
|
+
source: "Codex",
|
|
1976
|
+
details: {
|
|
1977
|
+
requestId: request.id,
|
|
1978
|
+
remoteAddress: request.ip,
|
|
1979
|
+
userAgent: request.headers["user-agent"],
|
|
1980
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1981
|
+
response: {
|
|
1982
|
+
upstreamEndpoint,
|
|
1983
|
+
retryCount
|
|
1984
|
+
},
|
|
1985
|
+
error: {
|
|
1986
|
+
message: normalized.message,
|
|
1987
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
1988
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1989
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
usage: {
|
|
1993
|
+
profile,
|
|
1994
|
+
imageRoute: codexImageRoute
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
if (headersCommitted) {
|
|
1998
|
+
streamFinished = true;
|
|
1999
|
+
reply.raw.end();
|
|
2000
|
+
return reply;
|
|
2001
|
+
}
|
|
2002
|
+
throw error;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
app.post("/codex/v1/responses", async (request, reply) => {
|
|
2006
|
+
const startedAt = performance.now();
|
|
2007
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
2008
|
+
if (!parsed.success) {
|
|
2009
|
+
pushGatewayRequestLog({
|
|
2010
|
+
method: request.method,
|
|
2011
|
+
endpoint: request.url,
|
|
2012
|
+
account: "-",
|
|
2013
|
+
model: "-",
|
|
2014
|
+
statusCode: 400,
|
|
2015
|
+
durationMs: performance.now() - startedAt,
|
|
2016
|
+
source: "Codex",
|
|
2017
|
+
details: {
|
|
2018
|
+
requestId: request.id,
|
|
2019
|
+
remoteAddress: request.ip,
|
|
2020
|
+
userAgent: request.headers["user-agent"],
|
|
2021
|
+
error: {
|
|
2022
|
+
type: "validation_error",
|
|
2023
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
reply.code(400);
|
|
2028
|
+
return {
|
|
2029
|
+
error: {
|
|
2030
|
+
type: "validation_error",
|
|
2031
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt);
|
|
2036
|
+
});
|
|
2037
|
+
app.post("/codex/v1/responses/compact", async (request, reply) => {
|
|
2038
|
+
const startedAt = performance.now();
|
|
2039
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
2040
|
+
if (!parsed.success) {
|
|
2041
|
+
pushGatewayRequestLog({
|
|
2042
|
+
method: request.method,
|
|
2043
|
+
endpoint: request.url,
|
|
2044
|
+
account: "-",
|
|
2045
|
+
model: "-",
|
|
2046
|
+
statusCode: 400,
|
|
2047
|
+
durationMs: performance.now() - startedAt,
|
|
2048
|
+
source: "Codex",
|
|
2049
|
+
details: {
|
|
2050
|
+
requestId: request.id,
|
|
2051
|
+
remoteAddress: request.ip,
|
|
2052
|
+
userAgent: request.headers["user-agent"],
|
|
2053
|
+
error: {
|
|
2054
|
+
type: "validation_error",
|
|
2055
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
reply.code(400);
|
|
2060
|
+
return {
|
|
2061
|
+
error: {
|
|
2062
|
+
type: "validation_error",
|
|
2063
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt, "responses/compact");
|
|
2068
|
+
});
|
|
2069
|
+
app.post("/v1/responses", async (request, reply) => {
|
|
2070
|
+
const startedAt = performance.now();
|
|
2071
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
2072
|
+
if (!parsed.success) {
|
|
2073
|
+
pushGatewayRequestLog({
|
|
2074
|
+
method: request.method,
|
|
2075
|
+
endpoint: request.url,
|
|
2076
|
+
account: "-",
|
|
2077
|
+
model: "-",
|
|
2078
|
+
statusCode: 400,
|
|
2079
|
+
durationMs: performance.now() - startedAt,
|
|
2080
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2081
|
+
details: {
|
|
2082
|
+
requestId: request.id,
|
|
2083
|
+
remoteAddress: request.ip,
|
|
2084
|
+
userAgent: request.headers["user-agent"],
|
|
2085
|
+
error: {
|
|
2086
|
+
type: "validation_error",
|
|
2087
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
reply.code(400);
|
|
2092
|
+
return {
|
|
2093
|
+
error: {
|
|
2094
|
+
type: "validation_error",
|
|
2095
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
const wantsEventStream = typeof request.headers.accept === "string" && request.headers.accept.toLowerCase().includes("text/event-stream");
|
|
2100
|
+
const input = extractTextInput(parsed.data.input);
|
|
2101
|
+
const hasInput = typeof parsed.data.input !== "undefined" || typeof parsed.data.experimental_codex?.body?.input !== "undefined";
|
|
2102
|
+
if (!hasInput) {
|
|
2103
|
+
pushGatewayRequestLog({
|
|
2104
|
+
method: request.method,
|
|
2105
|
+
endpoint: request.url,
|
|
2106
|
+
account: "-",
|
|
2107
|
+
model: parsed.data.model ?? "default",
|
|
2108
|
+
statusCode: 400,
|
|
2109
|
+
durationMs: performance.now() - startedAt,
|
|
2110
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2111
|
+
details: {
|
|
2112
|
+
requestId: request.id,
|
|
2113
|
+
remoteAddress: request.ip,
|
|
2114
|
+
userAgent: request.headers["user-agent"],
|
|
2115
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2116
|
+
error: {
|
|
2117
|
+
type: "validation_error",
|
|
2118
|
+
message: "\u6CA1\u6709\u63D0\u4F9B input\uFF0C\u4E5F\u6CA1\u6709\u5728 experimental_codex.body \u91CC\u900F\u4F20 input"
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
reply.code(400);
|
|
2123
|
+
return {
|
|
2124
|
+
error: {
|
|
2125
|
+
type: "validation_error",
|
|
2126
|
+
message: "\u6CA1\u6709\u63D0\u4F9B input\uFF0C\u4E5F\u6CA1\u6709\u5728 experimental_codex.body \u91CC\u900F\u4F20 input"
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
if (parsed.data.stream || wantsEventStream) {
|
|
2131
|
+
pushGatewayRequestLog({
|
|
2132
|
+
method: request.method,
|
|
2133
|
+
endpoint: request.url,
|
|
2134
|
+
account: "-",
|
|
2135
|
+
model: parsed.data.model ?? "default",
|
|
2136
|
+
statusCode: 501,
|
|
2137
|
+
durationMs: performance.now() - startedAt,
|
|
2138
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2139
|
+
details: {
|
|
2140
|
+
requestId: request.id,
|
|
2141
|
+
remoteAddress: request.ip,
|
|
2142
|
+
userAgent: request.headers["user-agent"],
|
|
2143
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2144
|
+
error: {
|
|
2145
|
+
type: "not_supported",
|
|
2146
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
reply.code(501);
|
|
2151
|
+
return {
|
|
2152
|
+
error: {
|
|
2153
|
+
type: "not_supported",
|
|
2154
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const codexBody = createResponsesCodexBody(parsed.data);
|
|
2159
|
+
let result;
|
|
2160
|
+
try {
|
|
2161
|
+
result = await ctx.chatService.chat({
|
|
2162
|
+
model: parsed.data.model,
|
|
2163
|
+
input: input || void 0,
|
|
2164
|
+
system: parsed.data.instructions,
|
|
2165
|
+
experimental: {
|
|
2166
|
+
codexBody,
|
|
2167
|
+
allowUnknownModel: parsed.data.experimental_codex?.allow_unknown_model
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
const normalized = normalizeError(error);
|
|
2172
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2173
|
+
const activeProfile2 = await ctx.authService.getActiveProfile();
|
|
2174
|
+
pushGatewayRequestLog({
|
|
2175
|
+
method: request.method,
|
|
2176
|
+
endpoint: request.url,
|
|
2177
|
+
account: profileLogLabel(activeProfile2),
|
|
2178
|
+
model: parsed.data.model ?? "default",
|
|
2179
|
+
statusCode,
|
|
2180
|
+
durationMs: performance.now() - startedAt,
|
|
2181
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2182
|
+
details: {
|
|
2183
|
+
requestId: request.id,
|
|
2184
|
+
remoteAddress: request.ip,
|
|
2185
|
+
userAgent: request.headers["user-agent"],
|
|
2186
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2187
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
2188
|
+
error: {
|
|
2189
|
+
message: normalized.message,
|
|
2190
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2191
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2192
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
usage: {
|
|
2196
|
+
profile: activeProfile2
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
throw error;
|
|
2200
|
+
}
|
|
2201
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
2202
|
+
pushGatewayRequestLog({
|
|
2203
|
+
method: request.method,
|
|
2204
|
+
endpoint: request.url,
|
|
2205
|
+
account: profileLogLabel(activeProfile),
|
|
2206
|
+
model: result.model,
|
|
2207
|
+
statusCode: 200,
|
|
2208
|
+
durationMs: performance.now() - startedAt,
|
|
2209
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2210
|
+
details: {
|
|
2211
|
+
requestId: request.id,
|
|
2212
|
+
remoteAddress: request.ip,
|
|
2213
|
+
userAgent: request.headers["user-agent"],
|
|
2214
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2215
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
2216
|
+
response: {
|
|
2217
|
+
textPreview: truncateForLog(result.text),
|
|
2218
|
+
textLength: result.text.length,
|
|
2219
|
+
artifactCount: result.artifacts.length
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
usage: {
|
|
2223
|
+
profile: activeProfile,
|
|
2224
|
+
tokenUsage: extractTokenUsage(result.raw)
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
1334
2228
|
});
|
|
1335
2229
|
app.post("/v1/chat/completions", async (request, reply) => {
|
|
1336
2230
|
const startedAt = performance.now();
|
|
@@ -1411,10 +2305,11 @@ function createApp(params) {
|
|
|
1411
2305
|
} catch (error) {
|
|
1412
2306
|
const normalized = normalizeError(error);
|
|
1413
2307
|
const statusCode = getErrorStatusCode(normalized);
|
|
2308
|
+
const activeProfile2 = await ctx.authService.getActiveProfile();
|
|
1414
2309
|
pushGatewayRequestLog({
|
|
1415
2310
|
method: request.method,
|
|
1416
2311
|
endpoint: request.url,
|
|
1417
|
-
account: profileLogLabel(
|
|
2312
|
+
account: profileLogLabel(activeProfile2),
|
|
1418
2313
|
model: parsed.data.model ?? "default",
|
|
1419
2314
|
statusCode,
|
|
1420
2315
|
durationMs: performance.now() - startedAt,
|
|
@@ -1431,14 +2326,18 @@ function createApp(params) {
|
|
|
1431
2326
|
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1432
2327
|
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1433
2328
|
}
|
|
2329
|
+
},
|
|
2330
|
+
usage: {
|
|
2331
|
+
profile: activeProfile2
|
|
1434
2332
|
}
|
|
1435
2333
|
});
|
|
1436
2334
|
throw error;
|
|
1437
2335
|
}
|
|
2336
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
1438
2337
|
pushGatewayRequestLog({
|
|
1439
2338
|
method: request.method,
|
|
1440
2339
|
endpoint: request.url,
|
|
1441
|
-
account: profileLogLabel(
|
|
2340
|
+
account: profileLogLabel(activeProfile),
|
|
1442
2341
|
model: result.model,
|
|
1443
2342
|
statusCode: 200,
|
|
1444
2343
|
durationMs: performance.now() - startedAt,
|
|
@@ -1461,6 +2360,10 @@ function createApp(params) {
|
|
|
1461
2360
|
artifactCount: result.artifacts.length,
|
|
1462
2361
|
stream: parsed.data.stream ?? false
|
|
1463
2362
|
}
|
|
2363
|
+
},
|
|
2364
|
+
usage: {
|
|
2365
|
+
profile: activeProfile,
|
|
2366
|
+
tokenUsage: extractTokenUsage(result.raw)
|
|
1464
2367
|
}
|
|
1465
2368
|
});
|
|
1466
2369
|
console.info("[gateway:chat:response]", {
|
|
@@ -1479,6 +2382,7 @@ function createApp(params) {
|
|
|
1479
2382
|
return buildChatCompletionsBody(result);
|
|
1480
2383
|
});
|
|
1481
2384
|
app.post("/v1/images/generations", async (request, reply) => {
|
|
2385
|
+
const startedAt = performance.now();
|
|
1482
2386
|
const parsed = imageGenerationsBodySchema.safeParse(request.body);
|
|
1483
2387
|
if (!parsed.success) {
|
|
1484
2388
|
console.error("[gateway:image] validation failure", {
|
|
@@ -1486,6 +2390,24 @@ function createApp(params) {
|
|
|
1486
2390
|
url: request.url,
|
|
1487
2391
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1488
2392
|
});
|
|
2393
|
+
pushGatewayRequestLog({
|
|
2394
|
+
method: request.method,
|
|
2395
|
+
endpoint: request.url,
|
|
2396
|
+
account: "-",
|
|
2397
|
+
model: "-",
|
|
2398
|
+
statusCode: 400,
|
|
2399
|
+
durationMs: performance.now() - startedAt,
|
|
2400
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2401
|
+
details: {
|
|
2402
|
+
requestId: request.id,
|
|
2403
|
+
remoteAddress: request.ip,
|
|
2404
|
+
userAgent: request.headers["user-agent"],
|
|
2405
|
+
error: {
|
|
2406
|
+
type: "validation_error",
|
|
2407
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
1489
2411
|
reply.code(400);
|
|
1490
2412
|
return {
|
|
1491
2413
|
error: {
|
|
@@ -1502,6 +2424,25 @@ function createApp(params) {
|
|
|
1502
2424
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1503
2425
|
issue: validationError
|
|
1504
2426
|
});
|
|
2427
|
+
pushGatewayRequestLog({
|
|
2428
|
+
method: request.method,
|
|
2429
|
+
endpoint: request.url,
|
|
2430
|
+
account: "-",
|
|
2431
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2432
|
+
statusCode: 400,
|
|
2433
|
+
durationMs: performance.now() - startedAt,
|
|
2434
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2435
|
+
details: {
|
|
2436
|
+
requestId: request.id,
|
|
2437
|
+
remoteAddress: request.ip,
|
|
2438
|
+
userAgent: request.headers["user-agent"],
|
|
2439
|
+
request: summarizeImageRequestForLog(parsed.data),
|
|
2440
|
+
error: {
|
|
2441
|
+
type: "validation_error",
|
|
2442
|
+
message: validationError
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
1505
2446
|
reply.code(400);
|
|
1506
2447
|
return {
|
|
1507
2448
|
error: {
|
|
@@ -1517,6 +2458,25 @@ function createApp(params) {
|
|
|
1517
2458
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1518
2459
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1519
2460
|
});
|
|
2461
|
+
pushGatewayRequestLog({
|
|
2462
|
+
method: request.method,
|
|
2463
|
+
endpoint: request.url,
|
|
2464
|
+
account: "-",
|
|
2465
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2466
|
+
statusCode: 501,
|
|
2467
|
+
durationMs: performance.now() - startedAt,
|
|
2468
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2469
|
+
details: {
|
|
2470
|
+
requestId: request.id,
|
|
2471
|
+
remoteAddress: request.ip,
|
|
2472
|
+
userAgent: request.headers["user-agent"],
|
|
2473
|
+
request: summarizeImageRequestForLog(parsed.data),
|
|
2474
|
+
error: {
|
|
2475
|
+
type: "not_supported",
|
|
2476
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
1520
2480
|
reply.code(501);
|
|
1521
2481
|
return {
|
|
1522
2482
|
error: {
|
|
@@ -1531,17 +2491,52 @@ function createApp(params) {
|
|
|
1531
2491
|
url: request.url,
|
|
1532
2492
|
summary: requestSummary
|
|
1533
2493
|
});
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2494
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
2495
|
+
const settings = await ctx.configService.getSettings();
|
|
2496
|
+
const imageRoute = activeProfile && isFreePlan(activeProfile) && settings.image.freeAccountWebGenerationEnabled ? "chatgpt-web" : "codex-tool";
|
|
2497
|
+
let response;
|
|
2498
|
+
try {
|
|
2499
|
+
response = await ctx.imageService.generate({
|
|
2500
|
+
prompt: parsed.data.prompt,
|
|
2501
|
+
model: parsed.data.model,
|
|
2502
|
+
n: parsed.data.n,
|
|
2503
|
+
size: parsed.data.size,
|
|
2504
|
+
quality: parsed.data.quality,
|
|
2505
|
+
background: parsed.data.background,
|
|
2506
|
+
outputFormat: parsed.data.output_format,
|
|
2507
|
+
outputCompression: parsed.data.output_compression,
|
|
2508
|
+
moderation: parsed.data.moderation
|
|
2509
|
+
});
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
const normalized = normalizeError(error);
|
|
2512
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2513
|
+
pushGatewayRequestLog({
|
|
2514
|
+
method: request.method,
|
|
2515
|
+
endpoint: request.url,
|
|
2516
|
+
account: profileLogLabel(activeProfile),
|
|
2517
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2518
|
+
statusCode,
|
|
2519
|
+
durationMs: performance.now() - startedAt,
|
|
2520
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2521
|
+
details: {
|
|
2522
|
+
requestId: request.id,
|
|
2523
|
+
remoteAddress: request.ip,
|
|
2524
|
+
userAgent: request.headers["user-agent"],
|
|
2525
|
+
request: requestSummary,
|
|
2526
|
+
error: {
|
|
2527
|
+
message: normalized.message,
|
|
2528
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2529
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2530
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2531
|
+
}
|
|
2532
|
+
},
|
|
2533
|
+
usage: {
|
|
2534
|
+
profile: activeProfile,
|
|
2535
|
+
imageRoute
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
throw error;
|
|
2539
|
+
}
|
|
1545
2540
|
console.info("[gateway:image] response ready", {
|
|
1546
2541
|
method: request.method,
|
|
1547
2542
|
url: request.url,
|
|
@@ -1552,11 +2547,57 @@ function createApp(params) {
|
|
|
1552
2547
|
quality: response.quality,
|
|
1553
2548
|
size: response.size
|
|
1554
2549
|
});
|
|
2550
|
+
pushGatewayRequestLog({
|
|
2551
|
+
method: request.method,
|
|
2552
|
+
endpoint: request.url,
|
|
2553
|
+
account: profileLogLabel(activeProfile),
|
|
2554
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2555
|
+
statusCode: 200,
|
|
2556
|
+
durationMs: performance.now() - startedAt,
|
|
2557
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2558
|
+
details: {
|
|
2559
|
+
requestId: request.id,
|
|
2560
|
+
remoteAddress: request.ip,
|
|
2561
|
+
userAgent: request.headers["user-agent"],
|
|
2562
|
+
request: requestSummary,
|
|
2563
|
+
response: {
|
|
2564
|
+
imageCount: response.data.length,
|
|
2565
|
+
outputFormat: response.output_format,
|
|
2566
|
+
quality: response.quality,
|
|
2567
|
+
size: response.size
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
usage: {
|
|
2571
|
+
profile: activeProfile,
|
|
2572
|
+
tokenUsage: imageUsageToTokenUsage(response.usage),
|
|
2573
|
+
imageCount: response.data.length,
|
|
2574
|
+
imageRoute
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
1555
2577
|
return response;
|
|
1556
2578
|
});
|
|
1557
2579
|
app.post("/v1/images/edits", async (request, reply) => {
|
|
2580
|
+
const startedAt = performance.now();
|
|
1558
2581
|
const contentType = request.headers["content-type"] ?? "";
|
|
1559
2582
|
if (!String(contentType).toLowerCase().includes("application/json")) {
|
|
2583
|
+
pushGatewayRequestLog({
|
|
2584
|
+
method: request.method,
|
|
2585
|
+
endpoint: request.url,
|
|
2586
|
+
account: "-",
|
|
2587
|
+
model: "-",
|
|
2588
|
+
statusCode: 415,
|
|
2589
|
+
durationMs: performance.now() - startedAt,
|
|
2590
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2591
|
+
details: {
|
|
2592
|
+
requestId: request.id,
|
|
2593
|
+
remoteAddress: request.ip,
|
|
2594
|
+
userAgent: request.headers["user-agent"],
|
|
2595
|
+
error: {
|
|
2596
|
+
type: "unsupported_media_type",
|
|
2597
|
+
message: "\u5F53\u524D\u7F51\u5173\u4EC5\u652F\u6301 JSON \u7248 images.edits\uFF1B\u8BF7\u4F7F\u7528 application/json\uFF0C\u5E76\u901A\u8FC7 images[].image_url \u4F20 URL \u6216 base64 data URL\u3002"
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
1560
2601
|
reply.code(415);
|
|
1561
2602
|
return {
|
|
1562
2603
|
error: {
|
|
@@ -1572,6 +2613,24 @@ function createApp(params) {
|
|
|
1572
2613
|
url: request.url,
|
|
1573
2614
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1574
2615
|
});
|
|
2616
|
+
pushGatewayRequestLog({
|
|
2617
|
+
method: request.method,
|
|
2618
|
+
endpoint: request.url,
|
|
2619
|
+
account: "-",
|
|
2620
|
+
model: "-",
|
|
2621
|
+
statusCode: 400,
|
|
2622
|
+
durationMs: performance.now() - startedAt,
|
|
2623
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2624
|
+
details: {
|
|
2625
|
+
requestId: request.id,
|
|
2626
|
+
remoteAddress: request.ip,
|
|
2627
|
+
userAgent: request.headers["user-agent"],
|
|
2628
|
+
error: {
|
|
2629
|
+
type: "validation_error",
|
|
2630
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
1575
2634
|
reply.code(400);
|
|
1576
2635
|
return {
|
|
1577
2636
|
error: {
|
|
@@ -1588,6 +2647,25 @@ function createApp(params) {
|
|
|
1588
2647
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1589
2648
|
issue: validationError
|
|
1590
2649
|
});
|
|
2650
|
+
pushGatewayRequestLog({
|
|
2651
|
+
method: request.method,
|
|
2652
|
+
endpoint: request.url,
|
|
2653
|
+
account: "-",
|
|
2654
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2655
|
+
statusCode: 400,
|
|
2656
|
+
durationMs: performance.now() - startedAt,
|
|
2657
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2658
|
+
details: {
|
|
2659
|
+
requestId: request.id,
|
|
2660
|
+
remoteAddress: request.ip,
|
|
2661
|
+
userAgent: request.headers["user-agent"],
|
|
2662
|
+
request: summarizeImageEditRequestForLog(parsed.data),
|
|
2663
|
+
error: {
|
|
2664
|
+
type: "validation_error",
|
|
2665
|
+
message: validationError
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
1591
2669
|
reply.code(400);
|
|
1592
2670
|
return {
|
|
1593
2671
|
error: {
|
|
@@ -1603,6 +2681,25 @@ function createApp(params) {
|
|
|
1603
2681
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1604
2682
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1605
2683
|
});
|
|
2684
|
+
pushGatewayRequestLog({
|
|
2685
|
+
method: request.method,
|
|
2686
|
+
endpoint: request.url,
|
|
2687
|
+
account: "-",
|
|
2688
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2689
|
+
statusCode: 501,
|
|
2690
|
+
durationMs: performance.now() - startedAt,
|
|
2691
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2692
|
+
details: {
|
|
2693
|
+
requestId: request.id,
|
|
2694
|
+
remoteAddress: request.ip,
|
|
2695
|
+
userAgent: request.headers["user-agent"],
|
|
2696
|
+
request: summarizeImageEditRequestForLog(parsed.data),
|
|
2697
|
+
error: {
|
|
2698
|
+
type: "not_supported",
|
|
2699
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
1606
2703
|
reply.code(501);
|
|
1607
2704
|
return {
|
|
1608
2705
|
error: {
|
|
@@ -1620,18 +2717,53 @@ function createApp(params) {
|
|
|
1620
2717
|
url: request.url,
|
|
1621
2718
|
summary: requestSummary
|
|
1622
2719
|
});
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2720
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
2721
|
+
const settings = await ctx.configService.getSettings();
|
|
2722
|
+
const imageRoute = activeProfile && isFreePlan(activeProfile) && settings.image.freeAccountWebGenerationEnabled ? "chatgpt-web" : "codex-tool";
|
|
2723
|
+
let response;
|
|
2724
|
+
try {
|
|
2725
|
+
response = await ctx.imageService.generate({
|
|
2726
|
+
prompt: parsed.data.prompt,
|
|
2727
|
+
inputImages: imageReferences,
|
|
2728
|
+
model: parsed.data.model,
|
|
2729
|
+
n: parsed.data.n,
|
|
2730
|
+
size: parsed.data.size,
|
|
2731
|
+
quality: parsed.data.quality,
|
|
2732
|
+
background: parsed.data.background,
|
|
2733
|
+
outputFormat: parsed.data.output_format,
|
|
2734
|
+
outputCompression: parsed.data.output_compression,
|
|
2735
|
+
moderation: parsed.data.moderation
|
|
2736
|
+
});
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
const normalized = normalizeError(error);
|
|
2739
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2740
|
+
pushGatewayRequestLog({
|
|
2741
|
+
method: request.method,
|
|
2742
|
+
endpoint: request.url,
|
|
2743
|
+
account: profileLogLabel(activeProfile),
|
|
2744
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2745
|
+
statusCode,
|
|
2746
|
+
durationMs: performance.now() - startedAt,
|
|
2747
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2748
|
+
details: {
|
|
2749
|
+
requestId: request.id,
|
|
2750
|
+
remoteAddress: request.ip,
|
|
2751
|
+
userAgent: request.headers["user-agent"],
|
|
2752
|
+
request: requestSummary,
|
|
2753
|
+
error: {
|
|
2754
|
+
message: normalized.message,
|
|
2755
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2756
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2757
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2758
|
+
}
|
|
2759
|
+
},
|
|
2760
|
+
usage: {
|
|
2761
|
+
profile: activeProfile,
|
|
2762
|
+
imageRoute
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
throw error;
|
|
2766
|
+
}
|
|
1635
2767
|
console.info("[gateway:image:edit] response ready", {
|
|
1636
2768
|
method: request.method,
|
|
1637
2769
|
url: request.url,
|
|
@@ -1642,6 +2774,33 @@ function createApp(params) {
|
|
|
1642
2774
|
quality: response.quality,
|
|
1643
2775
|
size: response.size
|
|
1644
2776
|
});
|
|
2777
|
+
pushGatewayRequestLog({
|
|
2778
|
+
method: request.method,
|
|
2779
|
+
endpoint: request.url,
|
|
2780
|
+
account: profileLogLabel(activeProfile),
|
|
2781
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2782
|
+
statusCode: 200,
|
|
2783
|
+
durationMs: performance.now() - startedAt,
|
|
2784
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2785
|
+
details: {
|
|
2786
|
+
requestId: request.id,
|
|
2787
|
+
remoteAddress: request.ip,
|
|
2788
|
+
userAgent: request.headers["user-agent"],
|
|
2789
|
+
request: requestSummary,
|
|
2790
|
+
response: {
|
|
2791
|
+
imageCount: response.data.length,
|
|
2792
|
+
outputFormat: response.output_format,
|
|
2793
|
+
quality: response.quality,
|
|
2794
|
+
size: response.size
|
|
2795
|
+
}
|
|
2796
|
+
},
|
|
2797
|
+
usage: {
|
|
2798
|
+
profile: activeProfile,
|
|
2799
|
+
tokenUsage: imageUsageToTokenUsage(response.usage),
|
|
2800
|
+
imageCount: response.data.length,
|
|
2801
|
+
imageRoute
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
1645
2804
|
return response;
|
|
1646
2805
|
});
|
|
1647
2806
|
return app;
|