ai-zero-token 2.0.5 → 2.0.7

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