ai-zero-token 2.0.5 → 2.0.7
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 +24 -0
- package/README.md +20 -17
- package/README.zh-CN.md +20 -17
- package/admin-ui/dist/assets/StatCard-7TEzqn2i.js +1 -0
- package/admin-ui/dist/assets/accounts-D3tsDc3k.js +4 -0
- package/admin-ui/dist/assets/{docs-Dh0aFha_.js → docs-BO-aSEzh.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-C1M7-0q1.js → image-bed-Dql7Vqd9.js} +1 -1
- package/admin-ui/dist/assets/index-C22_3Mxq.css +1 -0
- package/admin-ui/dist/assets/index-CCiBaGwU.js +10 -0
- package/admin-ui/dist/assets/{launch-pB7YlWFI.js → launch-DXLo-NIM.js} +1 -1
- package/admin-ui/dist/assets/{logs-B7McijSi.js → logs-Cwn8-rDu.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-Bx3XmXPk.js → network-detect-vzWfL-Tz.js} +1 -1
- package/admin-ui/dist/assets/overview-B_yad8ge.js +1 -0
- package/admin-ui/dist/assets/{profiles-DMOjJORP.js → profiles-C5SmQvju.js} +1 -1
- package/admin-ui/dist/assets/settings-BdRWcKJb.js +5 -0
- package/admin-ui/dist/assets/{tester-BG-up8qP.js → tester-BKoMSoCz.js} +1 -1
- package/admin-ui/dist/assets/usage-B-qQxXzQ.js +1 -0
- package/admin-ui/dist/index.html +3 -3
- package/dist/cli/commands/help.js +1 -1
- package/dist/cli/commands/models.js +3 -2
- package/dist/core/context.js +4 -1
- package/dist/core/models/openai-codex-models.js +106 -1
- package/dist/core/providers/http-client.js +160 -11
- package/dist/core/providers/openai-codex/chat.js +2 -1
- package/dist/core/providers/openai-codex/chatgpt-web-image.js +1404 -0
- package/dist/core/services/auth-service.js +154 -10
- package/dist/core/services/chat-service.js +16 -18
- package/dist/core/services/config-service.js +9 -0
- package/dist/core/services/image-service.js +31 -1
- package/dist/core/services/model-service.js +22 -8
- package/dist/core/services/usage-service.js +349 -0
- package/dist/core/store/codex-auth-store.js +149 -15
- package/dist/core/store/settings-store.js +8 -2
- package/dist/core/store/state-paths.js +17 -1
- package/dist/server/app.js +1023 -69
- package/dist/server/index.js +1 -1
- package/docs/API_USAGE.md +34 -4
- package/docs/DESKTOP_RELEASE.md +12 -1
- package/package.json +1 -1
- package/admin-ui/dist/assets/accounts-ABMyXo4H.js +0 -4
- package/admin-ui/dist/assets/index--rNjdmzf.js +0 -10
- package/admin-ui/dist/assets/index-DjtN30PC.css +0 -1
- package/admin-ui/dist/assets/overview-CV0H2Nsq.js +0 -1
- package/admin-ui/dist/assets/settings-ynCIdUvZ.js +0 -7
package/dist/server/app.js
CHANGED
|
@@ -3,17 +3,33 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
|
+
import { promisify } from "node:util";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
brotliDecompress,
|
|
10
|
+
gunzip,
|
|
11
|
+
inflate,
|
|
12
|
+
zstdDecompress
|
|
13
|
+
} from "node:zlib";
|
|
7
14
|
import Fastify from "fastify";
|
|
8
15
|
import cors from "@fastify/cors";
|
|
9
16
|
import { z } from "zod";
|
|
10
17
|
import { createGatewayContext } from "../core/context.js";
|
|
11
|
-
import { requestText } from "../core/providers/http-client.js";
|
|
18
|
+
import { isTransientHttpError, requestText } from "../core/providers/http-client.js";
|
|
12
19
|
import { streamOpenAICodex } from "../core/providers/openai-codex/chat.js";
|
|
20
|
+
import { generateChatGPTWebImage } from "../core/providers/openai-codex/chatgpt-web-image.js";
|
|
13
21
|
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
14
22
|
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
15
23
|
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
24
|
+
const BYTES_PER_MIB = 1024 * 1024;
|
|
16
25
|
const MAX_GATEWAY_REQUEST_LOGS = 100;
|
|
26
|
+
const MAX_CODEX_RESPONSE_PROFILE_BINDINGS = 5e3;
|
|
27
|
+
const DEFAULT_ROUTE_BODY_LIMIT_BYTES = 128 * BYTES_PER_MIB;
|
|
28
|
+
const CODEX_COMPACT_BODY_LIMIT_BYTES = 256 * BYTES_PER_MIB;
|
|
29
|
+
const gunzipAsync = promisify(gunzip);
|
|
30
|
+
const inflateAsync = promisify(inflate);
|
|
31
|
+
const brotliDecompressAsync = promisify(brotliDecompress);
|
|
32
|
+
const zstdDecompressAsync = typeof zstdDecompress === "function" ? promisify(zstdDecompress) : null;
|
|
17
33
|
const assetContentTypes = {
|
|
18
34
|
".css": "text/css; charset=utf-8",
|
|
19
35
|
".gif": "image/gif",
|
|
@@ -31,6 +47,35 @@ const assetContentTypes = {
|
|
|
31
47
|
function getContentType(filePath) {
|
|
32
48
|
return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
33
49
|
}
|
|
50
|
+
async function decodeJsonRequestBody(body, contentEncoding) {
|
|
51
|
+
const encodings = (Array.isArray(contentEncoding) ? contentEncoding.join(",") : contentEncoding ?? "").split(",").map((item) => item.trim().toLowerCase()).filter((item) => item && item !== "identity");
|
|
52
|
+
let decoded = body;
|
|
53
|
+
for (const encoding of encodings.reverse()) {
|
|
54
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
55
|
+
decoded = await gunzipAsync(decoded);
|
|
56
|
+
} else if (encoding === "deflate") {
|
|
57
|
+
decoded = await inflateAsync(decoded);
|
|
58
|
+
} else if (encoding === "br") {
|
|
59
|
+
decoded = await brotliDecompressAsync(decoded);
|
|
60
|
+
} else if (encoding === "zstd") {
|
|
61
|
+
if (!zstdDecompressAsync) {
|
|
62
|
+
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");
|
|
63
|
+
}
|
|
64
|
+
decoded = await zstdDecompressAsync(decoded);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u4F53\u538B\u7F29\u683C\u5F0F: ${encoding}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return decoded;
|
|
70
|
+
}
|
|
71
|
+
async function parseJsonRequestBody(request, body) {
|
|
72
|
+
const rawBody = typeof body === "string" ? Buffer.from(body) : body;
|
|
73
|
+
if (rawBody.length === 0) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
const decoded = await decodeJsonRequestBody(rawBody, request.headers["content-encoding"]);
|
|
77
|
+
return JSON.parse(decoded.toString("utf8"));
|
|
78
|
+
}
|
|
34
79
|
async function readAdminUiAsset(assetPath) {
|
|
35
80
|
const normalized = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
36
81
|
const filePath = path.resolve(adminUiDistDir, normalized);
|
|
@@ -114,6 +159,9 @@ const settingsUpdateSchema = z.object({
|
|
|
114
159
|
runtime: z.object({
|
|
115
160
|
quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
|
|
116
161
|
}).optional(),
|
|
162
|
+
image: z.object({
|
|
163
|
+
freeAccountWebGenerationEnabled: z.boolean().optional()
|
|
164
|
+
}).optional(),
|
|
117
165
|
server: z.object({
|
|
118
166
|
port: z.number().int().min(1).max(65535)
|
|
119
167
|
}).optional()
|
|
@@ -198,6 +246,82 @@ const imageEditsBodySchema = z.object({
|
|
|
198
246
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
199
247
|
user: z.string().optional()
|
|
200
248
|
}).passthrough();
|
|
249
|
+
function isObjectRecord(value) {
|
|
250
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
251
|
+
}
|
|
252
|
+
function tokenNumber(value) {
|
|
253
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
|
|
254
|
+
}
|
|
255
|
+
function normalizeTokenUsage(value) {
|
|
256
|
+
if (!isObjectRecord(value)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const inputTokens = tokenNumber(value.input_tokens ?? value.prompt_tokens);
|
|
260
|
+
const outputTokens = tokenNumber(value.output_tokens ?? value.completion_tokens);
|
|
261
|
+
const totalTokens = tokenNumber(value.total_tokens) ?? (inputTokens !== null || outputTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) : null);
|
|
262
|
+
if (inputTokens === null && outputTokens === null && totalTokens === null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
inputTokens,
|
|
267
|
+
outputTokens,
|
|
268
|
+
totalTokens
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function extractTokenUsage(value, depth = 0) {
|
|
272
|
+
if (depth > 5 || !value) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
for (const item of value) {
|
|
277
|
+
const usage = extractTokenUsage(item, depth + 1);
|
|
278
|
+
if (usage) {
|
|
279
|
+
return usage;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (!isObjectRecord(value)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const direct = normalizeTokenUsage(value);
|
|
288
|
+
if (direct) {
|
|
289
|
+
return direct;
|
|
290
|
+
}
|
|
291
|
+
for (const key of ["usage", "response", "events"]) {
|
|
292
|
+
const usage = extractTokenUsage(value[key], depth + 1);
|
|
293
|
+
if (usage) {
|
|
294
|
+
return usage;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function imageUsageToTokenUsage(usage) {
|
|
300
|
+
if (!usage) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
inputTokens: usage.input_tokens,
|
|
305
|
+
outputTokens: usage.output_tokens,
|
|
306
|
+
totalTokens: usage.total_tokens
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function extractUsageErrorType(details, statusCode) {
|
|
310
|
+
const error = isObjectRecord(details?.error) ? details.error : null;
|
|
311
|
+
const upstreamErrorCode = error?.upstreamErrorCode;
|
|
312
|
+
const upstreamStatus = error?.upstreamStatus;
|
|
313
|
+
const type = error?.type;
|
|
314
|
+
if (typeof upstreamErrorCode === "string" && upstreamErrorCode.trim()) {
|
|
315
|
+
return upstreamErrorCode.trim();
|
|
316
|
+
}
|
|
317
|
+
if (typeof type === "string" && type.trim()) {
|
|
318
|
+
return type.trim();
|
|
319
|
+
}
|
|
320
|
+
if (typeof upstreamStatus === "number") {
|
|
321
|
+
return `HTTP ${upstreamStatus}`;
|
|
322
|
+
}
|
|
323
|
+
return statusCode >= 400 ? `HTTP ${statusCode}` : void 0;
|
|
324
|
+
}
|
|
201
325
|
function extractTextFromInputContent(content) {
|
|
202
326
|
if (typeof content === "string" && content.trim()) {
|
|
203
327
|
return [content.trim()];
|
|
@@ -232,6 +356,49 @@ function extractTextInput(input) {
|
|
|
232
356
|
}
|
|
233
357
|
return chunks.join("\n").trim();
|
|
234
358
|
}
|
|
359
|
+
function extractImageUrlFromInputPart(part) {
|
|
360
|
+
if (!isObjectRecord(part)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const imageUrl = part.image_url ?? part.imageUrl;
|
|
364
|
+
if (typeof imageUrl === "string" && imageUrl.trim()) {
|
|
365
|
+
return imageUrl.trim();
|
|
366
|
+
}
|
|
367
|
+
if (isObjectRecord(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.trim()) {
|
|
368
|
+
return imageUrl.url.trim();
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function extractImageInputs(input) {
|
|
373
|
+
const images = [];
|
|
374
|
+
const addImage = (imageUrl) => {
|
|
375
|
+
if (imageUrl && !images.some((item) => item.imageUrl === imageUrl)) {
|
|
376
|
+
images.push({ imageUrl });
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
if (!Array.isArray(input)) {
|
|
380
|
+
addImage(extractImageUrlFromInputPart(input));
|
|
381
|
+
return images;
|
|
382
|
+
}
|
|
383
|
+
for (const item of input) {
|
|
384
|
+
addImage(extractImageUrlFromInputPart(item));
|
|
385
|
+
if (!isObjectRecord(item)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const content = item.content;
|
|
389
|
+
if (Array.isArray(content)) {
|
|
390
|
+
for (const part of content) {
|
|
391
|
+
addImage(extractImageUrlFromInputPart(part));
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
addImage(extractImageUrlFromInputPart(content));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return images;
|
|
398
|
+
}
|
|
399
|
+
function isFreePlan(profile) {
|
|
400
|
+
return profile.quota?.planType?.toLowerCase() === "free";
|
|
401
|
+
}
|
|
235
402
|
function normalizeResponseInput(input) {
|
|
236
403
|
if (typeof input === "undefined") {
|
|
237
404
|
return void 0;
|
|
@@ -428,14 +595,14 @@ function summarizeToolNames(tools) {
|
|
|
428
595
|
}
|
|
429
596
|
const record = tool;
|
|
430
597
|
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
431
|
-
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : "";
|
|
598
|
+
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : typeof record.type === "string" ? record.type : "";
|
|
432
599
|
}).filter(Boolean);
|
|
433
600
|
}
|
|
434
|
-
function summarizeResponsesRequest(data) {
|
|
601
|
+
function summarizeResponsesRequest(data, endpoint = "/v1/responses") {
|
|
435
602
|
const input = data.input;
|
|
436
603
|
const toolNames = summarizeToolNames(Array.isArray(data.tools) ? data.tools : void 0);
|
|
437
604
|
return {
|
|
438
|
-
endpoint
|
|
605
|
+
endpoint,
|
|
439
606
|
model: data.model ?? "default",
|
|
440
607
|
stream: data.stream ?? false,
|
|
441
608
|
inputKind: typeof input === "string" ? "string" : Array.isArray(input) ? "array" : "override",
|
|
@@ -447,9 +614,23 @@ function summarizeResponsesRequest(data) {
|
|
|
447
614
|
toolNamesTruncated: toolNames.length > 50,
|
|
448
615
|
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
449
616
|
parallelToolCalls: data.parallel_tool_calls,
|
|
450
|
-
hasReasoning: Boolean(data.reasoning)
|
|
617
|
+
hasReasoning: Boolean(data.reasoning),
|
|
618
|
+
hasPreviousResponseId: Boolean(getPreviousResponseId(data))
|
|
451
619
|
};
|
|
452
620
|
}
|
|
621
|
+
function getPreviousResponseId(data) {
|
|
622
|
+
const direct = data.previous_response_id;
|
|
623
|
+
if (typeof direct === "string" && direct.trim()) {
|
|
624
|
+
return direct.trim();
|
|
625
|
+
}
|
|
626
|
+
const experimental = data.experimental_codex?.body?.previous_response_id;
|
|
627
|
+
return typeof experimental === "string" && experimental.trim() ? experimental.trim() : void 0;
|
|
628
|
+
}
|
|
629
|
+
function removePreviousResponseId(body) {
|
|
630
|
+
const next = { ...body };
|
|
631
|
+
delete next.previous_response_id;
|
|
632
|
+
return next;
|
|
633
|
+
}
|
|
453
634
|
function createResponsesCodexBody(data) {
|
|
454
635
|
const experimentalBody = data.experimental_codex?.body ?? {};
|
|
455
636
|
const body = {
|
|
@@ -471,6 +652,119 @@ function createCodexPassthroughBody(data, model) {
|
|
|
471
652
|
delete body.experimental_codex;
|
|
472
653
|
return body;
|
|
473
654
|
}
|
|
655
|
+
function getImageGenerationTool(body) {
|
|
656
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
657
|
+
for (const tool of tools) {
|
|
658
|
+
if (isObjectRecord(tool) && tool.type === "image_generation") {
|
|
659
|
+
return tool;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
function hasImageGenerationToolChoice(body) {
|
|
665
|
+
const choice = body.tool_choice;
|
|
666
|
+
if (typeof choice === "string") {
|
|
667
|
+
return choice === "image_generation";
|
|
668
|
+
}
|
|
669
|
+
return isObjectRecord(choice) && choice.type === "image_generation";
|
|
670
|
+
}
|
|
671
|
+
function normalizeImageOutputFormat(value) {
|
|
672
|
+
return value === "png" || value === "webp" || value === "jpeg" ? value : void 0;
|
|
673
|
+
}
|
|
674
|
+
function extractCodexImageGenerationRequest(body) {
|
|
675
|
+
const imageTool = getImageGenerationTool(body);
|
|
676
|
+
if (!hasImageGenerationToolChoice(body)) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
prompt: extractTextInput(body.input),
|
|
681
|
+
inputImages: extractImageInputs(body.input),
|
|
682
|
+
imageModel: typeof imageTool?.model === "string" && imageTool.model.trim() ? imageTool.model.trim() : "gpt-image-2",
|
|
683
|
+
size: typeof imageTool?.size === "string" && imageTool.size.trim() ? imageTool.size.trim() : void 0,
|
|
684
|
+
outputFormat: normalizeImageOutputFormat(imageTool?.output_format)
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async function writeResponsesSseBlock(reply, block) {
|
|
688
|
+
if (!reply.raw.write(block)) {
|
|
689
|
+
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
690
|
+
}
|
|
691
|
+
return Buffer.byteLength(block);
|
|
692
|
+
}
|
|
693
|
+
async function writeResponsesSseEvent(reply, eventName, payload) {
|
|
694
|
+
return writeResponsesSseBlock(reply, `event: ${eventName}
|
|
695
|
+
data: ${JSON.stringify(payload)}
|
|
696
|
+
|
|
697
|
+
`);
|
|
698
|
+
}
|
|
699
|
+
async function sendSyntheticCodexImageSse(params) {
|
|
700
|
+
const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
|
|
701
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
702
|
+
const outputFormat = params.result.output_format ?? params.requestedOutputFormat ?? "png";
|
|
703
|
+
const size = params.result.size ?? params.requestedSize;
|
|
704
|
+
const output = params.result.data.map((image, index) => ({
|
|
705
|
+
id: `ig_${randomUUID().replace(/-/g, "")}`,
|
|
706
|
+
type: "image_generation_call",
|
|
707
|
+
status: "completed",
|
|
708
|
+
result: image.b64_json,
|
|
709
|
+
revised_prompt: image.revised_prompt ?? params.prompt,
|
|
710
|
+
output_format: outputFormat,
|
|
711
|
+
...size ? { size } : {}
|
|
712
|
+
}));
|
|
713
|
+
let bytes = 0;
|
|
714
|
+
params.reply.raw.writeHead(200, {
|
|
715
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
716
|
+
"Cache-Control": "no-cache, no-transform",
|
|
717
|
+
Connection: "keep-alive",
|
|
718
|
+
"X-Accel-Buffering": "no"
|
|
719
|
+
});
|
|
720
|
+
params.reply.raw.flushHeaders?.();
|
|
721
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.created", {
|
|
722
|
+
type: "response.created",
|
|
723
|
+
response: {
|
|
724
|
+
id: responseId,
|
|
725
|
+
object: "response",
|
|
726
|
+
created_at: created,
|
|
727
|
+
model: params.model,
|
|
728
|
+
status: "in_progress",
|
|
729
|
+
output: []
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
733
|
+
const item = output[index];
|
|
734
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.output_item.added", {
|
|
735
|
+
type: "response.output_item.added",
|
|
736
|
+
output_index: index,
|
|
737
|
+
item: {
|
|
738
|
+
id: item.id,
|
|
739
|
+
type: item.type,
|
|
740
|
+
status: "in_progress"
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.output_item.done", {
|
|
744
|
+
type: "response.output_item.done",
|
|
745
|
+
output_index: index,
|
|
746
|
+
item
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
bytes += await writeResponsesSseEvent(params.reply, "response.completed", {
|
|
750
|
+
type: "response.completed",
|
|
751
|
+
response: {
|
|
752
|
+
id: responseId,
|
|
753
|
+
object: "response",
|
|
754
|
+
created_at: created,
|
|
755
|
+
model: params.model,
|
|
756
|
+
status: "completed",
|
|
757
|
+
output,
|
|
758
|
+
usage: null
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
bytes += await writeResponsesSseBlock(params.reply, "data: [DONE]\n\n");
|
|
762
|
+
params.reply.raw.end();
|
|
763
|
+
return {
|
|
764
|
+
bytes,
|
|
765
|
+
imageCount: output.length
|
|
766
|
+
};
|
|
767
|
+
}
|
|
474
768
|
function summarizeChatCompletionsRequest(data) {
|
|
475
769
|
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
476
770
|
const toolNames = summarizeToolNames(data.tools);
|
|
@@ -507,6 +801,42 @@ function summarizeCodexChatBody(body) {
|
|
|
507
801
|
hasReasoning: Boolean(body.reasoning)
|
|
508
802
|
};
|
|
509
803
|
}
|
|
804
|
+
async function buildOpenAIModelsResponse(ctx) {
|
|
805
|
+
return {
|
|
806
|
+
object: "list",
|
|
807
|
+
data: (await ctx.modelService.listModels()).map((model) => ({
|
|
808
|
+
id: model.id,
|
|
809
|
+
object: "model",
|
|
810
|
+
owned_by: model.provider
|
|
811
|
+
}))
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
async function buildCodexModelsResponse(ctx) {
|
|
815
|
+
const [models, catalog] = await Promise.all([
|
|
816
|
+
ctx.modelService.listModels(),
|
|
817
|
+
ctx.modelService.getCatalog()
|
|
818
|
+
]);
|
|
819
|
+
return {
|
|
820
|
+
fetched_at: catalog.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
821
|
+
models: models.map((model, index) => ({
|
|
822
|
+
slug: model.id,
|
|
823
|
+
display_name: model.name,
|
|
824
|
+
description: model.name,
|
|
825
|
+
default_reasoning_level: "medium",
|
|
826
|
+
supported_reasoning_levels: [
|
|
827
|
+
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
828
|
+
{ effort: "medium", description: "Balanced speed and reasoning" },
|
|
829
|
+
{ effort: "high", description: "Deeper reasoning" },
|
|
830
|
+
{ effort: "xhigh", description: "Extra deep reasoning" }
|
|
831
|
+
],
|
|
832
|
+
shell_type: "shell_command",
|
|
833
|
+
visibility: "list",
|
|
834
|
+
supported_in_api: true,
|
|
835
|
+
priority: index,
|
|
836
|
+
input_modalities: model.input
|
|
837
|
+
}))
|
|
838
|
+
};
|
|
839
|
+
}
|
|
510
840
|
function profileLogLabel(profile) {
|
|
511
841
|
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
512
842
|
}
|
|
@@ -827,18 +1157,35 @@ function getErrorStatusCode(error) {
|
|
|
827
1157
|
}
|
|
828
1158
|
return 500;
|
|
829
1159
|
}
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1160
|
+
function formatBytesAsMiB(bytes) {
|
|
1161
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) {
|
|
1162
|
+
return "\u672A\u77E5";
|
|
1163
|
+
}
|
|
1164
|
+
return `${Math.round(bytes / BYTES_PER_MIB * 10) / 10} MB`;
|
|
834
1165
|
}
|
|
835
1166
|
function createSseStreamStats() {
|
|
836
1167
|
return {
|
|
837
1168
|
buffer: "",
|
|
838
1169
|
bytes: 0,
|
|
839
|
-
completed: false
|
|
1170
|
+
completed: false,
|
|
1171
|
+
responseIds: /* @__PURE__ */ new Set(),
|
|
1172
|
+
tokenUsage: null
|
|
840
1173
|
};
|
|
841
1174
|
}
|
|
1175
|
+
function extractSseResponseId(value) {
|
|
1176
|
+
if (!isObjectRecord(value)) {
|
|
1177
|
+
return void 0;
|
|
1178
|
+
}
|
|
1179
|
+
const directId = value.id;
|
|
1180
|
+
if (typeof directId === "string" && directId.startsWith("resp_")) {
|
|
1181
|
+
return directId;
|
|
1182
|
+
}
|
|
1183
|
+
const response = value.response;
|
|
1184
|
+
if (isObjectRecord(response) && typeof response.id === "string" && response.id.startsWith("resp_")) {
|
|
1185
|
+
return response.id;
|
|
1186
|
+
}
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
842
1189
|
function trackSseChunk(stats, chunk) {
|
|
843
1190
|
const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : String(chunk);
|
|
844
1191
|
stats.bytes += Buffer.byteLength(text);
|
|
@@ -856,6 +1203,14 @@ function trackSseChunk(stats, chunk) {
|
|
|
856
1203
|
if (typeof parsed.type === "string") {
|
|
857
1204
|
eventType = parsed.type;
|
|
858
1205
|
}
|
|
1206
|
+
const responseId = extractSseResponseId(parsed);
|
|
1207
|
+
if (responseId) {
|
|
1208
|
+
stats.responseIds.add(responseId);
|
|
1209
|
+
}
|
|
1210
|
+
const tokenUsage = extractTokenUsage(parsed);
|
|
1211
|
+
if (tokenUsage) {
|
|
1212
|
+
stats.tokenUsage = tokenUsage;
|
|
1213
|
+
}
|
|
859
1214
|
} catch {
|
|
860
1215
|
}
|
|
861
1216
|
}
|
|
@@ -872,14 +1227,40 @@ function trackSseChunk(stats, chunk) {
|
|
|
872
1227
|
}
|
|
873
1228
|
}
|
|
874
1229
|
function createApp(params) {
|
|
1230
|
+
const defaultBodyLimit = params?.bodyLimit ?? DEFAULT_ROUTE_BODY_LIMIT_BYTES;
|
|
1231
|
+
const codexCompactBodyLimit = Math.max(defaultBodyLimit, CODEX_COMPACT_BODY_LIMIT_BYTES);
|
|
875
1232
|
const app = Fastify({
|
|
876
1233
|
logger: false,
|
|
877
|
-
bodyLimit:
|
|
1234
|
+
bodyLimit: defaultBodyLimit
|
|
878
1235
|
});
|
|
1236
|
+
app.removeContentTypeParser("application/json");
|
|
1237
|
+
app.addContentTypeParser(
|
|
1238
|
+
/^application\/(?:[\w!#$&^.+-]+\+)?json(?:\s*;.*)?$/i,
|
|
1239
|
+
{ parseAs: "buffer" },
|
|
1240
|
+
(request, body, done) => {
|
|
1241
|
+
parseJsonRequestBody(request, Buffer.isBuffer(body) ? body : Buffer.from(body)).then((parsed) => done(null, parsed)).catch((error) => done(error));
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
879
1244
|
const ctx = createGatewayContext();
|
|
880
1245
|
const gatewayRequestLogs = [];
|
|
1246
|
+
const codexResponseProfileBindings = /* @__PURE__ */ new Map();
|
|
1247
|
+
function rememberCodexResponseProfile(responseId, profile) {
|
|
1248
|
+
codexResponseProfileBindings.set(responseId, {
|
|
1249
|
+
profileId: profile.profileId,
|
|
1250
|
+
accountId: profile.accountId,
|
|
1251
|
+
seenAt: Date.now()
|
|
1252
|
+
});
|
|
1253
|
+
if (codexResponseProfileBindings.size <= MAX_CODEX_RESPONSE_PROFILE_BINDINGS) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const overflow = codexResponseProfileBindings.size - MAX_CODEX_RESPONSE_PROFILE_BINDINGS;
|
|
1257
|
+
const oldest = Array.from(codexResponseProfileBindings.entries()).sort((left, right) => left[1].seenAt - right[1].seenAt).slice(0, overflow);
|
|
1258
|
+
for (const [key] of oldest) {
|
|
1259
|
+
codexResponseProfileBindings.delete(key);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
881
1262
|
function pushGatewayRequestLog(log) {
|
|
882
|
-
|
|
1263
|
+
const entry = {
|
|
883
1264
|
id: log.id ?? randomUUID(),
|
|
884
1265
|
time: log.time ?? Date.now(),
|
|
885
1266
|
method: log.method,
|
|
@@ -890,10 +1271,34 @@ function createApp(params) {
|
|
|
890
1271
|
durationMs: log.durationMs,
|
|
891
1272
|
source: log.source,
|
|
892
1273
|
details: log.details
|
|
893
|
-
}
|
|
1274
|
+
};
|
|
1275
|
+
gatewayRequestLogs.unshift(entry);
|
|
894
1276
|
if (gatewayRequestLogs.length > MAX_GATEWAY_REQUEST_LOGS) {
|
|
895
1277
|
gatewayRequestLogs.length = MAX_GATEWAY_REQUEST_LOGS;
|
|
896
1278
|
}
|
|
1279
|
+
const profile = log.usage?.profile ?? void 0;
|
|
1280
|
+
const usageEvent = {
|
|
1281
|
+
id: entry.id,
|
|
1282
|
+
timestamp: entry.time,
|
|
1283
|
+
method: entry.method,
|
|
1284
|
+
endpoint: entry.endpoint,
|
|
1285
|
+
model: entry.model,
|
|
1286
|
+
source: entry.source,
|
|
1287
|
+
statusCode: entry.statusCode,
|
|
1288
|
+
durationMs: entry.durationMs,
|
|
1289
|
+
success: entry.statusCode >= 200 && entry.statusCode < 400,
|
|
1290
|
+
profileId: profile?.profileId,
|
|
1291
|
+
accountId: profile?.accountId,
|
|
1292
|
+
accountLabel: entry.account,
|
|
1293
|
+
planType: profile?.quota?.planType,
|
|
1294
|
+
tokenUsage: log.usage?.tokenUsage,
|
|
1295
|
+
imageCount: log.usage?.imageCount,
|
|
1296
|
+
imageRoute: log.usage?.imageRoute ?? "none",
|
|
1297
|
+
errorType: log.usage?.errorType ?? extractUsageErrorType(log.details, entry.statusCode)
|
|
1298
|
+
};
|
|
1299
|
+
ctx.usageService.record(usageEvent).catch((error) => {
|
|
1300
|
+
console.warn("[gateway:usage] \u7EDF\u8BA1\u5199\u5165\u5931\u8D25", error);
|
|
1301
|
+
});
|
|
897
1302
|
}
|
|
898
1303
|
void app.register(cors, {
|
|
899
1304
|
origin: params?.corsOrigin ?? true,
|
|
@@ -902,26 +1307,31 @@ function createApp(params) {
|
|
|
902
1307
|
app.setErrorHandler((error, request, reply) => {
|
|
903
1308
|
const normalized = normalizeError(error);
|
|
904
1309
|
const statusCode = getErrorStatusCode(normalized);
|
|
1310
|
+
const isBodyTooLarge = statusCode === 413;
|
|
1311
|
+
const message = isBodyTooLarge ? `\u8BF7\u6C42\u4F53\u8FC7\u5927\uFF0C\u5F53\u524D\u7F51\u5173\u9ED8\u8BA4\u4E0A\u9650 ${formatBytesAsMiB(defaultBodyLimit)}\uFF0CCodex compact \u4E0A\u9650 ${formatBytesAsMiB(codexCompactBodyLimit)}\u3002\u5982\u4ECD\u4E0D\u591F\uFF0C\u8BF7\u7528 AZT_BODY_LIMIT_MB \u8C03\u5927\u540E\u91CD\u542F\u7F51\u5173\u3002` : normalized.message;
|
|
905
1312
|
console.error("[gateway:error]", {
|
|
906
1313
|
method: request.method,
|
|
907
1314
|
url: request.url,
|
|
908
1315
|
statusCode,
|
|
909
|
-
message
|
|
1316
|
+
message,
|
|
1317
|
+
code: normalized.code,
|
|
1318
|
+
upstreamRequestId: normalized.requestId,
|
|
910
1319
|
stack: normalized.stack
|
|
911
1320
|
});
|
|
912
1321
|
reply.code(statusCode);
|
|
913
1322
|
return {
|
|
914
1323
|
error: {
|
|
915
1324
|
type: "gateway_error",
|
|
916
|
-
message
|
|
1325
|
+
message
|
|
917
1326
|
}
|
|
918
1327
|
};
|
|
919
1328
|
});
|
|
920
1329
|
app.get("/_gateway/admin/request-logs", async () => ({
|
|
921
1330
|
data: gatewayRequestLogs
|
|
922
1331
|
}));
|
|
1332
|
+
app.get("/_gateway/admin/usage", async () => ctx.usageService.getSummary());
|
|
923
1333
|
async function buildAdminConfig(request) {
|
|
924
|
-
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
1334
|
+
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus, usage] = await Promise.all([
|
|
925
1335
|
ctx.authService.getStatus(),
|
|
926
1336
|
ctx.modelService.listModels(),
|
|
927
1337
|
ctx.modelService.getCatalog(),
|
|
@@ -929,7 +1339,8 @@ function createApp(params) {
|
|
|
929
1339
|
ctx.configService.getSettings(),
|
|
930
1340
|
ctx.authService.getActiveProfile(),
|
|
931
1341
|
ctx.authService.listProfiles(),
|
|
932
|
-
ctx.authService.getCodexStatus()
|
|
1342
|
+
ctx.authService.getCodexStatus(),
|
|
1343
|
+
ctx.usageService.getSummary()
|
|
933
1344
|
]);
|
|
934
1345
|
const origin = resolveOrigin(request);
|
|
935
1346
|
return {
|
|
@@ -941,6 +1352,7 @@ function createApp(params) {
|
|
|
941
1352
|
profile: serializeProfile(profile),
|
|
942
1353
|
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
943
1354
|
codex: codexStatus,
|
|
1355
|
+
usage,
|
|
944
1356
|
adminUrl: `${origin}/`,
|
|
945
1357
|
baseUrl: `${origin}/v1`,
|
|
946
1358
|
codexBaseUrl: `${origin}/codex/v1`,
|
|
@@ -962,6 +1374,11 @@ function createApp(params) {
|
|
|
962
1374
|
path: "/codex/v1/responses",
|
|
963
1375
|
description: "Codex custom provider \u4E13\u7528 Responses SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
964
1376
|
},
|
|
1377
|
+
{
|
|
1378
|
+
method: "POST",
|
|
1379
|
+
path: "/codex/v1/responses/compact",
|
|
1380
|
+
description: "Codex custom provider \u4E13\u7528 Responses compact SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
1381
|
+
},
|
|
965
1382
|
{
|
|
966
1383
|
method: "POST",
|
|
967
1384
|
path: "/v1/chat/completions",
|
|
@@ -1409,21 +1826,28 @@ function createApp(params) {
|
|
|
1409
1826
|
return ctx.githubImageBedService.deleteHistoryItem(parsed.data.id);
|
|
1410
1827
|
});
|
|
1411
1828
|
app.delete("/_gateway/image-bed/history", async () => ctx.githubImageBedService.clearHistory());
|
|
1412
|
-
app.get("/v1/models", async () => (
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1829
|
+
app.get("/v1/models", async () => buildOpenAIModelsResponse(ctx));
|
|
1830
|
+
app.get("/codex/v1/models", async () => buildCodexModelsResponse(ctx));
|
|
1831
|
+
app.get("/codex/v1/responses", async (_request, reply) => {
|
|
1832
|
+
reply.code(426);
|
|
1833
|
+
return {
|
|
1834
|
+
error: {
|
|
1835
|
+
type: "websocket_unsupported",
|
|
1836
|
+
message: "AI Zero Token \u5F53\u524D\u901A\u8FC7 HTTP SSE \u8F6C\u53D1 Codex Responses \u8BF7\u6C42\u3002"
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
});
|
|
1840
|
+
async function handleCodexResponsesPassthrough(request, reply, data, startedAt, upstreamEndpoint = "responses") {
|
|
1421
1841
|
const abortController = new AbortController();
|
|
1422
1842
|
let streamFinished = false;
|
|
1423
1843
|
let headersCommitted = false;
|
|
1424
1844
|
let profile = null;
|
|
1425
1845
|
let retryCount = 0;
|
|
1426
1846
|
let failureRecorded = false;
|
|
1847
|
+
let codexImageRoute = "none";
|
|
1848
|
+
const originalPreviousResponseId = getPreviousResponseId(data);
|
|
1849
|
+
let adventureFallbackUsed = false;
|
|
1850
|
+
let adventureFallbackReason;
|
|
1427
1851
|
reply.raw.on("close", () => {
|
|
1428
1852
|
if (!streamFinished) {
|
|
1429
1853
|
abortController.abort();
|
|
@@ -1433,25 +1857,162 @@ function createApp(params) {
|
|
|
1433
1857
|
const model = await ctx.modelService.resolveModel("openai-codex", data.model, {
|
|
1434
1858
|
allowUnknown: data.experimental_codex?.allow_unknown_model
|
|
1435
1859
|
});
|
|
1436
|
-
|
|
1860
|
+
let codexBody = createCodexPassthroughBody(data, model);
|
|
1861
|
+
let activePreviousResponseId = originalPreviousResponseId;
|
|
1862
|
+
let keepProfileSticky = Boolean(activePreviousResponseId);
|
|
1863
|
+
let stickyProfileId = activePreviousResponseId ? codexResponseProfileBindings.get(activePreviousResponseId)?.profileId : void 0;
|
|
1864
|
+
const useAdventureFallback = async (error, quota) => {
|
|
1865
|
+
if (!keepProfileSticky || abortController.signal.aborted) {
|
|
1866
|
+
return false;
|
|
1867
|
+
}
|
|
1868
|
+
const failedProfileId = profile?.profileId ?? stickyProfileId;
|
|
1869
|
+
if (failedProfileId) {
|
|
1870
|
+
await ctx.authService.recordProfileRequestFailure(failedProfileId, error, quota, "openai-codex", {
|
|
1871
|
+
skipAutoSwitch: true
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
codexBody = removePreviousResponseId(codexBody);
|
|
1875
|
+
activePreviousResponseId = void 0;
|
|
1876
|
+
keepProfileSticky = false;
|
|
1877
|
+
stickyProfileId = void 0;
|
|
1878
|
+
adventureFallbackUsed = true;
|
|
1879
|
+
adventureFallbackReason = error instanceof Error ? error.message : String(error);
|
|
1880
|
+
retryCount += 1;
|
|
1881
|
+
profile = null;
|
|
1882
|
+
failureRecorded = false;
|
|
1883
|
+
console.warn("[gateway:codex:stream] sticky continuation failed; dropping previous_response_id and retrying as new session", {
|
|
1884
|
+
requestId: request.id,
|
|
1885
|
+
model,
|
|
1886
|
+
retryCount,
|
|
1887
|
+
previousResponseId: "[present]",
|
|
1888
|
+
failedAccount: failedProfileId,
|
|
1889
|
+
errorCode: error.code,
|
|
1890
|
+
upstreamStatus: error.upstreamStatus,
|
|
1891
|
+
upstreamRequestId: error.requestId,
|
|
1892
|
+
message: adventureFallbackReason
|
|
1893
|
+
});
|
|
1894
|
+
return true;
|
|
1895
|
+
};
|
|
1896
|
+
const imageRequest = upstreamEndpoint === "responses" ? extractCodexImageGenerationRequest(codexBody) : null;
|
|
1897
|
+
if (imageRequest) {
|
|
1898
|
+
codexImageRoute = "codex-tool";
|
|
1899
|
+
const settings = await ctx.configService.getSettings();
|
|
1900
|
+
if (settings.image.freeAccountWebGenerationEnabled) {
|
|
1901
|
+
profile = await ctx.authService.requireUsableProfile("openai-codex", {
|
|
1902
|
+
skipAutoSwitch: true
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
if (profile && isFreePlan(profile)) {
|
|
1906
|
+
if (!imageRequest.prompt) {
|
|
1907
|
+
throw new Error("Codex \u751F\u56FE\u8BF7\u6C42\u7F3A\u5C11\u6587\u672C prompt\u3002");
|
|
1908
|
+
}
|
|
1909
|
+
console.info("[gateway:codex:image] using ChatGPT web image route for Free profile", {
|
|
1910
|
+
requestId: request.id,
|
|
1911
|
+
account: profileLogLabel(profile),
|
|
1912
|
+
model,
|
|
1913
|
+
imageModel: imageRequest.imageModel,
|
|
1914
|
+
promptLength: imageRequest.prompt.length,
|
|
1915
|
+
inputImageCount: imageRequest.inputImages.length,
|
|
1916
|
+
size: imageRequest.size ?? "default"
|
|
1917
|
+
});
|
|
1918
|
+
const imageResult = await generateChatGPTWebImage({
|
|
1919
|
+
profile,
|
|
1920
|
+
prompt: imageRequest.prompt,
|
|
1921
|
+
model: imageRequest.imageModel,
|
|
1922
|
+
inputImages: imageRequest.inputImages,
|
|
1923
|
+
size: imageRequest.size,
|
|
1924
|
+
responseFormat: "b64_json"
|
|
1925
|
+
});
|
|
1926
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, void 0, "openai-codex", {
|
|
1927
|
+
skipAutoSwitch: true
|
|
1928
|
+
});
|
|
1929
|
+
headersCommitted = true;
|
|
1930
|
+
const syntheticStats = await sendSyntheticCodexImageSse({
|
|
1931
|
+
reply,
|
|
1932
|
+
result: imageResult,
|
|
1933
|
+
model,
|
|
1934
|
+
prompt: imageRequest.prompt,
|
|
1935
|
+
requestedSize: imageRequest.size,
|
|
1936
|
+
requestedOutputFormat: imageRequest.outputFormat
|
|
1937
|
+
});
|
|
1938
|
+
streamFinished = true;
|
|
1939
|
+
pushGatewayRequestLog({
|
|
1940
|
+
method: request.method,
|
|
1941
|
+
endpoint: request.url,
|
|
1942
|
+
account: profileLogLabel(profile),
|
|
1943
|
+
model,
|
|
1944
|
+
statusCode: 200,
|
|
1945
|
+
durationMs: performance.now() - startedAt,
|
|
1946
|
+
source: "Codex",
|
|
1947
|
+
details: {
|
|
1948
|
+
requestId: request.id,
|
|
1949
|
+
remoteAddress: request.ip,
|
|
1950
|
+
userAgent: request.headers["user-agent"],
|
|
1951
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1952
|
+
response: {
|
|
1953
|
+
stream: true,
|
|
1954
|
+
passthrough: false,
|
|
1955
|
+
upstreamEndpoint,
|
|
1956
|
+
route: "chatgpt-web-image",
|
|
1957
|
+
imageModel: imageRequest.imageModel,
|
|
1958
|
+
imageCount: syntheticStats.imageCount,
|
|
1959
|
+
bytes: syntheticStats.bytes
|
|
1960
|
+
}
|
|
1961
|
+
},
|
|
1962
|
+
usage: {
|
|
1963
|
+
profile,
|
|
1964
|
+
imageCount: syntheticStats.imageCount,
|
|
1965
|
+
imageRoute: "chatgpt-web"
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
return reply;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1437
1971
|
let upstream = null;
|
|
1438
1972
|
const maxProfileAttempts = 5;
|
|
1973
|
+
const maxTransientStreamRetries = 1;
|
|
1974
|
+
let transientStreamRetryCount = 0;
|
|
1439
1975
|
for (let attempt = 0; attempt < maxProfileAttempts; attempt += 1) {
|
|
1440
|
-
profile = await ctx.authService.requireUsableProfile("openai-codex");
|
|
1441
1976
|
try {
|
|
1977
|
+
profile = stickyProfileId ? await ctx.authService.requireUsableProfileById(stickyProfileId, "openai-codex") : await ctx.authService.requireUsableProfile("openai-codex", {
|
|
1978
|
+
skipAutoSwitch: keepProfileSticky
|
|
1979
|
+
});
|
|
1442
1980
|
upstream = await streamOpenAICodex({
|
|
1443
1981
|
profile,
|
|
1444
1982
|
model,
|
|
1445
1983
|
bodyOverride: codexBody,
|
|
1984
|
+
endpoint: upstreamEndpoint,
|
|
1446
1985
|
passthroughBody: true,
|
|
1447
1986
|
signal: abortController.signal
|
|
1448
1987
|
});
|
|
1449
1988
|
break;
|
|
1450
1989
|
} catch (error) {
|
|
1451
1990
|
const quota = error.quota;
|
|
1452
|
-
|
|
1991
|
+
if (keepProfileSticky && attempt < maxProfileAttempts - 1 && await useAdventureFallback(error, quota)) {
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
if (!keepProfileSticky && isTransientHttpError(error) && transientStreamRetryCount < maxTransientStreamRetries && attempt < maxProfileAttempts - 1 && !abortController.signal.aborted) {
|
|
1995
|
+
transientStreamRetryCount += 1;
|
|
1996
|
+
retryCount += 1;
|
|
1997
|
+
console.warn("[gateway:codex:stream] transient curl stream failure before headers; retrying request", {
|
|
1998
|
+
requestId: request.id,
|
|
1999
|
+
account: profileLogLabel(profile),
|
|
2000
|
+
model,
|
|
2001
|
+
retryCount,
|
|
2002
|
+
errorCode: error.code,
|
|
2003
|
+
upstreamRequestId: error.requestId,
|
|
2004
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2005
|
+
});
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (!profile) {
|
|
2009
|
+
throw error;
|
|
2010
|
+
}
|
|
2011
|
+
const switchedProfile = await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex", {
|
|
2012
|
+
skipAutoSwitch: keepProfileSticky
|
|
2013
|
+
});
|
|
1453
2014
|
failureRecorded = true;
|
|
1454
|
-
if (attempt < maxProfileAttempts - 1 &&
|
|
2015
|
+
if (!keepProfileSticky && attempt < maxProfileAttempts - 1 && ctx.authService.isRotationTrigger(error, quota) && switchedProfile && switchedProfile.profileId !== profile.profileId && !abortController.signal.aborted) {
|
|
1455
2016
|
retryCount += 1;
|
|
1456
2017
|
failureRecorded = false;
|
|
1457
2018
|
continue;
|
|
@@ -1462,13 +2023,18 @@ function createApp(params) {
|
|
|
1462
2023
|
if (!upstream || !profile) {
|
|
1463
2024
|
throw new Error("Codex stream \u672A\u80FD\u5EFA\u7ACB\u3002");
|
|
1464
2025
|
}
|
|
1465
|
-
await ctx.authService.recordProfileRequestSuccess(profile.profileId, upstream.quota, "openai-codex"
|
|
2026
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, upstream.quota, "openai-codex", {
|
|
2027
|
+
skipAutoSwitch: keepProfileSticky
|
|
2028
|
+
});
|
|
1466
2029
|
const headers = {
|
|
1467
2030
|
"Content-Type": upstream.headers["content-type"] ?? "text/event-stream; charset=utf-8",
|
|
1468
2031
|
"Cache-Control": "no-cache, no-transform",
|
|
1469
2032
|
Connection: "keep-alive",
|
|
1470
2033
|
"X-Accel-Buffering": "no"
|
|
1471
2034
|
};
|
|
2035
|
+
if (adventureFallbackUsed) {
|
|
2036
|
+
headers["X-AZT-Codex-Continuation-Mode"] = "adventure-fallback";
|
|
2037
|
+
}
|
|
1472
2038
|
for (const [key, value] of Object.entries(upstream.headers)) {
|
|
1473
2039
|
if (key.startsWith("x-codex-") || key === "x-request-id") {
|
|
1474
2040
|
headers[key] = value;
|
|
@@ -1486,6 +2052,9 @@ function createApp(params) {
|
|
|
1486
2052
|
}
|
|
1487
2053
|
streamFinished = true;
|
|
1488
2054
|
reply.raw.end();
|
|
2055
|
+
for (const responseId of streamStats.responseIds) {
|
|
2056
|
+
rememberCodexResponseProfile(responseId, profile);
|
|
2057
|
+
}
|
|
1489
2058
|
if (!streamStats.completed) {
|
|
1490
2059
|
console.warn("[gateway:codex:stream] upstream stream ended without response.completed", {
|
|
1491
2060
|
requestId: request.id,
|
|
@@ -1509,22 +2078,37 @@ function createApp(params) {
|
|
|
1509
2078
|
upstreamRequestId: upstream.requestId,
|
|
1510
2079
|
remoteAddress: request.ip,
|
|
1511
2080
|
userAgent: request.headers["user-agent"],
|
|
1512
|
-
request: summarizeResponsesRequest(data),
|
|
2081
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1513
2082
|
response: {
|
|
1514
2083
|
stream: true,
|
|
1515
2084
|
passthrough: true,
|
|
2085
|
+
upstreamEndpoint,
|
|
1516
2086
|
retryCount,
|
|
2087
|
+
profileSticky: keepProfileSticky,
|
|
2088
|
+
previousResponseId: originalPreviousResponseId ? "[present]" : void 0,
|
|
2089
|
+
previousResponseDropped: adventureFallbackUsed,
|
|
2090
|
+
adventureFallbackReason: adventureFallbackUsed ? truncateForLog(adventureFallbackReason ?? "") : void 0,
|
|
2091
|
+
stickyProfileResolved: Boolean(stickyProfileId),
|
|
2092
|
+
responseIdsTracked: streamStats.responseIds.size,
|
|
1517
2093
|
completed: streamStats.completed,
|
|
1518
2094
|
terminalEvent: streamStats.terminalEvent,
|
|
1519
|
-
bytes: streamStats.bytes
|
|
2095
|
+
bytes: streamStats.bytes,
|
|
2096
|
+
usageCaptured: Boolean(streamStats.tokenUsage)
|
|
1520
2097
|
}
|
|
2098
|
+
},
|
|
2099
|
+
usage: {
|
|
2100
|
+
profile,
|
|
2101
|
+
tokenUsage: streamStats.tokenUsage,
|
|
2102
|
+
imageRoute: codexImageRoute
|
|
1521
2103
|
}
|
|
1522
2104
|
});
|
|
1523
2105
|
return reply;
|
|
1524
2106
|
} catch (error) {
|
|
1525
2107
|
const quota = error.quota;
|
|
1526
2108
|
if (profile && !failureRecorded) {
|
|
1527
|
-
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex"
|
|
2109
|
+
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex", {
|
|
2110
|
+
skipAutoSwitch: Boolean(originalPreviousResponseId) && !adventureFallbackUsed
|
|
2111
|
+
});
|
|
1528
2112
|
}
|
|
1529
2113
|
const normalized = normalizeError(error);
|
|
1530
2114
|
const statusCode = getErrorStatusCode(normalized);
|
|
@@ -1540,16 +2124,28 @@ function createApp(params) {
|
|
|
1540
2124
|
requestId: request.id,
|
|
1541
2125
|
remoteAddress: request.ip,
|
|
1542
2126
|
userAgent: request.headers["user-agent"],
|
|
1543
|
-
request: summarizeResponsesRequest(data),
|
|
2127
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1544
2128
|
response: {
|
|
1545
|
-
|
|
2129
|
+
upstreamEndpoint,
|
|
2130
|
+
retryCount,
|
|
2131
|
+
profileSticky: Boolean(originalPreviousResponseId) && !adventureFallbackUsed,
|
|
2132
|
+
previousResponseId: originalPreviousResponseId ? "[present]" : void 0,
|
|
2133
|
+
previousResponseDropped: adventureFallbackUsed,
|
|
2134
|
+
adventureFallbackReason: adventureFallbackUsed ? truncateForLog(adventureFallbackReason ?? "") : void 0,
|
|
2135
|
+
stickyProfileResolved: Boolean(originalPreviousResponseId && codexResponseProfileBindings.has(originalPreviousResponseId))
|
|
1546
2136
|
},
|
|
1547
2137
|
error: {
|
|
1548
2138
|
message: normalized.message,
|
|
2139
|
+
code: normalized.code,
|
|
2140
|
+
upstreamRequestId: normalized.requestId,
|
|
1549
2141
|
upstreamStatus: normalized.upstreamStatus,
|
|
1550
2142
|
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1551
2143
|
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1552
2144
|
}
|
|
2145
|
+
},
|
|
2146
|
+
usage: {
|
|
2147
|
+
profile,
|
|
2148
|
+
imageRoute: codexImageRoute
|
|
1553
2149
|
}
|
|
1554
2150
|
});
|
|
1555
2151
|
if (headersCommitted) {
|
|
@@ -1592,6 +2188,38 @@ function createApp(params) {
|
|
|
1592
2188
|
}
|
|
1593
2189
|
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt);
|
|
1594
2190
|
});
|
|
2191
|
+
app.post("/codex/v1/responses/compact", { bodyLimit: codexCompactBodyLimit }, async (request, reply) => {
|
|
2192
|
+
const startedAt = performance.now();
|
|
2193
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
2194
|
+
if (!parsed.success) {
|
|
2195
|
+
pushGatewayRequestLog({
|
|
2196
|
+
method: request.method,
|
|
2197
|
+
endpoint: request.url,
|
|
2198
|
+
account: "-",
|
|
2199
|
+
model: "-",
|
|
2200
|
+
statusCode: 400,
|
|
2201
|
+
durationMs: performance.now() - startedAt,
|
|
2202
|
+
source: "Codex",
|
|
2203
|
+
details: {
|
|
2204
|
+
requestId: request.id,
|
|
2205
|
+
remoteAddress: request.ip,
|
|
2206
|
+
userAgent: request.headers["user-agent"],
|
|
2207
|
+
error: {
|
|
2208
|
+
type: "validation_error",
|
|
2209
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
reply.code(400);
|
|
2214
|
+
return {
|
|
2215
|
+
error: {
|
|
2216
|
+
type: "validation_error",
|
|
2217
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt, "responses/compact");
|
|
2222
|
+
});
|
|
1595
2223
|
app.post("/v1/responses", async (request, reply) => {
|
|
1596
2224
|
const startedAt = performance.now();
|
|
1597
2225
|
const parsed = responsesBodySchema.safeParse(request.body);
|
|
@@ -1682,13 +2310,73 @@ function createApp(params) {
|
|
|
1682
2310
|
};
|
|
1683
2311
|
}
|
|
1684
2312
|
const codexBody = createResponsesCodexBody(parsed.data);
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2313
|
+
let result;
|
|
2314
|
+
try {
|
|
2315
|
+
result = await ctx.chatService.chat({
|
|
2316
|
+
model: parsed.data.model,
|
|
2317
|
+
input: input || void 0,
|
|
2318
|
+
system: parsed.data.instructions,
|
|
2319
|
+
experimental: {
|
|
2320
|
+
codexBody,
|
|
2321
|
+
allowUnknownModel: parsed.data.experimental_codex?.allow_unknown_model
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
const normalized = normalizeError(error);
|
|
2326
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2327
|
+
const activeProfile2 = await ctx.authService.getActiveProfile();
|
|
2328
|
+
pushGatewayRequestLog({
|
|
2329
|
+
method: request.method,
|
|
2330
|
+
endpoint: request.url,
|
|
2331
|
+
account: profileLogLabel(activeProfile2),
|
|
2332
|
+
model: parsed.data.model ?? "default",
|
|
2333
|
+
statusCode,
|
|
2334
|
+
durationMs: performance.now() - startedAt,
|
|
2335
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2336
|
+
details: {
|
|
2337
|
+
requestId: request.id,
|
|
2338
|
+
remoteAddress: request.ip,
|
|
2339
|
+
userAgent: request.headers["user-agent"],
|
|
2340
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2341
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
2342
|
+
error: {
|
|
2343
|
+
message: normalized.message,
|
|
2344
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2345
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2346
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2347
|
+
}
|
|
2348
|
+
},
|
|
2349
|
+
usage: {
|
|
2350
|
+
profile: activeProfile2
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
throw error;
|
|
2354
|
+
}
|
|
2355
|
+
const activeProfile = result.profile ?? await ctx.authService.getActiveProfile();
|
|
2356
|
+
pushGatewayRequestLog({
|
|
2357
|
+
method: request.method,
|
|
2358
|
+
endpoint: request.url,
|
|
2359
|
+
account: profileLogLabel(activeProfile),
|
|
2360
|
+
model: result.model,
|
|
2361
|
+
statusCode: 200,
|
|
2362
|
+
durationMs: performance.now() - startedAt,
|
|
2363
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2364
|
+
details: {
|
|
2365
|
+
requestId: request.id,
|
|
2366
|
+
remoteAddress: request.ip,
|
|
2367
|
+
userAgent: request.headers["user-agent"],
|
|
2368
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
2369
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
2370
|
+
response: {
|
|
2371
|
+
textPreview: truncateForLog(result.text),
|
|
2372
|
+
textLength: result.text.length,
|
|
2373
|
+
artifactCount: result.artifacts.length,
|
|
2374
|
+
retryCount: result.retryCount ?? 0
|
|
2375
|
+
}
|
|
2376
|
+
},
|
|
2377
|
+
usage: {
|
|
2378
|
+
profile: activeProfile,
|
|
2379
|
+
tokenUsage: extractTokenUsage(result.raw)
|
|
1692
2380
|
}
|
|
1693
2381
|
});
|
|
1694
2382
|
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
@@ -1772,10 +2460,11 @@ function createApp(params) {
|
|
|
1772
2460
|
} catch (error) {
|
|
1773
2461
|
const normalized = normalizeError(error);
|
|
1774
2462
|
const statusCode = getErrorStatusCode(normalized);
|
|
2463
|
+
const activeProfile2 = await ctx.authService.getActiveProfile();
|
|
1775
2464
|
pushGatewayRequestLog({
|
|
1776
2465
|
method: request.method,
|
|
1777
2466
|
endpoint: request.url,
|
|
1778
|
-
account: profileLogLabel(
|
|
2467
|
+
account: profileLogLabel(activeProfile2),
|
|
1779
2468
|
model: parsed.data.model ?? "default",
|
|
1780
2469
|
statusCode,
|
|
1781
2470
|
durationMs: performance.now() - startedAt,
|
|
@@ -1792,14 +2481,18 @@ function createApp(params) {
|
|
|
1792
2481
|
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1793
2482
|
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1794
2483
|
}
|
|
2484
|
+
},
|
|
2485
|
+
usage: {
|
|
2486
|
+
profile: activeProfile2
|
|
1795
2487
|
}
|
|
1796
2488
|
});
|
|
1797
2489
|
throw error;
|
|
1798
2490
|
}
|
|
2491
|
+
const activeProfile = result.profile ?? await ctx.authService.getActiveProfile();
|
|
1799
2492
|
pushGatewayRequestLog({
|
|
1800
2493
|
method: request.method,
|
|
1801
2494
|
endpoint: request.url,
|
|
1802
|
-
account: profileLogLabel(
|
|
2495
|
+
account: profileLogLabel(activeProfile),
|
|
1803
2496
|
model: result.model,
|
|
1804
2497
|
statusCode: 200,
|
|
1805
2498
|
durationMs: performance.now() - startedAt,
|
|
@@ -1820,8 +2513,13 @@ function createApp(params) {
|
|
|
1820
2513
|
argumentsPreview: truncateForLog(toolCall.function.arguments)
|
|
1821
2514
|
})),
|
|
1822
2515
|
artifactCount: result.artifacts.length,
|
|
1823
|
-
stream: parsed.data.stream ?? false
|
|
2516
|
+
stream: parsed.data.stream ?? false,
|
|
2517
|
+
retryCount: result.retryCount ?? 0
|
|
1824
2518
|
}
|
|
2519
|
+
},
|
|
2520
|
+
usage: {
|
|
2521
|
+
profile: activeProfile,
|
|
2522
|
+
tokenUsage: extractTokenUsage(result.raw)
|
|
1825
2523
|
}
|
|
1826
2524
|
});
|
|
1827
2525
|
console.info("[gateway:chat:response]", {
|
|
@@ -1840,6 +2538,7 @@ function createApp(params) {
|
|
|
1840
2538
|
return buildChatCompletionsBody(result);
|
|
1841
2539
|
});
|
|
1842
2540
|
app.post("/v1/images/generations", async (request, reply) => {
|
|
2541
|
+
const startedAt = performance.now();
|
|
1843
2542
|
const parsed = imageGenerationsBodySchema.safeParse(request.body);
|
|
1844
2543
|
if (!parsed.success) {
|
|
1845
2544
|
console.error("[gateway:image] validation failure", {
|
|
@@ -1847,6 +2546,24 @@ function createApp(params) {
|
|
|
1847
2546
|
url: request.url,
|
|
1848
2547
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1849
2548
|
});
|
|
2549
|
+
pushGatewayRequestLog({
|
|
2550
|
+
method: request.method,
|
|
2551
|
+
endpoint: request.url,
|
|
2552
|
+
account: "-",
|
|
2553
|
+
model: "-",
|
|
2554
|
+
statusCode: 400,
|
|
2555
|
+
durationMs: performance.now() - startedAt,
|
|
2556
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2557
|
+
details: {
|
|
2558
|
+
requestId: request.id,
|
|
2559
|
+
remoteAddress: request.ip,
|
|
2560
|
+
userAgent: request.headers["user-agent"],
|
|
2561
|
+
error: {
|
|
2562
|
+
type: "validation_error",
|
|
2563
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
1850
2567
|
reply.code(400);
|
|
1851
2568
|
return {
|
|
1852
2569
|
error: {
|
|
@@ -1863,6 +2580,25 @@ function createApp(params) {
|
|
|
1863
2580
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1864
2581
|
issue: validationError
|
|
1865
2582
|
});
|
|
2583
|
+
pushGatewayRequestLog({
|
|
2584
|
+
method: request.method,
|
|
2585
|
+
endpoint: request.url,
|
|
2586
|
+
account: "-",
|
|
2587
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2588
|
+
statusCode: 400,
|
|
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
|
+
request: summarizeImageRequestForLog(parsed.data),
|
|
2596
|
+
error: {
|
|
2597
|
+
type: "validation_error",
|
|
2598
|
+
message: validationError
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
1866
2602
|
reply.code(400);
|
|
1867
2603
|
return {
|
|
1868
2604
|
error: {
|
|
@@ -1878,6 +2614,25 @@ function createApp(params) {
|
|
|
1878
2614
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1879
2615
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1880
2616
|
});
|
|
2617
|
+
pushGatewayRequestLog({
|
|
2618
|
+
method: request.method,
|
|
2619
|
+
endpoint: request.url,
|
|
2620
|
+
account: "-",
|
|
2621
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2622
|
+
statusCode: 501,
|
|
2623
|
+
durationMs: performance.now() - startedAt,
|
|
2624
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2625
|
+
details: {
|
|
2626
|
+
requestId: request.id,
|
|
2627
|
+
remoteAddress: request.ip,
|
|
2628
|
+
userAgent: request.headers["user-agent"],
|
|
2629
|
+
request: summarizeImageRequestForLog(parsed.data),
|
|
2630
|
+
error: {
|
|
2631
|
+
type: "not_supported",
|
|
2632
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
1881
2636
|
reply.code(501);
|
|
1882
2637
|
return {
|
|
1883
2638
|
error: {
|
|
@@ -1892,17 +2647,52 @@ function createApp(params) {
|
|
|
1892
2647
|
url: request.url,
|
|
1893
2648
|
summary: requestSummary
|
|
1894
2649
|
});
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2650
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
2651
|
+
const settings = await ctx.configService.getSettings();
|
|
2652
|
+
const imageRoute = activeProfile && isFreePlan(activeProfile) && settings.image.freeAccountWebGenerationEnabled ? "chatgpt-web" : "codex-tool";
|
|
2653
|
+
let response;
|
|
2654
|
+
try {
|
|
2655
|
+
response = await ctx.imageService.generate({
|
|
2656
|
+
prompt: parsed.data.prompt,
|
|
2657
|
+
model: parsed.data.model,
|
|
2658
|
+
n: parsed.data.n,
|
|
2659
|
+
size: parsed.data.size,
|
|
2660
|
+
quality: parsed.data.quality,
|
|
2661
|
+
background: parsed.data.background,
|
|
2662
|
+
outputFormat: parsed.data.output_format,
|
|
2663
|
+
outputCompression: parsed.data.output_compression,
|
|
2664
|
+
moderation: parsed.data.moderation
|
|
2665
|
+
});
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
const normalized = normalizeError(error);
|
|
2668
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2669
|
+
pushGatewayRequestLog({
|
|
2670
|
+
method: request.method,
|
|
2671
|
+
endpoint: request.url,
|
|
2672
|
+
account: profileLogLabel(activeProfile),
|
|
2673
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2674
|
+
statusCode,
|
|
2675
|
+
durationMs: performance.now() - startedAt,
|
|
2676
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2677
|
+
details: {
|
|
2678
|
+
requestId: request.id,
|
|
2679
|
+
remoteAddress: request.ip,
|
|
2680
|
+
userAgent: request.headers["user-agent"],
|
|
2681
|
+
request: requestSummary,
|
|
2682
|
+
error: {
|
|
2683
|
+
message: normalized.message,
|
|
2684
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2685
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2686
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2687
|
+
}
|
|
2688
|
+
},
|
|
2689
|
+
usage: {
|
|
2690
|
+
profile: activeProfile,
|
|
2691
|
+
imageRoute
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
throw error;
|
|
2695
|
+
}
|
|
1906
2696
|
console.info("[gateway:image] response ready", {
|
|
1907
2697
|
method: request.method,
|
|
1908
2698
|
url: request.url,
|
|
@@ -1913,11 +2703,57 @@ function createApp(params) {
|
|
|
1913
2703
|
quality: response.quality,
|
|
1914
2704
|
size: response.size
|
|
1915
2705
|
});
|
|
2706
|
+
pushGatewayRequestLog({
|
|
2707
|
+
method: request.method,
|
|
2708
|
+
endpoint: request.url,
|
|
2709
|
+
account: profileLogLabel(activeProfile),
|
|
2710
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2711
|
+
statusCode: 200,
|
|
2712
|
+
durationMs: performance.now() - startedAt,
|
|
2713
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2714
|
+
details: {
|
|
2715
|
+
requestId: request.id,
|
|
2716
|
+
remoteAddress: request.ip,
|
|
2717
|
+
userAgent: request.headers["user-agent"],
|
|
2718
|
+
request: requestSummary,
|
|
2719
|
+
response: {
|
|
2720
|
+
imageCount: response.data.length,
|
|
2721
|
+
outputFormat: response.output_format,
|
|
2722
|
+
quality: response.quality,
|
|
2723
|
+
size: response.size
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
usage: {
|
|
2727
|
+
profile: activeProfile,
|
|
2728
|
+
tokenUsage: imageUsageToTokenUsage(response.usage),
|
|
2729
|
+
imageCount: response.data.length,
|
|
2730
|
+
imageRoute
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
1916
2733
|
return response;
|
|
1917
2734
|
});
|
|
1918
2735
|
app.post("/v1/images/edits", async (request, reply) => {
|
|
2736
|
+
const startedAt = performance.now();
|
|
1919
2737
|
const contentType = request.headers["content-type"] ?? "";
|
|
1920
2738
|
if (!String(contentType).toLowerCase().includes("application/json")) {
|
|
2739
|
+
pushGatewayRequestLog({
|
|
2740
|
+
method: request.method,
|
|
2741
|
+
endpoint: request.url,
|
|
2742
|
+
account: "-",
|
|
2743
|
+
model: "-",
|
|
2744
|
+
statusCode: 415,
|
|
2745
|
+
durationMs: performance.now() - startedAt,
|
|
2746
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2747
|
+
details: {
|
|
2748
|
+
requestId: request.id,
|
|
2749
|
+
remoteAddress: request.ip,
|
|
2750
|
+
userAgent: request.headers["user-agent"],
|
|
2751
|
+
error: {
|
|
2752
|
+
type: "unsupported_media_type",
|
|
2753
|
+
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"
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
1921
2757
|
reply.code(415);
|
|
1922
2758
|
return {
|
|
1923
2759
|
error: {
|
|
@@ -1933,6 +2769,24 @@ function createApp(params) {
|
|
|
1933
2769
|
url: request.url,
|
|
1934
2770
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1935
2771
|
});
|
|
2772
|
+
pushGatewayRequestLog({
|
|
2773
|
+
method: request.method,
|
|
2774
|
+
endpoint: request.url,
|
|
2775
|
+
account: "-",
|
|
2776
|
+
model: "-",
|
|
2777
|
+
statusCode: 400,
|
|
2778
|
+
durationMs: performance.now() - startedAt,
|
|
2779
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2780
|
+
details: {
|
|
2781
|
+
requestId: request.id,
|
|
2782
|
+
remoteAddress: request.ip,
|
|
2783
|
+
userAgent: request.headers["user-agent"],
|
|
2784
|
+
error: {
|
|
2785
|
+
type: "validation_error",
|
|
2786
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
1936
2790
|
reply.code(400);
|
|
1937
2791
|
return {
|
|
1938
2792
|
error: {
|
|
@@ -1949,6 +2803,25 @@ function createApp(params) {
|
|
|
1949
2803
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1950
2804
|
issue: validationError
|
|
1951
2805
|
});
|
|
2806
|
+
pushGatewayRequestLog({
|
|
2807
|
+
method: request.method,
|
|
2808
|
+
endpoint: request.url,
|
|
2809
|
+
account: "-",
|
|
2810
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2811
|
+
statusCode: 400,
|
|
2812
|
+
durationMs: performance.now() - startedAt,
|
|
2813
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2814
|
+
details: {
|
|
2815
|
+
requestId: request.id,
|
|
2816
|
+
remoteAddress: request.ip,
|
|
2817
|
+
userAgent: request.headers["user-agent"],
|
|
2818
|
+
request: summarizeImageEditRequestForLog(parsed.data),
|
|
2819
|
+
error: {
|
|
2820
|
+
type: "validation_error",
|
|
2821
|
+
message: validationError
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
1952
2825
|
reply.code(400);
|
|
1953
2826
|
return {
|
|
1954
2827
|
error: {
|
|
@@ -1964,6 +2837,25 @@ function createApp(params) {
|
|
|
1964
2837
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1965
2838
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1966
2839
|
});
|
|
2840
|
+
pushGatewayRequestLog({
|
|
2841
|
+
method: request.method,
|
|
2842
|
+
endpoint: request.url,
|
|
2843
|
+
account: "-",
|
|
2844
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2845
|
+
statusCode: 501,
|
|
2846
|
+
durationMs: performance.now() - startedAt,
|
|
2847
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2848
|
+
details: {
|
|
2849
|
+
requestId: request.id,
|
|
2850
|
+
remoteAddress: request.ip,
|
|
2851
|
+
userAgent: request.headers["user-agent"],
|
|
2852
|
+
request: summarizeImageEditRequestForLog(parsed.data),
|
|
2853
|
+
error: {
|
|
2854
|
+
type: "not_supported",
|
|
2855
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
1967
2859
|
reply.code(501);
|
|
1968
2860
|
return {
|
|
1969
2861
|
error: {
|
|
@@ -1981,18 +2873,53 @@ function createApp(params) {
|
|
|
1981
2873
|
url: request.url,
|
|
1982
2874
|
summary: requestSummary
|
|
1983
2875
|
});
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2876
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
2877
|
+
const settings = await ctx.configService.getSettings();
|
|
2878
|
+
const imageRoute = activeProfile && isFreePlan(activeProfile) && settings.image.freeAccountWebGenerationEnabled ? "chatgpt-web" : "codex-tool";
|
|
2879
|
+
let response;
|
|
2880
|
+
try {
|
|
2881
|
+
response = await ctx.imageService.generate({
|
|
2882
|
+
prompt: parsed.data.prompt,
|
|
2883
|
+
inputImages: imageReferences,
|
|
2884
|
+
model: parsed.data.model,
|
|
2885
|
+
n: parsed.data.n,
|
|
2886
|
+
size: parsed.data.size,
|
|
2887
|
+
quality: parsed.data.quality,
|
|
2888
|
+
background: parsed.data.background,
|
|
2889
|
+
outputFormat: parsed.data.output_format,
|
|
2890
|
+
outputCompression: parsed.data.output_compression,
|
|
2891
|
+
moderation: parsed.data.moderation
|
|
2892
|
+
});
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
const normalized = normalizeError(error);
|
|
2895
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
2896
|
+
pushGatewayRequestLog({
|
|
2897
|
+
method: request.method,
|
|
2898
|
+
endpoint: request.url,
|
|
2899
|
+
account: profileLogLabel(activeProfile),
|
|
2900
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2901
|
+
statusCode,
|
|
2902
|
+
durationMs: performance.now() - startedAt,
|
|
2903
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2904
|
+
details: {
|
|
2905
|
+
requestId: request.id,
|
|
2906
|
+
remoteAddress: request.ip,
|
|
2907
|
+
userAgent: request.headers["user-agent"],
|
|
2908
|
+
request: requestSummary,
|
|
2909
|
+
error: {
|
|
2910
|
+
message: normalized.message,
|
|
2911
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
2912
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
2913
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
2914
|
+
}
|
|
2915
|
+
},
|
|
2916
|
+
usage: {
|
|
2917
|
+
profile: activeProfile,
|
|
2918
|
+
imageRoute
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
throw error;
|
|
2922
|
+
}
|
|
1996
2923
|
console.info("[gateway:image:edit] response ready", {
|
|
1997
2924
|
method: request.method,
|
|
1998
2925
|
url: request.url,
|
|
@@ -2003,6 +2930,33 @@ function createApp(params) {
|
|
|
2003
2930
|
quality: response.quality,
|
|
2004
2931
|
size: response.size
|
|
2005
2932
|
});
|
|
2933
|
+
pushGatewayRequestLog({
|
|
2934
|
+
method: request.method,
|
|
2935
|
+
endpoint: request.url,
|
|
2936
|
+
account: profileLogLabel(activeProfile),
|
|
2937
|
+
model: parsed.data.model ?? "gpt-image-2",
|
|
2938
|
+
statusCode: 200,
|
|
2939
|
+
durationMs: performance.now() - startedAt,
|
|
2940
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
2941
|
+
details: {
|
|
2942
|
+
requestId: request.id,
|
|
2943
|
+
remoteAddress: request.ip,
|
|
2944
|
+
userAgent: request.headers["user-agent"],
|
|
2945
|
+
request: requestSummary,
|
|
2946
|
+
response: {
|
|
2947
|
+
imageCount: response.data.length,
|
|
2948
|
+
outputFormat: response.output_format,
|
|
2949
|
+
quality: response.quality,
|
|
2950
|
+
size: response.size
|
|
2951
|
+
}
|
|
2952
|
+
},
|
|
2953
|
+
usage: {
|
|
2954
|
+
profile: activeProfile,
|
|
2955
|
+
tokenUsage: imageUsageToTokenUsage(response.usage),
|
|
2956
|
+
imageCount: response.data.length,
|
|
2957
|
+
imageRoute
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2006
2960
|
return response;
|
|
2007
2961
|
});
|
|
2008
2962
|
return app;
|