ai-zero-token 1.0.5 → 1.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.
@@ -77,6 +77,17 @@ const settingsUpdateSchema = z.object({
77
77
  const profileActionSchema = z.object({
78
78
  profileId: z.string().min(1)
79
79
  });
80
+ const profileImportSchema = z.object({
81
+ profile: z.unknown()
82
+ });
83
+ const profileExportSchema = z.object({
84
+ profileId: z.string().min(1).optional(),
85
+ profileIds: z.array(z.string().min(1)).optional(),
86
+ all: z.boolean().optional()
87
+ });
88
+ const codexApplySchema = z.object({
89
+ profileId: z.string().min(1)
90
+ });
80
91
  const imageGenerationsBodySchema = z.object({
81
92
  prompt: z.string().min(1),
82
93
  model: z.string().optional(),
@@ -90,6 +101,29 @@ const imageGenerationsBodySchema = z.object({
90
101
  response_format: z.enum(["b64_json", "url"]).optional(),
91
102
  user: z.string().optional()
92
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();
93
127
  const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
94
128
  "messages",
95
129
  "n",
@@ -197,6 +231,46 @@ function summarizeImageRequestForLog(body) {
197
231
  user: body.user ?? void 0
198
232
  };
199
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
+ }
200
274
  function buildResponseApiBody(result, includeRaw) {
201
275
  const responseBody = {
202
276
  object: "response",
@@ -258,6 +332,30 @@ function validateImageRequest(data) {
258
332
  }
259
333
  return null;
260
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
+ }
261
359
  function maskSecret(value) {
262
360
  if (value.length <= 12) {
263
361
  return value;
@@ -344,14 +442,15 @@ function createApp(params) {
344
442
  };
345
443
  });
346
444
  async function buildAdminConfig(request) {
347
- const [status, models, modelCatalog, versionStatus, settings, profile, profiles] = await Promise.all([
445
+ const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
348
446
  ctx.authService.getStatus(),
349
447
  ctx.modelService.listModels(),
350
448
  ctx.modelService.getCatalog(),
351
449
  ctx.versionService.getVersionStatus(),
352
450
  ctx.configService.getSettings(),
353
451
  ctx.authService.getActiveProfile(),
354
- ctx.authService.listProfiles()
452
+ ctx.authService.listProfiles(),
453
+ ctx.authService.getCodexStatus()
355
454
  ]);
356
455
  const origin = resolveOrigin(request);
357
456
  return {
@@ -362,6 +461,7 @@ function createApp(params) {
362
461
  versionStatus,
363
462
  profile: serializeProfile(profile),
364
463
  profiles: profiles.map((item) => serializeManagedProfile(item)),
464
+ codex: codexStatus,
365
465
  adminUrl: `${origin}/`,
366
466
  baseUrl: `${origin}/v1`,
367
467
  supportedEndpoints: [
@@ -384,6 +484,11 @@ function createApp(params) {
384
484
  method: "POST",
385
485
  path: "/v1/images/generations",
386
486
  description: "OpenAI images.generations \u517C\u5BB9\u63A5\u53E3\u3002"
487
+ },
488
+ {
489
+ method: "POST",
490
+ path: "/v1/images/edits",
491
+ description: "OpenAI images.edits JSON \u517C\u5BB9\u63A5\u53E3\u3002"
387
492
  }
388
493
  ]
389
494
  };
@@ -467,6 +572,65 @@ function createApp(params) {
467
572
  await ctx.authService.removeProfile(parsed.data.profileId);
468
573
  return buildAdminConfig(request);
469
574
  });
575
+ app.post("/_gateway/admin/profiles/import", async (request, reply) => {
576
+ const parsed = profileImportSchema.safeParse(request.body);
577
+ if (!parsed.success) {
578
+ reply.code(400);
579
+ return {
580
+ error: {
581
+ type: "validation_error",
582
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
583
+ }
584
+ };
585
+ }
586
+ const importedProfiles = await ctx.authService.importProfiles(parsed.data.profile);
587
+ await ctx.authService.syncActiveProfileQuota("openai-codex", {
588
+ suppressErrors: true
589
+ });
590
+ return {
591
+ ...await buildAdminConfig(request),
592
+ importedProfileCount: importedProfiles.length
593
+ };
594
+ });
595
+ app.get("/_gateway/admin/profiles/import-template", async () => ({
596
+ profile: ctx.authService.getProfileImportTemplate()
597
+ }));
598
+ app.post("/_gateway/admin/profiles/export", async (request, reply) => {
599
+ const parsed = profileExportSchema.safeParse(request.body ?? {});
600
+ if (!parsed.success) {
601
+ reply.code(400);
602
+ return {
603
+ error: {
604
+ type: "validation_error",
605
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
606
+ }
607
+ };
608
+ }
609
+ if (parsed.data.all || parsed.data.profileIds) {
610
+ return {
611
+ profile: await ctx.authService.exportProfiles(parsed.data.profileIds)
612
+ };
613
+ }
614
+ return {
615
+ profile: await ctx.authService.exportProfile(parsed.data.profileId)
616
+ };
617
+ });
618
+ app.post("/_gateway/admin/codex/apply", async (request, reply) => {
619
+ const parsed = codexApplySchema.safeParse(request.body);
620
+ if (!parsed.success) {
621
+ reply.code(400);
622
+ return {
623
+ error: {
624
+ type: "validation_error",
625
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
626
+ }
627
+ };
628
+ }
629
+ return {
630
+ codex: await ctx.authService.applyProfileToCodex(parsed.data.profileId),
631
+ config: await buildAdminConfig(request)
632
+ };
633
+ });
470
634
  app.put("/_gateway/admin/settings", async (request, reply) => {
471
635
  const parsed = settingsUpdateSchema.safeParse(request.body);
472
636
  if (!parsed.success) {
@@ -682,6 +846,96 @@ function createApp(params) {
682
846
  });
683
847
  return response;
684
848
  });
849
+ app.post("/v1/images/edits", async (request, reply) => {
850
+ const contentType = request.headers["content-type"] ?? "";
851
+ if (!String(contentType).toLowerCase().includes("application/json")) {
852
+ reply.code(415);
853
+ return {
854
+ error: {
855
+ type: "unsupported_media_type",
856
+ 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"
857
+ }
858
+ };
859
+ }
860
+ const parsed = imageEditsBodySchema.safeParse(request.body);
861
+ if (!parsed.success) {
862
+ console.error("[gateway:image:edit] validation failure", {
863
+ method: request.method,
864
+ url: request.url,
865
+ issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
866
+ });
867
+ reply.code(400);
868
+ return {
869
+ error: {
870
+ type: "validation_error",
871
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
872
+ }
873
+ };
874
+ }
875
+ const validationError = validateImageEditRequest(parsed.data);
876
+ if (validationError) {
877
+ console.error("[gateway:image:edit] validation failure", {
878
+ method: request.method,
879
+ url: request.url,
880
+ summary: summarizeImageEditRequestForLog(parsed.data),
881
+ issue: validationError
882
+ });
883
+ reply.code(400);
884
+ return {
885
+ error: {
886
+ type: "validation_error",
887
+ message: validationError
888
+ }
889
+ };
890
+ }
891
+ if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
892
+ console.error("[gateway:image:edit] not supported", {
893
+ method: request.method,
894
+ url: request.url,
895
+ summary: summarizeImageEditRequestForLog(parsed.data),
896
+ issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
897
+ });
898
+ reply.code(501);
899
+ return {
900
+ error: {
901
+ type: "not_supported",
902
+ message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
903
+ }
904
+ };
905
+ }
906
+ const imageReferences = getImageEditReferences(parsed.data).map((reference) => normalizeJsonImageReference(reference)).map((reference) => ({
907
+ imageUrl: reference.imageUrl ?? ""
908
+ }));
909
+ const requestSummary = summarizeImageEditRequestForLog(parsed.data);
910
+ console.info("[gateway:image:edit] request accepted", {
911
+ method: request.method,
912
+ url: request.url,
913
+ summary: requestSummary
914
+ });
915
+ const response = await ctx.imageService.generate({
916
+ prompt: parsed.data.prompt,
917
+ inputImages: imageReferences,
918
+ model: parsed.data.model,
919
+ n: parsed.data.n,
920
+ size: parsed.data.size,
921
+ quality: parsed.data.quality,
922
+ background: parsed.data.background,
923
+ outputFormat: parsed.data.output_format,
924
+ outputCompression: parsed.data.output_compression,
925
+ moderation: parsed.data.moderation
926
+ });
927
+ console.info("[gateway:image:edit] response ready", {
928
+ method: request.method,
929
+ url: request.url,
930
+ summary: requestSummary,
931
+ created: response.created,
932
+ imageCount: response.data.length,
933
+ output_format: response.output_format,
934
+ quality: response.quality,
935
+ size: response.size
936
+ });
937
+ return response;
938
+ });
685
939
  return app;
686
940
  }
687
941
  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.5",
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.7",
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": {