ai-zero-token 2.0.5 → 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 +16 -0
- package/README.md +15 -12
- package/README.zh-CN.md +15 -12
- 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-Dh0aFha_.js → docs--eK_2fzC.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-C1M7-0q1.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-pB7YlWFI.js → launch-BiD1Khtg.js} +1 -1
- package/admin-ui/dist/assets/{logs-B7McijSi.js → logs-BdoKDqh2.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-Bx3XmXPk.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-BG-up8qP.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 +21 -2
- 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 +51 -4
- package/dist/core/services/config-service.js +9 -0
- 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 +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 +848 -50
- package/docs/API_USAGE.md +33 -3
- 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,29 @@ 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
18
|
import { 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");
|
|
16
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;
|
|
17
29
|
const assetContentTypes = {
|
|
18
30
|
".css": "text/css; charset=utf-8",
|
|
19
31
|
".gif": "image/gif",
|
|
@@ -31,6 +43,35 @@ const assetContentTypes = {
|
|
|
31
43
|
function getContentType(filePath) {
|
|
32
44
|
return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
33
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
|
+
}
|
|
34
75
|
async function readAdminUiAsset(assetPath) {
|
|
35
76
|
const normalized = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
36
77
|
const filePath = path.resolve(adminUiDistDir, normalized);
|
|
@@ -114,6 +155,9 @@ const settingsUpdateSchema = z.object({
|
|
|
114
155
|
runtime: z.object({
|
|
115
156
|
quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
|
|
116
157
|
}).optional(),
|
|
158
|
+
image: z.object({
|
|
159
|
+
freeAccountWebGenerationEnabled: z.boolean().optional()
|
|
160
|
+
}).optional(),
|
|
117
161
|
server: z.object({
|
|
118
162
|
port: z.number().int().min(1).max(65535)
|
|
119
163
|
}).optional()
|
|
@@ -198,6 +242,82 @@ const imageEditsBodySchema = z.object({
|
|
|
198
242
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
199
243
|
user: z.string().optional()
|
|
200
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
|
+
}
|
|
201
321
|
function extractTextFromInputContent(content) {
|
|
202
322
|
if (typeof content === "string" && content.trim()) {
|
|
203
323
|
return [content.trim()];
|
|
@@ -232,6 +352,49 @@ function extractTextInput(input) {
|
|
|
232
352
|
}
|
|
233
353
|
return chunks.join("\n").trim();
|
|
234
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
|
+
}
|
|
235
398
|
function normalizeResponseInput(input) {
|
|
236
399
|
if (typeof input === "undefined") {
|
|
237
400
|
return void 0;
|
|
@@ -428,14 +591,14 @@ function summarizeToolNames(tools) {
|
|
|
428
591
|
}
|
|
429
592
|
const record = tool;
|
|
430
593
|
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 : "";
|
|
594
|
+
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : typeof record.type === "string" ? record.type : "";
|
|
432
595
|
}).filter(Boolean);
|
|
433
596
|
}
|
|
434
|
-
function summarizeResponsesRequest(data) {
|
|
597
|
+
function summarizeResponsesRequest(data, endpoint = "/v1/responses") {
|
|
435
598
|
const input = data.input;
|
|
436
599
|
const toolNames = summarizeToolNames(Array.isArray(data.tools) ? data.tools : void 0);
|
|
437
600
|
return {
|
|
438
|
-
endpoint
|
|
601
|
+
endpoint,
|
|
439
602
|
model: data.model ?? "default",
|
|
440
603
|
stream: data.stream ?? false,
|
|
441
604
|
inputKind: typeof input === "string" ? "string" : Array.isArray(input) ? "array" : "override",
|
|
@@ -471,6 +634,119 @@ function createCodexPassthroughBody(data, model) {
|
|
|
471
634
|
delete body.experimental_codex;
|
|
472
635
|
return body;
|
|
473
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
|
+
}
|
|
474
750
|
function summarizeChatCompletionsRequest(data) {
|
|
475
751
|
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
476
752
|
const toolNames = summarizeToolNames(data.tools);
|
|
@@ -507,6 +783,42 @@ function summarizeCodexChatBody(body) {
|
|
|
507
783
|
hasReasoning: Boolean(body.reasoning)
|
|
508
784
|
};
|
|
509
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
|
+
}
|
|
510
822
|
function profileLogLabel(profile) {
|
|
511
823
|
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
512
824
|
}
|
|
@@ -876,10 +1188,18 @@ function createApp(params) {
|
|
|
876
1188
|
logger: false,
|
|
877
1189
|
bodyLimit: params?.bodyLimit
|
|
878
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
|
+
);
|
|
879
1199
|
const ctx = createGatewayContext();
|
|
880
1200
|
const gatewayRequestLogs = [];
|
|
881
1201
|
function pushGatewayRequestLog(log) {
|
|
882
|
-
|
|
1202
|
+
const entry = {
|
|
883
1203
|
id: log.id ?? randomUUID(),
|
|
884
1204
|
time: log.time ?? Date.now(),
|
|
885
1205
|
method: log.method,
|
|
@@ -890,10 +1210,34 @@ function createApp(params) {
|
|
|
890
1210
|
durationMs: log.durationMs,
|
|
891
1211
|
source: log.source,
|
|
892
1212
|
details: log.details
|
|
893
|
-
}
|
|
1213
|
+
};
|
|
1214
|
+
gatewayRequestLogs.unshift(entry);
|
|
894
1215
|
if (gatewayRequestLogs.length > MAX_GATEWAY_REQUEST_LOGS) {
|
|
895
1216
|
gatewayRequestLogs.length = MAX_GATEWAY_REQUEST_LOGS;
|
|
896
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
|
+
});
|
|
897
1241
|
}
|
|
898
1242
|
void app.register(cors, {
|
|
899
1243
|
origin: params?.corsOrigin ?? true,
|
|
@@ -920,8 +1264,9 @@ function createApp(params) {
|
|
|
920
1264
|
app.get("/_gateway/admin/request-logs", async () => ({
|
|
921
1265
|
data: gatewayRequestLogs
|
|
922
1266
|
}));
|
|
1267
|
+
app.get("/_gateway/admin/usage", async () => ctx.usageService.getSummary());
|
|
923
1268
|
async function buildAdminConfig(request) {
|
|
924
|
-
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([
|
|
925
1270
|
ctx.authService.getStatus(),
|
|
926
1271
|
ctx.modelService.listModels(),
|
|
927
1272
|
ctx.modelService.getCatalog(),
|
|
@@ -929,7 +1274,8 @@ function createApp(params) {
|
|
|
929
1274
|
ctx.configService.getSettings(),
|
|
930
1275
|
ctx.authService.getActiveProfile(),
|
|
931
1276
|
ctx.authService.listProfiles(),
|
|
932
|
-
ctx.authService.getCodexStatus()
|
|
1277
|
+
ctx.authService.getCodexStatus(),
|
|
1278
|
+
ctx.usageService.getSummary()
|
|
933
1279
|
]);
|
|
934
1280
|
const origin = resolveOrigin(request);
|
|
935
1281
|
return {
|
|
@@ -941,6 +1287,7 @@ function createApp(params) {
|
|
|
941
1287
|
profile: serializeProfile(profile),
|
|
942
1288
|
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
943
1289
|
codex: codexStatus,
|
|
1290
|
+
usage,
|
|
944
1291
|
adminUrl: `${origin}/`,
|
|
945
1292
|
baseUrl: `${origin}/v1`,
|
|
946
1293
|
codexBaseUrl: `${origin}/codex/v1`,
|
|
@@ -962,6 +1309,11 @@ function createApp(params) {
|
|
|
962
1309
|
path: "/codex/v1/responses",
|
|
963
1310
|
description: "Codex custom provider \u4E13\u7528 Responses SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
964
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
|
+
},
|
|
965
1317
|
{
|
|
966
1318
|
method: "POST",
|
|
967
1319
|
path: "/v1/chat/completions",
|
|
@@ -1409,21 +1761,25 @@ function createApp(params) {
|
|
|
1409
1761
|
return ctx.githubImageBedService.deleteHistoryItem(parsed.data.id);
|
|
1410
1762
|
});
|
|
1411
1763
|
app.delete("/_gateway/image-bed/history", async () => ctx.githubImageBedService.clearHistory());
|
|
1412
|
-
app.get("/v1/models", async () => (
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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") {
|
|
1421
1776
|
const abortController = new AbortController();
|
|
1422
1777
|
let streamFinished = false;
|
|
1423
1778
|
let headersCommitted = false;
|
|
1424
1779
|
let profile = null;
|
|
1425
1780
|
let retryCount = 0;
|
|
1426
1781
|
let failureRecorded = false;
|
|
1782
|
+
let codexImageRoute = "none";
|
|
1427
1783
|
reply.raw.on("close", () => {
|
|
1428
1784
|
if (!streamFinished) {
|
|
1429
1785
|
abortController.abort();
|
|
@@ -1434,6 +1790,81 @@ function createApp(params) {
|
|
|
1434
1790
|
allowUnknown: data.experimental_codex?.allow_unknown_model
|
|
1435
1791
|
});
|
|
1436
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
|
+
});
|
|
1801
|
+
}
|
|
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;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1437
1868
|
let upstream = null;
|
|
1438
1869
|
const maxProfileAttempts = 5;
|
|
1439
1870
|
for (let attempt = 0; attempt < maxProfileAttempts; attempt += 1) {
|
|
@@ -1443,6 +1874,7 @@ function createApp(params) {
|
|
|
1443
1874
|
profile,
|
|
1444
1875
|
model,
|
|
1445
1876
|
bodyOverride: codexBody,
|
|
1877
|
+
endpoint: upstreamEndpoint,
|
|
1446
1878
|
passthroughBody: true,
|
|
1447
1879
|
signal: abortController.signal
|
|
1448
1880
|
});
|
|
@@ -1509,15 +1941,20 @@ function createApp(params) {
|
|
|
1509
1941
|
upstreamRequestId: upstream.requestId,
|
|
1510
1942
|
remoteAddress: request.ip,
|
|
1511
1943
|
userAgent: request.headers["user-agent"],
|
|
1512
|
-
request: summarizeResponsesRequest(data),
|
|
1944
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1513
1945
|
response: {
|
|
1514
1946
|
stream: true,
|
|
1515
1947
|
passthrough: true,
|
|
1948
|
+
upstreamEndpoint,
|
|
1516
1949
|
retryCount,
|
|
1517
1950
|
completed: streamStats.completed,
|
|
1518
1951
|
terminalEvent: streamStats.terminalEvent,
|
|
1519
1952
|
bytes: streamStats.bytes
|
|
1520
1953
|
}
|
|
1954
|
+
},
|
|
1955
|
+
usage: {
|
|
1956
|
+
profile,
|
|
1957
|
+
imageRoute: codexImageRoute
|
|
1521
1958
|
}
|
|
1522
1959
|
});
|
|
1523
1960
|
return reply;
|
|
@@ -1540,8 +1977,9 @@ function createApp(params) {
|
|
|
1540
1977
|
requestId: request.id,
|
|
1541
1978
|
remoteAddress: request.ip,
|
|
1542
1979
|
userAgent: request.headers["user-agent"],
|
|
1543
|
-
request: summarizeResponsesRequest(data),
|
|
1980
|
+
request: summarizeResponsesRequest(data, request.url),
|
|
1544
1981
|
response: {
|
|
1982
|
+
upstreamEndpoint,
|
|
1545
1983
|
retryCount
|
|
1546
1984
|
},
|
|
1547
1985
|
error: {
|
|
@@ -1550,6 +1988,10 @@ function createApp(params) {
|
|
|
1550
1988
|
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1551
1989
|
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1552
1990
|
}
|
|
1991
|
+
},
|
|
1992
|
+
usage: {
|
|
1993
|
+
profile,
|
|
1994
|
+
imageRoute: codexImageRoute
|
|
1553
1995
|
}
|
|
1554
1996
|
});
|
|
1555
1997
|
if (headersCommitted) {
|
|
@@ -1592,6 +2034,38 @@ function createApp(params) {
|
|
|
1592
2034
|
}
|
|
1593
2035
|
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt);
|
|
1594
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
|
+
});
|
|
1595
2069
|
app.post("/v1/responses", async (request, reply) => {
|
|
1596
2070
|
const startedAt = performance.now();
|
|
1597
2071
|
const parsed = responsesBodySchema.safeParse(request.body);
|
|
@@ -1682,13 +2156,72 @@ function createApp(params) {
|
|
|
1682
2156
|
};
|
|
1683
2157
|
}
|
|
1684
2158
|
const codexBody = createResponsesCodexBody(parsed.data);
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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)
|
|
1692
2225
|
}
|
|
1693
2226
|
});
|
|
1694
2227
|
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
@@ -1772,10 +2305,11 @@ function createApp(params) {
|
|
|
1772
2305
|
} catch (error) {
|
|
1773
2306
|
const normalized = normalizeError(error);
|
|
1774
2307
|
const statusCode = getErrorStatusCode(normalized);
|
|
2308
|
+
const activeProfile2 = await ctx.authService.getActiveProfile();
|
|
1775
2309
|
pushGatewayRequestLog({
|
|
1776
2310
|
method: request.method,
|
|
1777
2311
|
endpoint: request.url,
|
|
1778
|
-
account: profileLogLabel(
|
|
2312
|
+
account: profileLogLabel(activeProfile2),
|
|
1779
2313
|
model: parsed.data.model ?? "default",
|
|
1780
2314
|
statusCode,
|
|
1781
2315
|
durationMs: performance.now() - startedAt,
|
|
@@ -1792,14 +2326,18 @@ function createApp(params) {
|
|
|
1792
2326
|
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1793
2327
|
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1794
2328
|
}
|
|
2329
|
+
},
|
|
2330
|
+
usage: {
|
|
2331
|
+
profile: activeProfile2
|
|
1795
2332
|
}
|
|
1796
2333
|
});
|
|
1797
2334
|
throw error;
|
|
1798
2335
|
}
|
|
2336
|
+
const activeProfile = await ctx.authService.getActiveProfile();
|
|
1799
2337
|
pushGatewayRequestLog({
|
|
1800
2338
|
method: request.method,
|
|
1801
2339
|
endpoint: request.url,
|
|
1802
|
-
account: profileLogLabel(
|
|
2340
|
+
account: profileLogLabel(activeProfile),
|
|
1803
2341
|
model: result.model,
|
|
1804
2342
|
statusCode: 200,
|
|
1805
2343
|
durationMs: performance.now() - startedAt,
|
|
@@ -1822,6 +2360,10 @@ function createApp(params) {
|
|
|
1822
2360
|
artifactCount: result.artifacts.length,
|
|
1823
2361
|
stream: parsed.data.stream ?? false
|
|
1824
2362
|
}
|
|
2363
|
+
},
|
|
2364
|
+
usage: {
|
|
2365
|
+
profile: activeProfile,
|
|
2366
|
+
tokenUsage: extractTokenUsage(result.raw)
|
|
1825
2367
|
}
|
|
1826
2368
|
});
|
|
1827
2369
|
console.info("[gateway:chat:response]", {
|
|
@@ -1840,6 +2382,7 @@ function createApp(params) {
|
|
|
1840
2382
|
return buildChatCompletionsBody(result);
|
|
1841
2383
|
});
|
|
1842
2384
|
app.post("/v1/images/generations", async (request, reply) => {
|
|
2385
|
+
const startedAt = performance.now();
|
|
1843
2386
|
const parsed = imageGenerationsBodySchema.safeParse(request.body);
|
|
1844
2387
|
if (!parsed.success) {
|
|
1845
2388
|
console.error("[gateway:image] validation failure", {
|
|
@@ -1847,6 +2390,24 @@ function createApp(params) {
|
|
|
1847
2390
|
url: request.url,
|
|
1848
2391
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1849
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
|
+
});
|
|
1850
2411
|
reply.code(400);
|
|
1851
2412
|
return {
|
|
1852
2413
|
error: {
|
|
@@ -1863,6 +2424,25 @@ function createApp(params) {
|
|
|
1863
2424
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1864
2425
|
issue: validationError
|
|
1865
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
|
+
});
|
|
1866
2446
|
reply.code(400);
|
|
1867
2447
|
return {
|
|
1868
2448
|
error: {
|
|
@@ -1878,6 +2458,25 @@ function createApp(params) {
|
|
|
1878
2458
|
summary: summarizeImageRequestForLog(parsed.data),
|
|
1879
2459
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1880
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
|
+
});
|
|
1881
2480
|
reply.code(501);
|
|
1882
2481
|
return {
|
|
1883
2482
|
error: {
|
|
@@ -1892,17 +2491,52 @@ function createApp(params) {
|
|
|
1892
2491
|
url: request.url,
|
|
1893
2492
|
summary: requestSummary
|
|
1894
2493
|
});
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
+
}
|
|
1906
2540
|
console.info("[gateway:image] response ready", {
|
|
1907
2541
|
method: request.method,
|
|
1908
2542
|
url: request.url,
|
|
@@ -1913,11 +2547,57 @@ function createApp(params) {
|
|
|
1913
2547
|
quality: response.quality,
|
|
1914
2548
|
size: response.size
|
|
1915
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
|
+
});
|
|
1916
2577
|
return response;
|
|
1917
2578
|
});
|
|
1918
2579
|
app.post("/v1/images/edits", async (request, reply) => {
|
|
2580
|
+
const startedAt = performance.now();
|
|
1919
2581
|
const contentType = request.headers["content-type"] ?? "";
|
|
1920
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
|
+
});
|
|
1921
2601
|
reply.code(415);
|
|
1922
2602
|
return {
|
|
1923
2603
|
error: {
|
|
@@ -1933,6 +2613,24 @@ function createApp(params) {
|
|
|
1933
2613
|
url: request.url,
|
|
1934
2614
|
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1935
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
|
+
});
|
|
1936
2634
|
reply.code(400);
|
|
1937
2635
|
return {
|
|
1938
2636
|
error: {
|
|
@@ -1949,6 +2647,25 @@ function createApp(params) {
|
|
|
1949
2647
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1950
2648
|
issue: validationError
|
|
1951
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
|
+
});
|
|
1952
2669
|
reply.code(400);
|
|
1953
2670
|
return {
|
|
1954
2671
|
error: {
|
|
@@ -1964,6 +2681,25 @@ function createApp(params) {
|
|
|
1964
2681
|
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
1965
2682
|
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
1966
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
|
+
});
|
|
1967
2703
|
reply.code(501);
|
|
1968
2704
|
return {
|
|
1969
2705
|
error: {
|
|
@@ -1981,18 +2717,53 @@ function createApp(params) {
|
|
|
1981
2717
|
url: request.url,
|
|
1982
2718
|
summary: requestSummary
|
|
1983
2719
|
});
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
+
}
|
|
1996
2767
|
console.info("[gateway:image:edit] response ready", {
|
|
1997
2768
|
method: request.method,
|
|
1998
2769
|
url: request.url,
|
|
@@ -2003,6 +2774,33 @@ function createApp(params) {
|
|
|
2003
2774
|
quality: response.quality,
|
|
2004
2775
|
size: response.size
|
|
2005
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
|
+
});
|
|
2006
2804
|
return response;
|
|
2007
2805
|
});
|
|
2008
2806
|
return app;
|