ai-zero-token 1.0.6 → 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 +8 -0
- package/README.md +47 -3
- package/dist/core/providers/openai-codex/oauth.js +6 -3
- package/dist/core/services/auth-service.js +27 -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 +4 -0
- package/dist/server/admin-page.js +70 -6
- package/dist/server/app.js +205 -2
- package/docs/API_USAGE.md +19 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.7 - 2026-04-29
|
|
4
|
+
|
|
5
|
+
- Added JSON `POST /v1/images/edits` support for image-to-image workflows with URL, base64 data URL, or raw base64 image references.
|
|
6
|
+
- Added management-page Edits test examples and documented the JSON image editing request format.
|
|
7
|
+
- Added `id_token` persistence for login, refresh, import, export, and account transfer JSON.
|
|
8
|
+
- Added "Apply to Codex" account action to back up and update local `~/.codex/auth.json` for new Codex sessions.
|
|
9
|
+
- Updated the local AI-Zero-Token skill documentation package with image editing and Codex account switching guidance.
|
|
10
|
+
|
|
3
11
|
## 1.0.6 - 2026-04-28
|
|
4
12
|
|
|
5
13
|
- Added account JSON import/export support in the management page.
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ AI Zero Token 是一个本地优先的单用户 AI CLI 和本地网关。
|
|
|
7
7
|
它把账号授权能力整理成 OpenAI 风格接口,重点是把图片生成能力也代理出来:
|
|
8
8
|
|
|
9
9
|
- `POST /v1/images/generations`
|
|
10
|
+
- `POST /v1/images/edits`
|
|
10
11
|
- `POST /v1/responses`
|
|
11
12
|
- `POST /v1/chat/completions`
|
|
12
13
|
- `GET /v1/models`
|
|
@@ -15,9 +16,10 @@ AI Zero Token 是一个本地优先的单用户 AI CLI 和本地网关。
|
|
|
15
16
|
|
|
16
17
|
## 这次迭代亮点
|
|
17
18
|
|
|
18
|
-
- 直接代理 `gpt-image-2`,把图片生成能力暴露成 OpenAI 风格 `images.generations` 接口
|
|
19
|
+
- 直接代理 `gpt-image-2`,把图片生成能力暴露成 OpenAI 风格 `images.generations` / `images.edits` 接口
|
|
19
20
|
- 启动 `azt start` 后即可获得本地管理页和本地网关,适合脚本、前端和自动化流程接入
|
|
20
21
|
- 支持多账号保存、切换当前账号、查看账号套餐 plan,以及当前账号是否支持生图
|
|
22
|
+
- 支持账号 JSON 批量导入/勾选导出,并可一键把已保存账号应用到本机 Codex
|
|
21
23
|
- 模型列表会优先同步本机 `~/.codex/models_cache.json`,不需要每次为新模型重新 build
|
|
22
24
|
- 管理页会每 10 分钟自动同步额度快照和版本状态,并提示当前版本是否可更新
|
|
23
25
|
- `free` 账号会在管理页直接预警,并在网关层明确拦截生图请求
|
|
@@ -222,6 +224,7 @@ azt start
|
|
|
222
224
|
- 在多个已保存账号之间切换当前使用账号
|
|
223
225
|
- 在“新增账号”里选择 OAuth 登录,或粘贴外部账号 JSON 批量导入
|
|
224
226
|
- 导出单个账号,或勾选多个账号后批量导出所选账号 JSON
|
|
227
|
+
- 将任一已保存账号“应用到 Codex”,自动备份并更新本机 `~/.codex/auth.json`
|
|
225
228
|
- 删除单个本地账号,或一键清空全部本地账号
|
|
226
229
|
- 切换默认模型
|
|
227
230
|
- 测试 `models` / `responses` / `chat.completions`
|
|
@@ -231,6 +234,8 @@ azt start
|
|
|
231
234
|
|
|
232
235
|
导出的账号 JSON 包含完整 `access_token` 和 `refresh_token`,等同于账号登录凭据,只适合在可信环境中传递。
|
|
233
236
|
|
|
237
|
+
账号卡片里的“应用到 Codex”会把该账号的 `access_token`、`refresh_token`、`id_token` 和 `account_id` 写入本机 Codex 的 `~/.codex/auth.json`。写入前会自动备份原文件;新开的 Codex 会话会使用该账号。
|
|
238
|
+
|
|
234
239
|
如果当前网络访问海外上游不稳定,可以在管理页的“接口测试 / 系统设置”区域启用“上游代理”,并填写你自己的代理地址。保存后,OAuth 换取 token、模型刷新和接口转发都会通过该代理访问上游;本地管理页和 `127.0.0.1` 默认保持直连。
|
|
235
240
|
|
|
236
241
|
默认监听地址:
|
|
@@ -322,9 +327,28 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
322
327
|
响应会返回 OpenAI 同类型结构的 `data[].b64_json`。如果你在管理页里测试,这张图片会直接显示预览。
|
|
323
328
|
如果请求里不显式传 `model`,当前默认会使用 `gpt-image-2`。
|
|
324
329
|
|
|
330
|
+
JSON 版 `images.edits` 支持通过 URL 或 base64 data URL 传参考图:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
curl http://127.0.0.1:8787/v1/images/edits \
|
|
334
|
+
-H "content-type: application/json" \
|
|
335
|
+
-d '{
|
|
336
|
+
"model": "gpt-image-2",
|
|
337
|
+
"prompt": "参考这张图,生成一张更适合科技产品广告的版本。",
|
|
338
|
+
"images": [
|
|
339
|
+
{
|
|
340
|
+
"image_url": "data:image/png;base64,替换为你的图片base64"
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
"size": "1024x1024",
|
|
344
|
+
"quality": "low",
|
|
345
|
+
"response_format": "b64_json"
|
|
346
|
+
}'
|
|
347
|
+
```
|
|
348
|
+
|
|
325
349
|
生图能力和账号套餐有关:
|
|
326
350
|
|
|
327
|
-
- `plus` 或更高套餐账号可正常调用 `images.generations`
|
|
351
|
+
- `plus` 或更高套餐账号可正常调用 `images.generations` / `images.edits`
|
|
328
352
|
- `free` 账号不支持生图,网关会直接返回明确错误,而不是继续请求上游
|
|
329
353
|
- 管理页会显示当前账号的 `plan` 和“生图能力”状态
|
|
330
354
|
- 当当前账号是 `free` 且你选中 `Images` 测试时,“发送请求”按钮会被直接禁用
|
|
@@ -345,6 +369,7 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
345
369
|
- `POST /v1/responses`
|
|
346
370
|
- `POST /v1/chat/completions`
|
|
347
371
|
- `POST /v1/images/generations`
|
|
372
|
+
- `POST /v1/images/edits`
|
|
348
373
|
|
|
349
374
|
### 7. 当前支持的主要参数
|
|
350
375
|
|
|
@@ -394,6 +419,24 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
394
419
|
- `response_format`
|
|
395
420
|
- `user`
|
|
396
421
|
|
|
422
|
+
`POST /v1/images/edits` 当前主要支持 JSON 请求:
|
|
423
|
+
|
|
424
|
+
- `prompt`
|
|
425
|
+
- `images`
|
|
426
|
+
- `image`
|
|
427
|
+
- `model`
|
|
428
|
+
- `n`
|
|
429
|
+
- `size`
|
|
430
|
+
- `quality`
|
|
431
|
+
- `background`
|
|
432
|
+
- `output_format`
|
|
433
|
+
- `output_compression`
|
|
434
|
+
- `moderation`
|
|
435
|
+
- `response_format`
|
|
436
|
+
- `user`
|
|
437
|
+
|
|
438
|
+
其中 `images` / `image` 支持 `image_url`,可以是 `https://...` URL、`data:image/...;base64,...`,或裸 base64 字符串。
|
|
439
|
+
|
|
397
440
|
### 8. 当前限制
|
|
398
441
|
|
|
399
442
|
- `stream=true` 目前只识别,不返回真实流式结果
|
|
@@ -402,7 +445,8 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
402
445
|
- `images.generations` 暂不支持 `n > 1`
|
|
403
446
|
- `images.generations` 当前只返回 `b64_json`,暂不支持托管图片 `url`
|
|
404
447
|
- `images.generations` 当前只透传 GPT Image 路径,不兼容 DALL·E 专有参数
|
|
405
|
-
- `images.
|
|
448
|
+
- `images.edits` 当前只支持 `application/json`,暂不支持 `multipart/form-data`、`mask` 和 `file_id`
|
|
449
|
+
- `images.generations` / `images.edits` 对账号套餐有要求;`free` 账号会被网关直接拦截并返回“不支持图片生成”
|
|
406
450
|
- 网关当前默认面向本地单用户使用
|
|
407
451
|
|
|
408
452
|
## 兼容说明
|
|
@@ -80,7 +80,7 @@ function parseAuthorizationInput(value) {
|
|
|
80
80
|
}
|
|
81
81
|
return { code: trimmed };
|
|
82
82
|
}
|
|
83
|
-
function extractProfile(accessToken, refreshToken, expires) {
|
|
83
|
+
function extractProfile(accessToken, refreshToken, expires, idToken) {
|
|
84
84
|
const payload = decodeJwtPayload(accessToken);
|
|
85
85
|
const authClaim = payload?.[JWT_CLAIM_PATH];
|
|
86
86
|
const accountId = authClaim?.chatgpt_account_id;
|
|
@@ -94,6 +94,7 @@ function extractProfile(accessToken, refreshToken, expires) {
|
|
|
94
94
|
mode: "oauth_account",
|
|
95
95
|
access: accessToken,
|
|
96
96
|
refresh: refreshToken,
|
|
97
|
+
idToken,
|
|
97
98
|
expires,
|
|
98
99
|
accountId,
|
|
99
100
|
email
|
|
@@ -124,6 +125,7 @@ async function exchangeAuthorizationCode(code, verifier) {
|
|
|
124
125
|
return {
|
|
125
126
|
access: json.access_token,
|
|
126
127
|
refresh: json.refresh_token,
|
|
128
|
+
idToken: json.id_token,
|
|
127
129
|
expires: Date.now() + json.expires_in * 1e3
|
|
128
130
|
};
|
|
129
131
|
}
|
|
@@ -150,7 +152,8 @@ async function refreshOpenAICodexToken(profile) {
|
|
|
150
152
|
return extractProfile(
|
|
151
153
|
json.access_token,
|
|
152
154
|
json.refresh_token,
|
|
153
|
-
Date.now() + json.expires_in * 1e3
|
|
155
|
+
Date.now() + json.expires_in * 1e3,
|
|
156
|
+
json.id_token ?? profile.idToken
|
|
154
157
|
);
|
|
155
158
|
}
|
|
156
159
|
function tryOpenBrowser(url) {
|
|
@@ -284,7 +287,7 @@ async function loginOpenAICodex() {
|
|
|
284
287
|
console.log("\u5DF2\u6536\u5230\u6388\u6743\u56DE\u8C03\uFF0C\u6B63\u5728\u4EA4\u6362 access token...");
|
|
285
288
|
const token = await exchangeAuthorizationCode(code, verifier);
|
|
286
289
|
console.log("token \u4EA4\u6362\u6210\u529F\uFF0C\u6B63\u5728\u89E3\u6790\u8D26\u53F7\u4FE1\u606F...");
|
|
287
|
-
return extractProfile(token.access, token.refresh, token.expires);
|
|
290
|
+
return extractProfile(token.access, token.refresh, token.expires, token.idToken);
|
|
288
291
|
} finally {
|
|
289
292
|
callbackServer.close();
|
|
290
293
|
}
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
refreshOpenAICodexToken
|
|
14
14
|
} from "../providers/openai-codex/oauth.js";
|
|
15
15
|
import { askOpenAICodex } from "../providers/openai-codex/chat.js";
|
|
16
|
+
import {
|
|
17
|
+
applyProfileToCodexAuth,
|
|
18
|
+
getCodexAuthStatus
|
|
19
|
+
} from "../store/codex-auth-store.js";
|
|
16
20
|
import {
|
|
17
21
|
exportProfilesToJson,
|
|
18
22
|
exportProfileToJson,
|
|
@@ -97,6 +101,13 @@ class AuthService {
|
|
|
97
101
|
getProfileImportTemplate() {
|
|
98
102
|
return getProfileImportTemplate();
|
|
99
103
|
}
|
|
104
|
+
async getCodexStatus() {
|
|
105
|
+
return getCodexAuthStatus();
|
|
106
|
+
}
|
|
107
|
+
async applyProfileToCodex(profileId, provider = "openai-codex") {
|
|
108
|
+
const profile = await this.requireFreshProfileWithIdToken(profileId, provider);
|
|
109
|
+
return applyProfileToCodexAuth(profile);
|
|
110
|
+
}
|
|
100
111
|
async getActiveProfile(provider = "openai-codex") {
|
|
101
112
|
const profile = await getActiveProfile();
|
|
102
113
|
if (!profile || profile.provider !== provider) {
|
|
@@ -152,6 +163,22 @@ class AuthService {
|
|
|
152
163
|
await saveProfile(refreshed);
|
|
153
164
|
return this.toManagedProfile(refreshed);
|
|
154
165
|
}
|
|
166
|
+
async requireFreshProfileWithIdToken(profileId, provider = "openai-codex") {
|
|
167
|
+
const profiles = await listProfiles();
|
|
168
|
+
const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
|
|
169
|
+
if (!profile) {
|
|
170
|
+
throw new Error(`\u6CA1\u6709\u627E\u5230\u8D26\u53F7: ${profileId}`);
|
|
171
|
+
}
|
|
172
|
+
if (profile.idToken && Date.now() < profile.expires) {
|
|
173
|
+
return this.toManagedProfile(profile);
|
|
174
|
+
}
|
|
175
|
+
const refreshed = await refreshOpenAICodexToken(profile);
|
|
176
|
+
await saveProfile(refreshed);
|
|
177
|
+
if (!refreshed.idToken) {
|
|
178
|
+
throw new Error("\u5237\u65B0 token \u6210\u529F\uFF0C\u4F46\u4E0A\u6E38\u6CA1\u6709\u8FD4\u56DE id_token\u3002");
|
|
179
|
+
}
|
|
180
|
+
return this.toManagedProfile(refreshed);
|
|
181
|
+
}
|
|
155
182
|
async logoutAll() {
|
|
156
183
|
await clearStore();
|
|
157
184
|
}
|
|
@@ -280,7 +280,8 @@ class ImageService {
|
|
|
280
280
|
background: request.background ?? "default",
|
|
281
281
|
outputFormat: request.outputFormat ?? "default",
|
|
282
282
|
outputCompression: typeof request.outputCompression === "number" ? request.outputCompression : void 0,
|
|
283
|
-
moderation: request.moderation ?? "default"
|
|
283
|
+
moderation: request.moderation ?? "default",
|
|
284
|
+
inputImageCount: request.inputImages?.length ?? 0
|
|
284
285
|
};
|
|
285
286
|
console.info("[gateway:image] upstream request", requestSummary);
|
|
286
287
|
const tool = {
|
|
@@ -305,6 +306,9 @@ class ImageService {
|
|
|
305
306
|
if (request.moderation) {
|
|
306
307
|
tool.moderation = request.moderation;
|
|
307
308
|
}
|
|
309
|
+
if (request.inputImages && request.inputImages.length > 0) {
|
|
310
|
+
tool.action = "edit";
|
|
311
|
+
}
|
|
308
312
|
for (let attempt = 1; attempt <= IMAGE_GENERATION_MAX_ATTEMPTS; attempt += 1) {
|
|
309
313
|
let result;
|
|
310
314
|
try {
|
|
@@ -320,7 +324,11 @@ class ImageService {
|
|
|
320
324
|
{
|
|
321
325
|
type: "input_text",
|
|
322
326
|
text: request.prompt
|
|
323
|
-
}
|
|
327
|
+
},
|
|
328
|
+
...(request.inputImages ?? []).map((image) => ({
|
|
329
|
+
type: "input_image",
|
|
330
|
+
image_url: image.imageUrl
|
|
331
|
+
}))
|
|
324
332
|
]
|
|
325
333
|
}
|
|
326
334
|
],
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function getCodexHomeDir() {
|
|
6
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
7
|
+
}
|
|
8
|
+
function getCodexAuthPath() {
|
|
9
|
+
return path.join(getCodexHomeDir(), "auth.json");
|
|
10
|
+
}
|
|
11
|
+
function createBackupSuffix() {
|
|
12
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
async function readCodexAuth() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(getCodexAuthPath(), "utf8");
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
return isRecord(parsed) ? parsed : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function getCodexAuthStatus() {
|
|
27
|
+
const authPath = getCodexAuthPath();
|
|
28
|
+
const auth = await readCodexAuth();
|
|
29
|
+
if (!auth) {
|
|
30
|
+
return {
|
|
31
|
+
path: authPath,
|
|
32
|
+
exists: false,
|
|
33
|
+
hasIdToken: false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const tokens = isRecord(auth.tokens) ? auth.tokens : {};
|
|
37
|
+
return {
|
|
38
|
+
path: authPath,
|
|
39
|
+
exists: true,
|
|
40
|
+
accountId: typeof tokens.account_id === "string" ? tokens.account_id : void 0,
|
|
41
|
+
hasIdToken: typeof tokens.id_token === "string" && tokens.id_token.length > 0,
|
|
42
|
+
lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function applyProfileToCodexAuth(profile) {
|
|
46
|
+
if (!profile.idToken) {
|
|
47
|
+
throw new Error("\u5F53\u524D\u8D26\u53F7\u7F3A\u5C11 id_token\u3002\u8BF7\u5148\u5237\u65B0\u8D26\u53F7 token \u6216\u91CD\u65B0\u5BFC\u5165\u5305\u542B id_token \u7684\u8D26\u53F7 JSON\u3002");
|
|
48
|
+
}
|
|
49
|
+
const authPath = getCodexAuthPath();
|
|
50
|
+
const codexHomeDir = path.dirname(authPath);
|
|
51
|
+
await fs.mkdir(codexHomeDir, { recursive: true });
|
|
52
|
+
let backupPath;
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(authPath);
|
|
55
|
+
backupPath = `${authPath}.azt-backup-${createBackupSuffix()}`;
|
|
56
|
+
await fs.copyFile(authPath, backupPath);
|
|
57
|
+
} catch {
|
|
58
|
+
backupPath = void 0;
|
|
59
|
+
}
|
|
60
|
+
const authFile = {
|
|
61
|
+
auth_mode: "chatgpt",
|
|
62
|
+
OPENAI_API_KEY: null,
|
|
63
|
+
tokens: {
|
|
64
|
+
id_token: profile.idToken,
|
|
65
|
+
access_token: profile.access,
|
|
66
|
+
refresh_token: profile.refresh,
|
|
67
|
+
account_id: profile.accountId
|
|
68
|
+
},
|
|
69
|
+
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
70
|
+
};
|
|
71
|
+
const tmpPath = `${authPath}.tmp-${process.pid}`;
|
|
72
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(authFile, null, 2)}
|
|
73
|
+
`, {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
mode: 384
|
|
76
|
+
});
|
|
77
|
+
await fs.rename(tmpPath, authPath);
|
|
78
|
+
await fs.chmod(authPath, 384);
|
|
79
|
+
return {
|
|
80
|
+
path: authPath,
|
|
81
|
+
exists: true,
|
|
82
|
+
accountId: profile.accountId,
|
|
83
|
+
hasIdToken: true,
|
|
84
|
+
lastRefresh: authFile.last_refresh,
|
|
85
|
+
backupPath,
|
|
86
|
+
appliedProfileId: profile.profileId,
|
|
87
|
+
appliedEmail: profile.email
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
applyProfileToCodexAuth,
|
|
92
|
+
getCodexAuthPath,
|
|
93
|
+
getCodexAuthStatus
|
|
94
|
+
};
|
|
@@ -67,6 +67,7 @@ function importProfileFromJson(value) {
|
|
|
67
67
|
}
|
|
68
68
|
const access = getString(value.access_token) ?? getString(value.access);
|
|
69
69
|
const refresh = getString(value.refresh_token) ?? getString(value.refresh);
|
|
70
|
+
const idToken = getString(value.id_token) ?? getString(value.idToken);
|
|
70
71
|
if (!access || !refresh) {
|
|
71
72
|
throw new Error("\u5BFC\u5165\u5931\u8D25: \u7F3A\u5C11 access_token/access \u6216 refresh_token/refresh\u3002");
|
|
72
73
|
}
|
|
@@ -80,6 +81,7 @@ function importProfileFromJson(value) {
|
|
|
80
81
|
mode: "oauth_account",
|
|
81
82
|
access,
|
|
82
83
|
refresh,
|
|
84
|
+
idToken,
|
|
83
85
|
expires,
|
|
84
86
|
accountId,
|
|
85
87
|
email
|
|
@@ -101,6 +103,7 @@ function exportProfileToJson(profile) {
|
|
|
101
103
|
type: "codex",
|
|
102
104
|
access_token: profile.access,
|
|
103
105
|
refresh_token: profile.refresh,
|
|
106
|
+
id_token: profile.idToken,
|
|
104
107
|
expired: new Date(profile.expires).toISOString(),
|
|
105
108
|
email: profile.email,
|
|
106
109
|
account_id: profile.accountId,
|
|
@@ -124,6 +127,7 @@ function getProfileImportTemplate() {
|
|
|
124
127
|
type: "codex",
|
|
125
128
|
access_token: "eyJ...access_token",
|
|
126
129
|
refresh_token: "rt_...",
|
|
130
|
+
id_token: "eyJ...id_token",
|
|
127
131
|
expired: "2026-05-04T22:13:00.000Z",
|
|
128
132
|
email: "user@example.com",
|
|
129
133
|
account_id: "\u53EF\u9009\uFF0C\u901A\u5E38\u4F1A\u4ECE access_token \u81EA\u52A8\u89E3\u6790",
|
|
@@ -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");
|
|
@@ -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 %
|
|
2597
|
+
const endpoint = index % 5 === 0
|
|
2565
2598
|
? "/v1/chat/completions"
|
|
2566
|
-
: index %
|
|
2599
|
+
: index % 5 === 1
|
|
2567
2600
|
? "/v1/responses"
|
|
2568
|
-
: index %
|
|
2601
|
+
: index % 5 === 2
|
|
2569
2602
|
? "/v1/models"
|
|
2570
|
-
:
|
|
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
|
|
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,7 +2841,7 @@ 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
2847
|
runTestBtn.disabled = isImageEndpoint && !capability.supported;
|
|
@@ -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);
|
package/dist/server/app.js
CHANGED
|
@@ -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;
|
|
@@ -352,14 +442,15 @@ function createApp(params) {
|
|
|
352
442
|
};
|
|
353
443
|
});
|
|
354
444
|
async function buildAdminConfig(request) {
|
|
355
|
-
const [status, models, modelCatalog, versionStatus, settings, profile, profiles] = await Promise.all([
|
|
445
|
+
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
356
446
|
ctx.authService.getStatus(),
|
|
357
447
|
ctx.modelService.listModels(),
|
|
358
448
|
ctx.modelService.getCatalog(),
|
|
359
449
|
ctx.versionService.getVersionStatus(),
|
|
360
450
|
ctx.configService.getSettings(),
|
|
361
451
|
ctx.authService.getActiveProfile(),
|
|
362
|
-
ctx.authService.listProfiles()
|
|
452
|
+
ctx.authService.listProfiles(),
|
|
453
|
+
ctx.authService.getCodexStatus()
|
|
363
454
|
]);
|
|
364
455
|
const origin = resolveOrigin(request);
|
|
365
456
|
return {
|
|
@@ -370,6 +461,7 @@ function createApp(params) {
|
|
|
370
461
|
versionStatus,
|
|
371
462
|
profile: serializeProfile(profile),
|
|
372
463
|
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
464
|
+
codex: codexStatus,
|
|
373
465
|
adminUrl: `${origin}/`,
|
|
374
466
|
baseUrl: `${origin}/v1`,
|
|
375
467
|
supportedEndpoints: [
|
|
@@ -392,6 +484,11 @@ function createApp(params) {
|
|
|
392
484
|
method: "POST",
|
|
393
485
|
path: "/v1/images/generations",
|
|
394
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"
|
|
395
492
|
}
|
|
396
493
|
]
|
|
397
494
|
};
|
|
@@ -518,6 +615,22 @@ function createApp(params) {
|
|
|
518
615
|
profile: await ctx.authService.exportProfile(parsed.data.profileId)
|
|
519
616
|
};
|
|
520
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
|
+
});
|
|
521
634
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
522
635
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
523
636
|
if (!parsed.success) {
|
|
@@ -733,6 +846,96 @@ function createApp(params) {
|
|
|
733
846
|
});
|
|
734
847
|
return response;
|
|
735
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
|
+
});
|
|
736
939
|
return app;
|
|
737
940
|
}
|
|
738
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": {
|