ai-zero-token 1.0.6 → 1.0.8

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.
@@ -1498,6 +1498,7 @@ function renderAdminPage() {
1498
1498
  <div class="tester-tabs" id="testerTabs">
1499
1499
  <button class="tab-btn is-active" type="button" data-endpoint="/v1/chat/completions">Chat</button>
1500
1500
  <button class="tab-btn" type="button" data-endpoint="/v1/images/generations">Images</button>
1501
+ <button class="tab-btn" type="button" data-endpoint="/v1/images/edits">Edits</button>
1501
1502
  <button class="tab-btn" type="button" data-endpoint="/v1/responses">Responses</button>
1502
1503
  <button class="tab-btn" type="button" data-endpoint="/v1/models">Models</button>
1503
1504
  </div>
@@ -1545,6 +1546,7 @@ function renderAdminPage() {
1545
1546
  <button class="btn-secondary" type="button" data-example="/v1/responses">\u793A\u4F8B Responses</button>
1546
1547
  <button class="btn-secondary" type="button" data-example="/v1/chat/completions">\u793A\u4F8B Chat</button>
1547
1548
  <button class="btn-secondary" type="button" data-example="/v1/images/generations">\u793A\u4F8B Images</button>
1549
+ <button class="btn-secondary" type="button" data-example="/v1/images/edits">\u793A\u4F8B Edits</button>
1548
1550
  <button class="btn-primary" id="runTestBtn" type="button">\u53D1\u9001\u8BF7\u6C42</button>
1549
1551
  </div>
1550
1552
 
@@ -1720,6 +1722,11 @@ function renderAdminPage() {
1720
1722
  tab: "Images",
1721
1723
  description: "\u517C\u5BB9 OpenAI images.generations \u63A5\u53E3\u3002",
1722
1724
  },
1725
+ "/v1/images/edits": {
1726
+ method: "POST",
1727
+ tab: "Edits",
1728
+ description: "\u517C\u5BB9 OpenAI images.edits JSON \u63A5\u53E3\u3002",
1729
+ },
1723
1730
  };
1724
1731
 
1725
1732
  const endpointSelect = document.getElementById("endpointSelect");
@@ -1926,12 +1933,12 @@ function renderAdminPage() {
1926
1933
  title.textContent = "\u53D1\u73B0\u65B0\u7248\u672C\u53EF\u66F4\u65B0";
1927
1934
  detail.textContent = "\u5F53\u524D\u7248\u672C " + versionStatus.currentVersion + "\uFF0C\u6700\u65B0\u7248\u672C "
1928
1935
  + versionStatus.latestVersion + "\u3002\u66F4\u65B0\u540E\u53EF\u83B7\u5F97\u6700\u65B0\u6A21\u578B\u5217\u8868\u903B\u8F91\u3001\u7BA1\u7406\u9875\u4F53\u9A8C\u548C\u63A5\u53E3\u4FEE\u590D\u3002";
1929
- command.textContent = "npm install -g " + versionStatus.packageName + "@latest";
1936
+ command.textContent = "npm install -g " + versionStatus.packageName;
1930
1937
  panel.classList.add("is-visible");
1931
1938
  }
1932
1939
 
1933
1940
  function supportsImageGeneration(profile) {
1934
- return getPlanType(profile) !== "free";
1941
+ return Boolean(profile);
1935
1942
  }
1936
1943
 
1937
1944
  function getImageCapability(profile) {
@@ -1947,10 +1954,10 @@ function renderAdminPage() {
1947
1954
  const planType = getPlanType(profile);
1948
1955
  if (planType === "free") {
1949
1956
  return {
1950
- supported: false,
1951
- label: "\u751F\u56FE\u53D7\u9650",
1952
- detail: "free \u5957\u9910\u8D26\u53F7\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\uFF0C\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002",
1953
- badgeClass: "red",
1957
+ supported: true,
1958
+ label: "\u53EF\u5C1D\u8BD5\u751F\u56FE",
1959
+ detail: "free \u8D26\u53F7\u53EF\u5C1D\u8BD5\u56FE\u7247\u751F\u6210\uFF0C\u989D\u5EA6\u548C\u53EF\u7528\u6027\u4EE5\u4E0A\u6E38\u8FD4\u56DE\u4E3A\u51C6\u3002",
1960
+ badgeClass: "orange",
1954
1961
  };
1955
1962
  }
1956
1963
 
@@ -2325,6 +2332,21 @@ function renderAdminPage() {
2325
2332
  });
2326
2333
  }
2327
2334
 
2335
+ if (endpoint === "/v1/images/edits") {
2336
+ return formatJson({
2337
+ model: "gpt-image-2",
2338
+ prompt: "\u53C2\u8003\u8FD9\u5F20\u56FE\u7247\uFF0C\u751F\u6210\u4E00\u5F20\u66F4\u9002\u5408\u79D1\u6280\u4EA7\u54C1\u5E7F\u544A\u7684\u7248\u672C\uFF0C\u4FDD\u7559\u4E3B\u4F53\u6784\u56FE\uFF0C\u589E\u5F3A\u5149\u7EBF\u548C\u8D28\u611F\u3002",
2339
+ images: [
2340
+ {
2341
+ image_url: "data:image/png;base64,\u66FF\u6362\u4E3A\u4F60\u7684\u56FE\u7247base64",
2342
+ },
2343
+ ],
2344
+ size: "1024x1024",
2345
+ quality: "low",
2346
+ response_format: "b64_json",
2347
+ });
2348
+ }
2349
+
2328
2350
  return formatJson({
2329
2351
  model: model,
2330
2352
  input: "\u8BF7\u53EA\u56DE\u590D OK",
@@ -2336,6 +2358,10 @@ function renderAdminPage() {
2336
2358
  const avg = requests.length
2337
2359
  ? requests.reduce(function (sum, item) { return sum + (item.durationMs || 0); }, 0) / requests.length
2338
2360
  : 0;
2361
+ const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
2362
+ const codexProfile = codexAccountId && Array.isArray(config.profiles)
2363
+ ? config.profiles.find(function (profile) { return profile.accountId === codexAccountId; })
2364
+ : null;
2339
2365
 
2340
2366
  return [
2341
2367
  {
@@ -2357,6 +2383,12 @@ function renderAdminPage() {
2357
2383
  detail: "\u672A\u663E\u5F0F\u6307\u5B9A model \u65F6\u751F\u6548",
2358
2384
  compact: true,
2359
2385
  },
2386
+ {
2387
+ label: "Codex \u5F53\u524D\u8D26\u53F7",
2388
+ value: codexProfile ? getProfileDisplayLabel(codexProfile) : (codexAccountId ? maskIdentifier(codexAccountId) : "\u672A\u68C0\u6D4B\u5230"),
2389
+ detail: config.codex && config.codex.exists ? "\u6765\u81EA ~/.codex/auth.json" : "\u5C1A\u672A\u5E94\u7528\u5230 Codex",
2390
+ compact: true,
2391
+ },
2360
2392
  {
2361
2393
  label: "\u5F53\u524D\u7248\u672C",
2362
2394
  value: getVersionValue(config),
@@ -2550,6 +2582,7 @@ function renderAdminPage() {
2550
2582
  + "</div>"
2551
2583
  + '<div class="account-actions">'
2552
2584
  + actionButton
2585
+ + '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u5230 Codex</button>'
2553
2586
  + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
2554
2587
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
2555
2588
  + "</div>"
@@ -2561,13 +2594,15 @@ function renderAdminPage() {
2561
2594
  const profiles = Array.isArray(config.profiles) ? config.profiles : [];
2562
2595
  const now = Date.now();
2563
2596
  const seeds = profiles.slice(0, 5).map(function (profile, index) {
2564
- const endpoint = index % 4 === 0
2597
+ const endpoint = index % 5 === 0
2565
2598
  ? "/v1/chat/completions"
2566
- : index % 4 === 1
2599
+ : index % 5 === 1
2567
2600
  ? "/v1/responses"
2568
- : index % 4 === 2
2601
+ : index % 5 === 2
2569
2602
  ? "/v1/models"
2570
- : "/v1/images/generations";
2603
+ : index % 5 === 3
2604
+ ? "/v1/images/generations"
2605
+ : "/v1/images/edits";
2571
2606
  const method = endpointMeta[endpoint].method;
2572
2607
  return {
2573
2608
  time: now - index * 15 * 60 * 1000,
@@ -2575,7 +2610,7 @@ function renderAdminPage() {
2575
2610
  endpoint: endpoint,
2576
2611
  accountEmail: profile.email || "",
2577
2612
  accountFallback: profile.accountId || profile.profileId || "\u672A\u547D\u540D\u8D26\u53F7",
2578
- model: endpoint === "/v1/images/generations" ? "gpt-image-2" : config.settings.defaultModel,
2613
+ model: endpoint.indexOf("/v1/images/") === 0 ? "gpt-image-2" : config.settings.defaultModel,
2579
2614
  statusCode: 200,
2580
2615
  durationMs: 860 + index * 230 + getPrimaryUsage(profile) * 8,
2581
2616
  source: index % 2 === 0 ? "\u7BA1\u7406\u9875" : "CLI",
@@ -2806,13 +2841,13 @@ function renderAdminPage() {
2806
2841
 
2807
2842
  function syncImageCapabilityHint(config) {
2808
2843
  const capability = getImageCapability(config ? config.profile : null);
2809
- const isImageEndpoint = endpointSelect.value === "/v1/images/generations";
2844
+ const isImageEndpoint = endpointSelect.value === "/v1/images/generations" || endpointSelect.value === "/v1/images/edits";
2810
2845
  imageCapabilityHint.textContent = capability.detail;
2811
2846
  imageCapabilityHint.className = capability.supported && !isImageEndpoint ? "hint" : "hint warn";
2812
- runTestBtn.disabled = isImageEndpoint && !capability.supported;
2847
+ runTestBtn.disabled = isImageEndpoint && !config.profile;
2813
2848
  if (isImageEndpoint && !capability.supported) {
2814
2849
  testerMeta.textContent = capability.label;
2815
- } else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650") {
2850
+ } else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650" || testerMeta.textContent === "\u53EF\u5C1D\u8BD5\u751F\u56FE") {
2816
2851
  testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
2817
2852
  }
2818
2853
  }
@@ -2843,7 +2878,7 @@ function renderAdminPage() {
2843
2878
  authStatus.textContent = config.status.loggedIn
2844
2879
  ? (supportsImageGeneration(config.profile)
2845
2880
  ? "\u7F51\u5173\u5DF2\u53EF\u76F4\u63A5\u8F6C\u53D1\u8BF7\u6C42\uFF0C\u53EF\u4EE5\u5728\u4E0B\u65B9\u5207\u6362\u9ED8\u8BA4\u6A21\u578B\u5E76\u53D1\u9001\u6D4B\u8BD5\u8BF7\u6C42\u3002"
2846
- : "\u5F53\u524D\u8D26\u53F7\u53EF\u7528\u4E8E\u6587\u672C\u63A5\u53E3\uFF0C\u4F46\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\u3002\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002")
2881
+ : "\u5F53\u524D\u8D26\u53F7\u672A\u5C31\u7EEA\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002")
2847
2882
  : "\u8BF7\u5148\u70B9\u51FB\u201C\u65B0\u589E\u8D26\u53F7\u201D\uFF0C\u5B8C\u6210 OAuth \u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002";
2848
2883
  if (!requestBody.value) {
2849
2884
  requestBody.value = buildExample(endpointSelect.value || "/v1/chat/completions");
@@ -2987,6 +3022,10 @@ function renderAdminPage() {
2987
3022
  await exportProfile(profileId, button);
2988
3023
  return;
2989
3024
  }
3025
+ if (action === "apply-codex") {
3026
+ await applyProfileToCodex(profileId, button);
3027
+ return;
3028
+ }
2990
3029
 
2991
3030
  setBusy(button, true);
2992
3031
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
@@ -3020,6 +3059,31 @@ function renderAdminPage() {
3020
3059
  }
3021
3060
  }
3022
3061
 
3062
+ async function applyProfileToCodex(profileId, button) {
3063
+ setBusy(button, true);
3064
+ authStatus.textContent = "\u6B63\u5728\u5E94\u7528\u8D26\u53F7\u5230 Codex...";
3065
+ try {
3066
+ const result = await fetchJson("/_gateway/admin/codex/apply", {
3067
+ method: "POST",
3068
+ headers: {
3069
+ "Content-Type": "application/json",
3070
+ },
3071
+ body: formatJson({
3072
+ profileId: profileId,
3073
+ }),
3074
+ });
3075
+ const config = result.config || await fetchJson("/_gateway/admin/config");
3076
+ renderConfig(config);
3077
+ const codex = result.codex || config.codex || {};
3078
+ authStatus.textContent = "\u5DF2\u5E94\u7528\u5230 Codex\u3002\u65B0\u5F00\u7684 Codex \u4F1A\u8BDD\u5C06\u4F7F\u7528\u8BE5\u8D26\u53F7\u3002"
3079
+ + (codex.backupPath ? " \u5DF2\u5907\u4EFD\u539F auth.json\u3002" : "");
3080
+ } catch (error) {
3081
+ authStatus.textContent = error.message;
3082
+ } finally {
3083
+ setBusy(button, false);
3084
+ }
3085
+ }
3086
+
3023
3087
  function downloadJsonFile(fileName, value) {
3024
3088
  const blob = new Blob([formatJson(value) + "\\n"], { type: "application/json" });
3025
3089
  const url = URL.createObjectURL(blob);
@@ -85,6 +85,9 @@ const profileExportSchema = z.object({
85
85
  profileIds: z.array(z.string().min(1)).optional(),
86
86
  all: z.boolean().optional()
87
87
  });
88
+ const codexApplySchema = z.object({
89
+ profileId: z.string().min(1)
90
+ });
88
91
  const imageGenerationsBodySchema = z.object({
89
92
  prompt: z.string().min(1),
90
93
  model: z.string().optional(),
@@ -98,6 +101,29 @@ const imageGenerationsBodySchema = z.object({
98
101
  response_format: z.enum(["b64_json", "url"]).optional(),
99
102
  user: z.string().optional()
100
103
  }).passthrough();
104
+ const imageReferenceSchema = z.union([
105
+ z.string().min(1),
106
+ z.object({
107
+ image_url: z.string().min(1).optional(),
108
+ file_id: z.string().min(1).optional()
109
+ }).passthrough()
110
+ ]);
111
+ const imageEditsBodySchema = z.object({
112
+ prompt: z.string().min(1),
113
+ images: z.array(imageReferenceSchema).min(1).max(16).optional(),
114
+ image: z.union([imageReferenceSchema, z.array(imageReferenceSchema).min(1).max(16)]).optional(),
115
+ mask: imageReferenceSchema.optional(),
116
+ model: z.string().optional(),
117
+ n: z.number().int().positive().optional(),
118
+ quality: z.enum(["low", "medium", "high", "auto"]).optional(),
119
+ size: z.string().min(1).optional(),
120
+ background: z.enum(["transparent", "opaque", "auto"]).optional(),
121
+ output_format: z.enum(["png", "webp", "jpeg"]).optional(),
122
+ output_compression: z.number().int().min(0).max(100).optional(),
123
+ moderation: z.enum(["auto", "low"]).optional(),
124
+ response_format: z.enum(["b64_json", "url"]).optional(),
125
+ user: z.string().optional()
126
+ }).passthrough();
101
127
  const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
102
128
  "messages",
103
129
  "n",
@@ -205,6 +231,46 @@ function summarizeImageRequestForLog(body) {
205
231
  user: body.user ?? void 0
206
232
  };
207
233
  }
234
+ function getImageEditReferences(data) {
235
+ if (Array.isArray(data.images)) {
236
+ return data.images;
237
+ }
238
+ if (Array.isArray(data.image)) {
239
+ return data.image;
240
+ }
241
+ if (data.image) {
242
+ return [data.image];
243
+ }
244
+ return [];
245
+ }
246
+ function normalizeJsonImageReference(reference) {
247
+ if (typeof reference === "string") {
248
+ return {
249
+ imageUrl: normalizeJsonImageUrl(reference)
250
+ };
251
+ }
252
+ return {
253
+ imageUrl: reference.image_url ? normalizeJsonImageUrl(reference.image_url) : void 0,
254
+ fileId: reference.file_id
255
+ };
256
+ }
257
+ function normalizeJsonImageUrl(value) {
258
+ const trimmed = value.trim();
259
+ if (/^https?:\/\//i.test(trimmed) || /^data:image\//i.test(trimmed)) {
260
+ return trimmed;
261
+ }
262
+ if (/^[A-Za-z0-9+/=_-]+$/.test(trimmed) && trimmed.length > 80) {
263
+ return `data:image/png;base64,${trimmed}`;
264
+ }
265
+ return trimmed;
266
+ }
267
+ function summarizeImageEditRequestForLog(body) {
268
+ return {
269
+ ...summarizeImageRequestForLog(body),
270
+ imageCount: getImageEditReferences(body).length,
271
+ hasMask: Boolean(body.mask)
272
+ };
273
+ }
208
274
  function buildResponseApiBody(result, includeRaw) {
209
275
  const responseBody = {
210
276
  object: "response",
@@ -266,6 +332,30 @@ function validateImageRequest(data) {
266
332
  }
267
333
  return null;
268
334
  }
335
+ function validateImageEditRequest(data) {
336
+ const generationValidationError = validateImageRequest(data);
337
+ if (generationValidationError) {
338
+ return generationValidationError;
339
+ }
340
+ if (data.mask) {
341
+ return "\u5F53\u524D\u7F51\u5173\u7684 JSON \u7248 images.edits \u6682\u4E0D\u652F\u6301 mask\uFF1B\u8BF7\u5148\u4F7F\u7528\u53C2\u8003\u56FE\u7F16\u8F91\u3002";
342
+ }
343
+ const references = getImageEditReferences(data);
344
+ if (references.length === 0) {
345
+ return "images.edits \u8BF7\u6C42\u7F3A\u5C11 images \u6216 image\u3002";
346
+ }
347
+ const normalized = references.map((reference) => normalizeJsonImageReference(reference));
348
+ if (normalized.some((reference) => reference.fileId)) {
349
+ return "\u5F53\u524D\u7F51\u5173\u7684 JSON \u7248 images.edits \u6682\u4E0D\u652F\u6301 file_id\uFF0C\u8BF7\u4F7F\u7528 image_url URL \u6216 base64 data URL\u3002";
350
+ }
351
+ if (normalized.some((reference) => !reference.imageUrl)) {
352
+ return "images.edits \u7684\u6BCF\u4E2A\u56FE\u7247\u5F15\u7528\u90FD\u9700\u8981\u63D0\u4F9B image_url\u3002";
353
+ }
354
+ if (normalized.some((reference) => reference.imageUrl && !/^https?:\/\//i.test(reference.imageUrl) && !/^data:image\//i.test(reference.imageUrl))) {
355
+ return "images.edits \u7684 image_url \u9700\u8981\u662F http(s) URL\u3001data:image/...;base64,...\uFF0C\u6216\u88F8 base64 \u5B57\u7B26\u4E32\u3002";
356
+ }
357
+ return null;
358
+ }
269
359
  function maskSecret(value) {
270
360
  if (value.length <= 12) {
271
361
  return value;
@@ -326,7 +416,8 @@ function getErrorStatusCode(error) {
326
416
  }
327
417
  function createApp(params) {
328
418
  const app = Fastify({
329
- logger: false
419
+ logger: false,
420
+ bodyLimit: params?.bodyLimit
330
421
  });
331
422
  const ctx = createGatewayContext();
332
423
  void app.register(cors, {
@@ -352,14 +443,15 @@ function createApp(params) {
352
443
  };
353
444
  });
354
445
  async function buildAdminConfig(request) {
355
- const [status, models, modelCatalog, versionStatus, settings, profile, profiles] = await Promise.all([
446
+ const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
356
447
  ctx.authService.getStatus(),
357
448
  ctx.modelService.listModels(),
358
449
  ctx.modelService.getCatalog(),
359
450
  ctx.versionService.getVersionStatus(),
360
451
  ctx.configService.getSettings(),
361
452
  ctx.authService.getActiveProfile(),
362
- ctx.authService.listProfiles()
453
+ ctx.authService.listProfiles(),
454
+ ctx.authService.getCodexStatus()
363
455
  ]);
364
456
  const origin = resolveOrigin(request);
365
457
  return {
@@ -370,6 +462,7 @@ function createApp(params) {
370
462
  versionStatus,
371
463
  profile: serializeProfile(profile),
372
464
  profiles: profiles.map((item) => serializeManagedProfile(item)),
465
+ codex: codexStatus,
373
466
  adminUrl: `${origin}/`,
374
467
  baseUrl: `${origin}/v1`,
375
468
  supportedEndpoints: [
@@ -392,6 +485,11 @@ function createApp(params) {
392
485
  method: "POST",
393
486
  path: "/v1/images/generations",
394
487
  description: "OpenAI images.generations \u517C\u5BB9\u63A5\u53E3\u3002"
488
+ },
489
+ {
490
+ method: "POST",
491
+ path: "/v1/images/edits",
492
+ description: "OpenAI images.edits JSON \u517C\u5BB9\u63A5\u53E3\u3002"
395
493
  }
396
494
  ]
397
495
  };
@@ -518,6 +616,22 @@ function createApp(params) {
518
616
  profile: await ctx.authService.exportProfile(parsed.data.profileId)
519
617
  };
520
618
  });
619
+ app.post("/_gateway/admin/codex/apply", async (request, reply) => {
620
+ const parsed = codexApplySchema.safeParse(request.body);
621
+ if (!parsed.success) {
622
+ reply.code(400);
623
+ return {
624
+ error: {
625
+ type: "validation_error",
626
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
627
+ }
628
+ };
629
+ }
630
+ return {
631
+ codex: await ctx.authService.applyProfileToCodex(parsed.data.profileId),
632
+ config: await buildAdminConfig(request)
633
+ };
634
+ });
521
635
  app.put("/_gateway/admin/settings", async (request, reply) => {
522
636
  const parsed = settingsUpdateSchema.safeParse(request.body);
523
637
  if (!parsed.success) {
@@ -733,6 +847,96 @@ function createApp(params) {
733
847
  });
734
848
  return response;
735
849
  });
850
+ app.post("/v1/images/edits", async (request, reply) => {
851
+ const contentType = request.headers["content-type"] ?? "";
852
+ if (!String(contentType).toLowerCase().includes("application/json")) {
853
+ reply.code(415);
854
+ return {
855
+ error: {
856
+ type: "unsupported_media_type",
857
+ 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"
858
+ }
859
+ };
860
+ }
861
+ const parsed = imageEditsBodySchema.safeParse(request.body);
862
+ if (!parsed.success) {
863
+ console.error("[gateway:image:edit] validation failure", {
864
+ method: request.method,
865
+ url: request.url,
866
+ issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
867
+ });
868
+ reply.code(400);
869
+ return {
870
+ error: {
871
+ type: "validation_error",
872
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
873
+ }
874
+ };
875
+ }
876
+ const validationError = validateImageEditRequest(parsed.data);
877
+ if (validationError) {
878
+ console.error("[gateway:image:edit] validation failure", {
879
+ method: request.method,
880
+ url: request.url,
881
+ summary: summarizeImageEditRequestForLog(parsed.data),
882
+ issue: validationError
883
+ });
884
+ reply.code(400);
885
+ return {
886
+ error: {
887
+ type: "validation_error",
888
+ message: validationError
889
+ }
890
+ };
891
+ }
892
+ if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
893
+ console.error("[gateway:image:edit] not supported", {
894
+ method: request.method,
895
+ url: request.url,
896
+ summary: summarizeImageEditRequestForLog(parsed.data),
897
+ issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
898
+ });
899
+ reply.code(501);
900
+ return {
901
+ error: {
902
+ type: "not_supported",
903
+ message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
904
+ }
905
+ };
906
+ }
907
+ const imageReferences = getImageEditReferences(parsed.data).map((reference) => normalizeJsonImageReference(reference)).map((reference) => ({
908
+ imageUrl: reference.imageUrl ?? ""
909
+ }));
910
+ const requestSummary = summarizeImageEditRequestForLog(parsed.data);
911
+ console.info("[gateway:image:edit] request accepted", {
912
+ method: request.method,
913
+ url: request.url,
914
+ summary: requestSummary
915
+ });
916
+ const response = await ctx.imageService.generate({
917
+ prompt: parsed.data.prompt,
918
+ inputImages: imageReferences,
919
+ model: parsed.data.model,
920
+ n: parsed.data.n,
921
+ size: parsed.data.size,
922
+ quality: parsed.data.quality,
923
+ background: parsed.data.background,
924
+ outputFormat: parsed.data.output_format,
925
+ outputCompression: parsed.data.output_compression,
926
+ moderation: parsed.data.moderation
927
+ });
928
+ console.info("[gateway:image:edit] response ready", {
929
+ method: request.method,
930
+ url: request.url,
931
+ summary: requestSummary,
932
+ created: response.created,
933
+ imageCount: response.data.length,
934
+ output_format: response.output_format,
935
+ quality: response.quality,
936
+ size: response.size
937
+ });
938
+ return response;
939
+ });
736
940
  return app;
737
941
  }
738
942
  export {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { ConfigService } from "../core/services/config-service.js";
3
3
  import { createApp } from "./app.js";
4
+ const DEFAULT_BODY_LIMIT_MB = 32;
4
5
  function resolveCorsOrigin() {
5
6
  const raw = process.env.AZT_CORS_ORIGIN?.trim();
6
7
  if (!raw || raw === "*") {
@@ -9,9 +10,22 @@ function resolveCorsOrigin() {
9
10
  const values = raw.split(",").map((item) => item.trim()).filter(Boolean);
10
11
  return values.length <= 1 ? values[0] ?? true : values;
11
12
  }
13
+ function resolveBodyLimitBytes() {
14
+ const raw = process.env.AZT_BODY_LIMIT_MB?.trim();
15
+ if (!raw) {
16
+ return DEFAULT_BODY_LIMIT_MB * 1024 * 1024;
17
+ }
18
+ const value = Number(raw);
19
+ if (!Number.isFinite(value) || value <= 0) {
20
+ return DEFAULT_BODY_LIMIT_MB * 1024 * 1024;
21
+ }
22
+ return Math.floor(value * 1024 * 1024);
23
+ }
12
24
  async function startServer(params) {
25
+ const bodyLimit = resolveBodyLimitBytes();
13
26
  const app = createApp({
14
- corsOrigin: resolveCorsOrigin()
27
+ corsOrigin: resolveCorsOrigin(),
28
+ bodyLimit
15
29
  });
16
30
  const configService = new ConfigService();
17
31
  const defaults = await configService.getServerConfig();
@@ -25,7 +39,8 @@ async function startServer(params) {
25
39
  app,
26
40
  host,
27
41
  port,
28
- corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*"
42
+ corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*",
43
+ bodyLimit
29
44
  };
30
45
  }
31
46
  export {
package/docs/API_USAGE.md CHANGED
@@ -91,6 +91,25 @@ curl http://127.0.0.1:8787/v1/images/generations \
91
91
  }'
92
92
  ```
93
93
 
94
+ JSON image edit with a reference image URL or base64 data URL:
95
+
96
+ ```bash
97
+ curl http://127.0.0.1:8787/v1/images/edits \
98
+ -H "Content-Type: application/json" \
99
+ -d '{
100
+ "model": "gpt-image-2",
101
+ "prompt": "Use this reference image and make it look like a clean product advertisement.",
102
+ "images": [
103
+ {
104
+ "image_url": "data:image/png;base64,REPLACE_WITH_IMAGE_BASE64"
105
+ }
106
+ ],
107
+ "size": "1024x1024",
108
+ "quality": "low",
109
+ "response_format": "b64_json"
110
+ }'
111
+ ```
112
+
94
113
  ## JavaScript SDK Example
95
114
 
96
115
  ```ts
@@ -117,4 +136,3 @@ console.log(response.choices[0]?.message?.content);
117
136
  - A model appearing in `/v1/models` means the local Codex cache lists it. Final availability still depends on the active account.
118
137
  - `stream=true` is not supported yet.
119
138
  - The default listener is `0.0.0.0:8787`, so local-network clients can call the gateway by using the machine IP.
120
-
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-zero-token",
3
- "version": "1.0.6",
4
- "description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation.",
3
+ "version": "1.0.8",
4
+ "description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation/editing.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -60,6 +60,7 @@
60
60
  "CHANGELOG.md",
61
61
  "docs/API_USAGE.md",
62
62
  "README.md",
63
+ "README.zh-CN.md",
63
64
  "package.json"
64
65
  ]
65
66
  }