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.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +15 -12
  3. package/README.zh-CN.md +15 -12
  4. package/admin-ui/dist/assets/StatCard-7TEzqn2i.js +1 -0
  5. package/admin-ui/dist/assets/accounts-bCDKXGg9.js +4 -0
  6. package/admin-ui/dist/assets/{docs-Dh0aFha_.js → docs--eK_2fzC.js} +1 -1
  7. package/admin-ui/dist/assets/{image-bed-C1M7-0q1.js → image-bed-7wBZ1GhS.js} +1 -1
  8. package/admin-ui/dist/assets/index-C22_3Mxq.css +1 -0
  9. package/admin-ui/dist/assets/index-CdFYy5j6.js +10 -0
  10. package/admin-ui/dist/assets/{launch-pB7YlWFI.js → launch-BiD1Khtg.js} +1 -1
  11. package/admin-ui/dist/assets/{logs-B7McijSi.js → logs-BdoKDqh2.js} +1 -1
  12. package/admin-ui/dist/assets/{network-detect-Bx3XmXPk.js → network-detect-BvKns5nQ.js} +1 -1
  13. package/admin-ui/dist/assets/overview-wm6M45fu.js +1 -0
  14. package/admin-ui/dist/assets/settings-DOOu7Kd8.js +5 -0
  15. package/admin-ui/dist/assets/{tester-BG-up8qP.js → tester-NrARmlis.js} +1 -1
  16. package/admin-ui/dist/assets/usage-CdWRVMDV.js +1 -0
  17. package/admin-ui/dist/index.html +2 -2
  18. package/dist/core/context.js +3 -0
  19. package/dist/core/providers/http-client.js +21 -2
  20. package/dist/core/providers/openai-codex/chat.js +2 -1
  21. package/dist/core/providers/openai-codex/chatgpt-web-image.js +1404 -0
  22. package/dist/core/services/auth-service.js +51 -4
  23. package/dist/core/services/config-service.js +9 -0
  24. package/dist/core/services/image-service.js +31 -1
  25. package/dist/core/services/usage-service.js +349 -0
  26. package/dist/core/store/codex-auth-store.js +149 -15
  27. package/dist/core/store/settings-store.js +8 -2
  28. package/dist/core/store/state-paths.js +17 -1
  29. package/dist/server/app.js +848 -50
  30. package/docs/API_USAGE.md +33 -3
  31. package/package.json +1 -1
  32. package/admin-ui/dist/assets/accounts-ABMyXo4H.js +0 -4
  33. package/admin-ui/dist/assets/index--rNjdmzf.js +0 -10
  34. package/admin-ui/dist/assets/index-DjtN30PC.css +0 -1
  35. package/admin-ui/dist/assets/overview-CV0H2Nsq.js +0 -1
  36. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +0 -7
@@ -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: "/v1/responses",
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
- gatewayRequestLogs.unshift({
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
- object: "list",
1414
- data: (await ctx.modelService.listModels()).map((model) => ({
1415
- id: model.id,
1416
- object: "model",
1417
- owned_by: model.provider
1418
- }))
1419
- }));
1420
- async function handleCodexResponsesPassthrough(request, reply, data, startedAt) {
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
- const result = await ctx.chatService.chat({
1686
- model: parsed.data.model,
1687
- input: input || void 0,
1688
- system: parsed.data.instructions,
1689
- experimental: {
1690
- codexBody,
1691
- allowUnknownModel: parsed.data.experimental_codex?.allow_unknown_model
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(await ctx.authService.getActiveProfile()),
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(await ctx.authService.getActiveProfile()),
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 response = await ctx.imageService.generate({
1896
- prompt: parsed.data.prompt,
1897
- model: parsed.data.model,
1898
- n: parsed.data.n,
1899
- size: parsed.data.size,
1900
- quality: parsed.data.quality,
1901
- background: parsed.data.background,
1902
- outputFormat: parsed.data.output_format,
1903
- outputCompression: parsed.data.output_compression,
1904
- moderation: parsed.data.moderation
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 response = await ctx.imageService.generate({
1985
- prompt: parsed.data.prompt,
1986
- inputImages: imageReferences,
1987
- model: parsed.data.model,
1988
- n: parsed.data.n,
1989
- size: parsed.data.size,
1990
- quality: parsed.data.quality,
1991
- background: parsed.data.background,
1992
- outputFormat: parsed.data.output_format,
1993
- outputCompression: parsed.data.output_compression,
1994
- moderation: parsed.data.moderation
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;