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.
- package/CHANGELOG.md +16 -0
- package/README.md +61 -4
- package/dist/cli/commands/help.js +4 -0
- package/dist/cli/commands/profiles.js +63 -0
- package/dist/cli/index.js +4 -0
- package/dist/core/providers/openai-codex/oauth.js +6 -3
- package/dist/core/services/auth-service.js +74 -0
- package/dist/core/services/image-service.js +10 -2
- package/dist/core/store/codex-auth-store.js +94 -0
- package/dist/core/store/profile-transfer.js +146 -0
- package/dist/server/admin-page.js +348 -10
- package/dist/server/app.js +256 -2
- package/docs/API_USAGE.md +19 -1
- package/package.json +2 -2
package/dist/server/app.js
CHANGED
|
@@ -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.
|
|
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": {
|