ai-zero-token 2.0.4 → 2.0.6

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