ai-zero-token 1.0.2 → 1.0.4
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 +20 -0
- package/README.md +9 -0
- package/dist/cli/commands/help.js +2 -1
- package/dist/cli/commands/models.js +14 -3
- package/dist/cli/index.js +1 -1
- package/dist/core/context.js +3 -0
- package/dist/core/models/openai-codex-models.js +89 -0
- package/dist/core/providers/http-client.js +41 -6
- package/dist/core/services/auth-service.js +36 -0
- package/dist/core/services/config-service.js +4 -4
- package/dist/core/services/image-service.js +119 -74
- package/dist/core/services/model-service.js +31 -6
- package/dist/core/services/version-service.js +97 -0
- package/dist/core/store/profile-store.js +11 -15
- package/dist/core/store/settings-store.js +10 -11
- package/dist/core/store/state-paths.js +54 -0
- package/dist/server/admin-page.js +641 -91
- package/dist/server/app.js +40 -3
- package/docs/API_USAGE.md +120 -0
- package/package.json +3 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.4 - 2026-04-24
|
|
4
|
+
|
|
5
|
+
- Moved persistent account and settings state to the user home directory at `~/.ai-zero-token/.state`.
|
|
6
|
+
- Added automatic one-time migration from the old package-local `.state` directory when available.
|
|
7
|
+
- Added `AI_ZERO_TOKEN_HOME` support for overriding the persistent state location.
|
|
8
|
+
- Fixed repeated login prompts after npm upgrades or global package reinstalls.
|
|
9
|
+
|
|
10
|
+
## 1.0.3 - 2026-04-24
|
|
11
|
+
|
|
12
|
+
- Added dynamic Codex model discovery from the local `~/.codex/models_cache.json` cache, with static model fallback when the cache is unavailable.
|
|
13
|
+
- Added `azt models --refresh` and a management-page action to re-read the local Codex model list without rebuilding the package.
|
|
14
|
+
- Added runtime version checks against npm, including a prominent update panel in the management UI when a newer version is available.
|
|
15
|
+
- Added 10-minute automatic refresh for quota snapshots and version status in the management UI.
|
|
16
|
+
- Improved quota display so account cards show used and remaining quota percentages clearly.
|
|
17
|
+
- Improved quota syncing so inactive or missing login state does not break runtime refresh.
|
|
18
|
+
- Improved image generation error handling with transient retries and clearer failure details.
|
|
19
|
+
- Preserved response headers when using the curl HTTP fallback so quota metadata can still be captured.
|
|
20
|
+
- Added Vibe Coding / OpenAI-compatible client integration documentation.
|
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ AI Zero Token 是一个本地优先的单用户 AI CLI 和本地网关。
|
|
|
18
18
|
- 直接代理 `gpt-image-2`,把图片生成能力暴露成 OpenAI 风格 `images.generations` 接口
|
|
19
19
|
- 启动 `azt start` 后即可获得本地管理页和本地网关,适合脚本、前端和自动化流程接入
|
|
20
20
|
- 支持多账号保存、切换当前账号、查看账号套餐 plan,以及当前账号是否支持生图
|
|
21
|
+
- 模型列表会优先同步本机 `~/.codex/models_cache.json`,不需要每次为新模型重新 build
|
|
22
|
+
- 管理页会每 10 分钟自动同步额度快照和版本状态,并提示当前版本是否可更新
|
|
21
23
|
- `free` 账号会在管理页直接预警,并在网关层明确拦截生图请求
|
|
22
24
|
|
|
23
25
|
如果你只关心一句话,可以把这个项目理解为:
|
|
@@ -78,6 +80,7 @@ AI Zero Token 就是围绕这些问题设计的。
|
|
|
78
80
|
- 在 token 过期时自动刷新
|
|
79
81
|
- 通过 `azt start` 一键启动本地 HTTP 网关和管理页面
|
|
80
82
|
- 在管理页面里完成多账号登录、查看账号状态、切换当前账号、切换默认模型、测试接口
|
|
83
|
+
- 模型列表优先读取本机 Codex 最新缓存,并支持在 CLI / 管理页手动同步
|
|
81
84
|
- 暴露 OpenAI 风格接口:
|
|
82
85
|
- `GET /v1/models`
|
|
83
86
|
- `POST /v1/responses`
|
|
@@ -131,6 +134,7 @@ npm install -g ai-zero-token
|
|
|
131
134
|
|
|
132
135
|
```bash
|
|
133
136
|
azt start
|
|
137
|
+
azt models --refresh
|
|
134
138
|
```
|
|
135
139
|
|
|
136
140
|
如果你是为了开发、构建、`npm link`、`npm pack` 或准备发布,单独看:
|
|
@@ -174,6 +178,10 @@ http://127.0.0.1:8787
|
|
|
174
178
|
http://127.0.0.1:8787/v1
|
|
175
179
|
```
|
|
176
180
|
|
|
181
|
+
Vibe Coding、OpenAI-compatible SDK 和脚本接入可以参考:
|
|
182
|
+
|
|
183
|
+
- [API 使用说明](docs/API_USAGE.md)
|
|
184
|
+
|
|
177
185
|
如果你要让本地网页直接从浏览器请求这个网关,现在已经默认开启 CORS。
|
|
178
186
|
|
|
179
187
|
如需限制来源,可以在启动前指定:
|
|
@@ -320,6 +328,7 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
320
328
|
- `GET /_gateway/health`
|
|
321
329
|
- `GET /_gateway/status`
|
|
322
330
|
- `GET /_gateway/models`
|
|
331
|
+
- `POST /_gateway/models/refresh`
|
|
323
332
|
- `GET /_gateway/admin/config`
|
|
324
333
|
- `POST /_gateway/admin/login`
|
|
325
334
|
- `POST /_gateway/admin/logout`
|
|
@@ -4,6 +4,7 @@ function printHelp() {
|
|
|
4
4
|
|
|
5
5
|
azt login
|
|
6
6
|
azt models
|
|
7
|
+
azt models --refresh
|
|
7
8
|
azt status
|
|
8
9
|
azt ask "\u4F60\u597D\uFF0C\u8BF7\u7B80\u5355\u4ECB\u7ECD\u4E00\u4E0B\u81EA\u5DF1"
|
|
9
10
|
azt ask --model gpt-5.3-codex "\u4F60\u597D"
|
|
@@ -16,7 +17,7 @@ function printHelp() {
|
|
|
16
17
|
\u8BF4\u660E:
|
|
17
18
|
|
|
18
19
|
login \u8D70\u771F\u5B9E OpenAI Codex OAuth\uFF0C\u65B0\u589E\u5E76\u4FDD\u5B58\u4E00\u4E2A\u8D26\u53F7 profile
|
|
19
|
-
models \u67E5\u770B\
|
|
20
|
+
models \u67E5\u770B\u5F53\u524D\u53EF\u7528\u6A21\u578B\u5217\u8868\uFF1B\u4F18\u5148\u8BFB\u53D6 ~/.codex/models_cache.json\uFF0C--refresh \u53EF\u624B\u52A8\u91CD\u8BFB
|
|
20
21
|
status \u67E5\u770B\u5F53\u524D demo \u5F53\u524D\u6FC0\u6D3B\u8D26\u53F7\u3001\u8D26\u53F7\u6570\u91CF\u548C\u8FC7\u671F\u65F6\u95F4
|
|
21
22
|
ask \u7528\u4FDD\u5B58\u7684 token \u8C03\u771F\u5B9E Codex Responses API
|
|
22
23
|
\u5B9E\u9A8C\u6A21\u5F0F\u53EF\u7528 --payload-file \u900F\u4F20\u989D\u5916\u8BF7\u6C42\u4F53\uFF0C\u914D\u5408 --dump-raw / --print-raw \u89C2\u5BDF SSE \u539F\u59CB\u4E8B\u4EF6
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createGatewayContext } from "../../core/context.js";
|
|
3
|
-
async function runModelsCommand() {
|
|
3
|
+
async function runModelsCommand(args = []) {
|
|
4
4
|
const ctx = createGatewayContext();
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const refresh = args.includes("--refresh");
|
|
6
|
+
const result = refresh ? await ctx.modelService.refreshModels() : {
|
|
7
|
+
models: await ctx.modelService.listModels(),
|
|
8
|
+
catalog: await ctx.modelService.getCatalog()
|
|
9
|
+
};
|
|
10
|
+
console.log(refresh ? "\u5DF2\u91CD\u65B0\u8BFB\u53D6 Codex \u6A21\u578B\u5217\u8868:" : "\u5F53\u524D demo \u53EF\u7528\u6A21\u578B\u5217\u8868:");
|
|
11
|
+
console.log(`- \u6765\u6E90: ${result.catalog.source === "codex-cache" ? "Codex \u672C\u5730\u7F13\u5B58" : "\u9879\u76EE\u5185\u7F6E\u56DE\u9000\u5217\u8868"}`);
|
|
12
|
+
console.log(`- \u8DEF\u5F84: ${result.catalog.cachePath}`);
|
|
13
|
+
if (result.catalog.fetchedAt) {
|
|
14
|
+
console.log(`- Codex \u66F4\u65B0\u65F6\u95F4: ${result.catalog.fetchedAt}`);
|
|
15
|
+
}
|
|
16
|
+
console.log(`- \u6570\u91CF: ${result.catalog.modelCount}`);
|
|
17
|
+
for (const model of result.models) {
|
|
7
18
|
const suffix = model.isDefault ? " (\u9ED8\u8BA4)" : "";
|
|
8
19
|
console.log(`- ${model.id}${suffix}`);
|
|
9
20
|
}
|
package/dist/cli/index.js
CHANGED
package/dist/core/context.js
CHANGED
|
@@ -4,10 +4,12 @@ import { AuthService } from "./services/auth-service.js";
|
|
|
4
4
|
import { ChatService } from "./services/chat-service.js";
|
|
5
5
|
import { ImageService } from "./services/image-service.js";
|
|
6
6
|
import { ModelService } from "./services/model-service.js";
|
|
7
|
+
import { VersionService } from "./services/version-service.js";
|
|
7
8
|
function createGatewayContext() {
|
|
8
9
|
const configService = new ConfigService();
|
|
9
10
|
const authService = new AuthService(configService);
|
|
10
11
|
const modelService = new ModelService(configService);
|
|
12
|
+
const versionService = new VersionService();
|
|
11
13
|
const chatService = new ChatService({
|
|
12
14
|
authService,
|
|
13
15
|
modelService
|
|
@@ -20,6 +22,7 @@ function createGatewayContext() {
|
|
|
20
22
|
configService,
|
|
21
23
|
authService,
|
|
22
24
|
modelService,
|
|
25
|
+
versionService,
|
|
23
26
|
chatService,
|
|
24
27
|
imageService
|
|
25
28
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
const DEFAULT_CODEX_MODEL = "gpt-5.4";
|
|
3
6
|
const CODEX_MODEL_INFOS = [
|
|
4
7
|
{ provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4", input: ["text", "image"], source: "static" },
|
|
@@ -22,6 +25,88 @@ const SUPPORTED_CODEX_MODELS = [
|
|
|
22
25
|
"gpt-5.1-codex-mini",
|
|
23
26
|
"gpt-5.1-codex-max"
|
|
24
27
|
];
|
|
28
|
+
function getCodexModelsCachePath() {
|
|
29
|
+
return process.env.CODEX_MODELS_CACHE_PATH || path.join(os.homedir(), ".codex", "models_cache.json");
|
|
30
|
+
}
|
|
31
|
+
function normalizeInputModalities(input) {
|
|
32
|
+
const rawValues = Array.isArray(input) ? input : [];
|
|
33
|
+
const values = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const item of rawValues) {
|
|
35
|
+
if (item === "text" || item === "image") {
|
|
36
|
+
values.add(item);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (values.size === 0) {
|
|
40
|
+
values.add("text");
|
|
41
|
+
}
|
|
42
|
+
return Array.from(values);
|
|
43
|
+
}
|
|
44
|
+
function normalizeCodexCacheEntry(entry) {
|
|
45
|
+
if (!entry || typeof entry.slug !== "string" || !entry.slug) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (typeof entry.visibility === "string" && entry.visibility !== "list") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
provider: "openai-codex",
|
|
53
|
+
id: entry.slug,
|
|
54
|
+
name: typeof entry.display_name === "string" && entry.display_name ? entry.display_name : entry.slug,
|
|
55
|
+
input: normalizeInputModalities(entry.input_modalities),
|
|
56
|
+
source: "codex-cache"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function dedupeModels(models) {
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
const next = [];
|
|
62
|
+
for (const model of models) {
|
|
63
|
+
if (seen.has(model.id)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
seen.add(model.id);
|
|
67
|
+
next.push(model);
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
async function getCodexModelCatalog() {
|
|
72
|
+
const cachePath = getCodexModelsCachePath();
|
|
73
|
+
try {
|
|
74
|
+
const raw = await fs.readFile(cachePath, "utf8");
|
|
75
|
+
const parsed = JSON.parse(raw);
|
|
76
|
+
const models = dedupeModels((parsed.models ?? []).map(normalizeCodexCacheEntry).filter(Boolean));
|
|
77
|
+
if (models.length > 0) {
|
|
78
|
+
return {
|
|
79
|
+
models,
|
|
80
|
+
catalog: {
|
|
81
|
+
source: "codex-cache",
|
|
82
|
+
cachePath,
|
|
83
|
+
fetchedAt: parsed.fetched_at,
|
|
84
|
+
modelCount: models.length
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
models: CODEX_MODEL_INFOS,
|
|
92
|
+
catalog: {
|
|
93
|
+
source: "static-fallback",
|
|
94
|
+
cachePath,
|
|
95
|
+
modelCount: CODEX_MODEL_INFOS.length
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function hasCodexModel(model) {
|
|
100
|
+
const { models } = await getCodexModelCatalog();
|
|
101
|
+
return models.some((item) => item.id === model);
|
|
102
|
+
}
|
|
103
|
+
async function getPreferredCodexModel() {
|
|
104
|
+
const { models } = await getCodexModelCatalog();
|
|
105
|
+
if (models.some((item) => item.id === DEFAULT_CODEX_MODEL)) {
|
|
106
|
+
return DEFAULT_CODEX_MODEL;
|
|
107
|
+
}
|
|
108
|
+
return models[0]?.id ?? DEFAULT_CODEX_MODEL;
|
|
109
|
+
}
|
|
25
110
|
function isSupportedCodexModel(model) {
|
|
26
111
|
return SUPPORTED_CODEX_MODELS.includes(model);
|
|
27
112
|
}
|
|
@@ -29,5 +114,9 @@ export {
|
|
|
29
114
|
CODEX_MODEL_INFOS,
|
|
30
115
|
DEFAULT_CODEX_MODEL,
|
|
31
116
|
SUPPORTED_CODEX_MODELS,
|
|
117
|
+
getCodexModelCatalog,
|
|
118
|
+
getCodexModelsCachePath,
|
|
119
|
+
getPreferredCodexModel,
|
|
120
|
+
hasCodexModel,
|
|
32
121
|
isSupportedCodexModel
|
|
33
122
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
const CURL_STATUS_MARKER = "\n__CURL_STATUS__:";
|
|
4
|
+
const CURL_HEADERS_MARKER = "\n__CURL_HEADERS__:";
|
|
4
5
|
let requestSequence = 0;
|
|
5
6
|
function nextRequestId() {
|
|
6
7
|
requestSequence += 1;
|
|
@@ -37,6 +38,23 @@ function normalizeHeaders(headers) {
|
|
|
37
38
|
});
|
|
38
39
|
return normalized;
|
|
39
40
|
}
|
|
41
|
+
function normalizeCurlHeaders(value) {
|
|
42
|
+
if (!value || typeof value !== "object") {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(value).flatMap(([key, rawValue]) => {
|
|
47
|
+
if (typeof rawValue === "string" && rawValue.trim()) {
|
|
48
|
+
return [[key.toLowerCase(), rawValue.trim()]];
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(rawValue)) {
|
|
51
|
+
const joined = rawValue.filter((item) => typeof item === "string" && item.trim()).join(", ");
|
|
52
|
+
return joined ? [[key.toLowerCase(), joined]] : [];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
40
58
|
async function runCurlRequest(init, params) {
|
|
41
59
|
const requestId = params?.requestId ?? nextRequestId();
|
|
42
60
|
const startedAt = performance.now();
|
|
@@ -48,7 +66,7 @@ async function runCurlRequest(init, params) {
|
|
|
48
66
|
init.method,
|
|
49
67
|
init.url,
|
|
50
68
|
"--write-out",
|
|
51
|
-
`${CURL_STATUS_MARKER}%{http_code}`
|
|
69
|
+
`${CURL_STATUS_MARKER}%{http_code}${CURL_HEADERS_MARKER}%{header_json}`
|
|
52
70
|
];
|
|
53
71
|
for (const [key, value] of Object.entries(init.headers ?? {})) {
|
|
54
72
|
args.push("--header", `${key}: ${value}`);
|
|
@@ -82,16 +100,33 @@ async function runCurlRequest(init, params) {
|
|
|
82
100
|
throw new Error(stderr.trim() || `curl \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}`);
|
|
83
101
|
}
|
|
84
102
|
const parseStartedAt = performance.now();
|
|
85
|
-
const
|
|
86
|
-
|
|
103
|
+
const statusMarkerIndex = stdout.lastIndexOf(CURL_STATUS_MARKER);
|
|
104
|
+
const headersMarkerIndex = stdout.lastIndexOf(CURL_HEADERS_MARKER);
|
|
105
|
+
if (statusMarkerIndex === -1) {
|
|
87
106
|
throw new Error("curl \u54CD\u5E94\u7F3A\u5C11\u72B6\u6001\u7801\u6807\u8BB0\u3002");
|
|
88
107
|
}
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
if (headersMarkerIndex === -1 || headersMarkerIndex < statusMarkerIndex) {
|
|
109
|
+
throw new Error("curl \u54CD\u5E94\u7F3A\u5C11\u54CD\u5E94\u5934\u6807\u8BB0\u3002");
|
|
110
|
+
}
|
|
111
|
+
const body = stdout.slice(0, statusMarkerIndex);
|
|
112
|
+
const statusText = stdout.slice(statusMarkerIndex + CURL_STATUS_MARKER.length, headersMarkerIndex).trim();
|
|
91
113
|
const status = Number.parseInt(statusText, 10);
|
|
92
114
|
if (!Number.isFinite(status)) {
|
|
93
115
|
throw new Error(`\u65E0\u6CD5\u89E3\u6790 curl \u72B6\u6001\u7801: ${statusText}`);
|
|
94
116
|
}
|
|
117
|
+
const headersText = stdout.slice(headersMarkerIndex + CURL_HEADERS_MARKER.length).trim();
|
|
118
|
+
let headers = {};
|
|
119
|
+
if (headersText) {
|
|
120
|
+
try {
|
|
121
|
+
headers = normalizeCurlHeaders(JSON.parse(headersText));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn("[http] failed to parse curl response headers", {
|
|
124
|
+
requestId,
|
|
125
|
+
url: init.url,
|
|
126
|
+
error: error instanceof Error ? error.message : String(error)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
95
130
|
phases.parseResponseMs = performance.now() - parseStartedAt;
|
|
96
131
|
const timing = finalizeTiming(startedAt, phases);
|
|
97
132
|
logHttpTiming({
|
|
@@ -110,7 +145,7 @@ async function runCurlRequest(init, params) {
|
|
|
110
145
|
transport: "curl",
|
|
111
146
|
timing,
|
|
112
147
|
requestId,
|
|
113
|
-
headers
|
|
148
|
+
headers
|
|
114
149
|
};
|
|
115
150
|
}
|
|
116
151
|
async function requestText(init) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
loginOpenAICodex,
|
|
13
13
|
refreshOpenAICodexToken
|
|
14
14
|
} from "../providers/openai-codex/oauth.js";
|
|
15
|
+
import { askOpenAICodex } from "../providers/openai-codex/chat.js";
|
|
15
16
|
class AuthService {
|
|
16
17
|
constructor(configService) {
|
|
17
18
|
this.configService = configService;
|
|
@@ -107,6 +108,41 @@ class AuthService {
|
|
|
107
108
|
async logoutAll() {
|
|
108
109
|
await clearStore();
|
|
109
110
|
}
|
|
111
|
+
async syncActiveProfileQuota(provider = "openai-codex", options) {
|
|
112
|
+
let profile;
|
|
113
|
+
try {
|
|
114
|
+
profile = await this.requireUsableProfile(provider);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (options?.suppressErrors) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
const model = await this.configService.getDefaultModel(provider);
|
|
122
|
+
try {
|
|
123
|
+
const result = await askOpenAICodex({
|
|
124
|
+
profile,
|
|
125
|
+
model,
|
|
126
|
+
system: "Reply with OK only.",
|
|
127
|
+
prompt: "ping",
|
|
128
|
+
bodyOverride: {
|
|
129
|
+
text: { verbosity: "low" }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
await this.updateProfileQuota(profile.profileId, result.quota, provider);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const quota = error.quota;
|
|
135
|
+
await this.updateProfileQuota(profile.profileId, quota, provider);
|
|
136
|
+
if (!options?.suppressErrors) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
console.warn("[auth] sync active profile quota failed", {
|
|
140
|
+
provider,
|
|
141
|
+
profileId: profile.profileId,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
110
146
|
async updateProfileQuota(profileId, quota, provider = "openai-codex") {
|
|
111
147
|
if (!quota) {
|
|
112
148
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { getPreferredCodexModel, hasCodexModel } from "../models/openai-codex-models.js";
|
|
3
3
|
import {
|
|
4
4
|
createDefaultSettings,
|
|
5
5
|
loadSettings,
|
|
@@ -23,14 +23,14 @@ class ConfigService {
|
|
|
23
23
|
if (provider !== "openai-codex") {
|
|
24
24
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
25
25
|
}
|
|
26
|
-
return
|
|
26
|
+
return await hasCodexModel(settings.defaultModel) ? settings.defaultModel : getPreferredCodexModel();
|
|
27
27
|
}
|
|
28
28
|
async setDefaultModel(model, provider = "openai-codex") {
|
|
29
29
|
if (provider !== "openai-codex") {
|
|
30
30
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
31
31
|
}
|
|
32
|
-
if (!
|
|
33
|
-
throw new Error(`\u5F53\u524D
|
|
32
|
+
if (!await hasCodexModel(model)) {
|
|
33
|
+
throw new Error(`\u5F53\u524D\u7F51\u5173\u672A\u627E\u5230\u53EF\u7528\u6A21\u578B: ${model}`);
|
|
34
34
|
}
|
|
35
35
|
const settings = await this.getSettings();
|
|
36
36
|
const next = {
|
|
@@ -25,6 +25,8 @@ const SUPPORTED_IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
|
|
|
25
25
|
"transparent",
|
|
26
26
|
"opaque"
|
|
27
27
|
]);
|
|
28
|
+
const IMAGE_GENERATION_MAX_ATTEMPTS = 3;
|
|
29
|
+
const IMAGE_GENERATION_RETRY_DELAYS_MS = [1500, 4e3];
|
|
28
30
|
function truncateForLog(value, max = 160) {
|
|
29
31
|
if (value.length <= max) {
|
|
30
32
|
return value;
|
|
@@ -167,7 +169,24 @@ function summarizeImageDebug(raw) {
|
|
|
167
169
|
image_events: imageEvents
|
|
168
170
|
};
|
|
169
171
|
}
|
|
170
|
-
function
|
|
172
|
+
function extractRequestIdFromMessage(message) {
|
|
173
|
+
const match = message.match(/request ID ([a-z0-9-]+)/i);
|
|
174
|
+
return match?.[1];
|
|
175
|
+
}
|
|
176
|
+
function createImageFailureDetails(code, message) {
|
|
177
|
+
const normalizedMessage = typeof message === "string" && message.trim() ? message.trim() : typeof code === "string" && code.trim() ? code.trim() : null;
|
|
178
|
+
if (!normalizedMessage) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const normalizedCode = typeof code === "string" && code.trim() ? code.trim() : void 0;
|
|
182
|
+
return {
|
|
183
|
+
code: normalizedCode,
|
|
184
|
+
message: normalizedMessage,
|
|
185
|
+
requestId: extractRequestIdFromMessage(normalizedMessage),
|
|
186
|
+
transient: normalizedCode === "server_error" || /retry your request/i.test(normalizedMessage) || /temporar/i.test(normalizedMessage)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function extractImageFailureDetails(raw) {
|
|
171
190
|
if (!isRecord(raw)) {
|
|
172
191
|
return null;
|
|
173
192
|
}
|
|
@@ -175,9 +194,9 @@ function extractImageFailureMessage(raw) {
|
|
|
175
194
|
if (response) {
|
|
176
195
|
const responseError = isRecord(response.error) ? response.error : null;
|
|
177
196
|
const responseStatus = typeof response.status === "string" ? response.status : void 0;
|
|
178
|
-
const
|
|
179
|
-
if (responseStatus === "failed" &&
|
|
180
|
-
return
|
|
197
|
+
const details = createImageFailureDetails(responseError?.code, responseError?.message);
|
|
198
|
+
if (responseStatus === "failed" && details) {
|
|
199
|
+
return details;
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
const events = Array.isArray(raw.events) ? raw.events : [];
|
|
@@ -187,21 +206,29 @@ function extractImageFailureMessage(raw) {
|
|
|
187
206
|
}
|
|
188
207
|
if (event.type === "error") {
|
|
189
208
|
const eventError = isRecord(event.error) ? event.error : event;
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
return
|
|
209
|
+
const details = createImageFailureDetails(eventError.code, eventError.message);
|
|
210
|
+
if (details) {
|
|
211
|
+
return details;
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
214
|
if (event.type === "response.failed" && isRecord(event.response)) {
|
|
196
215
|
const responseError = isRecord(event.response.error) ? event.response.error : null;
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
216
|
+
const details = createImageFailureDetails(responseError?.code, responseError?.message);
|
|
217
|
+
if (details) {
|
|
218
|
+
return details;
|
|
200
219
|
}
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
return null;
|
|
204
223
|
}
|
|
224
|
+
function createError(message, statusCode) {
|
|
225
|
+
const error = new Error(message);
|
|
226
|
+
error.statusCode = statusCode;
|
|
227
|
+
return error;
|
|
228
|
+
}
|
|
229
|
+
function sleep(ms) {
|
|
230
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
|
+
}
|
|
205
232
|
function extractImageUsage(raw) {
|
|
206
233
|
if (!isRecord(raw) || !isRecord(raw.response)) {
|
|
207
234
|
return void 0;
|
|
@@ -283,76 +310,94 @@ class ImageService {
|
|
|
283
310
|
if (request.moderation) {
|
|
284
311
|
tool.moderation = request.moderation;
|
|
285
312
|
}
|
|
286
|
-
let
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
bodyOverride: {
|
|
313
|
+
for (let attempt = 1; attempt <= IMAGE_GENERATION_MAX_ATTEMPTS; attempt += 1) {
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
result = await askOpenAICodex({
|
|
317
|
+
profile,
|
|
292
318
|
model: orchestratorModel,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
319
|
+
bodyOverride: {
|
|
320
|
+
model: orchestratorModel,
|
|
321
|
+
input: [
|
|
322
|
+
{
|
|
323
|
+
role: "user",
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: "input_text",
|
|
327
|
+
text: request.prompt
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
tools: [tool],
|
|
333
|
+
tool_choice: {
|
|
334
|
+
type: "image_generation"
|
|
335
|
+
},
|
|
336
|
+
include: ["reasoning.encrypted_content"]
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
await this.deps.authService.updateProfileQuota(profile.profileId, result.quota, "openai-codex");
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const quota = error.quota;
|
|
342
|
+
await this.deps.authService.updateProfileQuota(profile.profileId, quota, "openai-codex");
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
const raw = isRecord(result.raw) ? result.raw : {};
|
|
346
|
+
const response = isRecord(raw.response) ? raw.response : null;
|
|
347
|
+
const images = collectImageGenerationOutputs(raw);
|
|
348
|
+
const debugSummary = summarizeImageDebug(raw);
|
|
349
|
+
if (images.length === 0) {
|
|
350
|
+
const upstreamFailure = extractImageFailureDetails(raw);
|
|
351
|
+
console.error("[gateway:image] parse failure", {
|
|
352
|
+
...requestSummary,
|
|
353
|
+
attempt,
|
|
354
|
+
upstreamFailure,
|
|
355
|
+
debug: debugSummary
|
|
356
|
+
});
|
|
357
|
+
if (upstreamFailure?.transient && attempt < IMAGE_GENERATION_MAX_ATTEMPTS) {
|
|
358
|
+
const retryDelayMs = IMAGE_GENERATION_RETRY_DELAYS_MS[attempt - 1] ?? 4e3;
|
|
359
|
+
console.warn("[gateway:image] transient upstream failure, retrying", {
|
|
360
|
+
...requestSummary,
|
|
361
|
+
attempt,
|
|
362
|
+
retryDelayMs,
|
|
363
|
+
code: upstreamFailure.code,
|
|
364
|
+
requestId: upstreamFailure.requestId
|
|
365
|
+
});
|
|
366
|
+
await sleep(retryDelayMs);
|
|
367
|
+
continue;
|
|
309
368
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
369
|
+
if (upstreamFailure) {
|
|
370
|
+
const reason = upstreamFailure.code ? `${upstreamFailure.code}: ${upstreamFailure.message}` : upstreamFailure.message;
|
|
371
|
+
throw createError(`\u4E0A\u6E38\u56FE\u7247\u751F\u6210\u5931\u8D25: ${reason}`, upstreamFailure.transient ? 503 : 502);
|
|
372
|
+
}
|
|
373
|
+
throw createError("\u56FE\u7247\u751F\u6210\u8BF7\u6C42\u5DF2\u5B8C\u6210\uFF0C\u4F46\u6CA1\u6709\u89E3\u6790\u51FA image_generation_call \u7ED3\u679C\u3002", 502);
|
|
374
|
+
}
|
|
375
|
+
const first = images[0];
|
|
376
|
+
const imageResult = {
|
|
377
|
+
created: typeof response?.created_at === "number" ? response.created_at : Math.floor(Date.now() / 1e3),
|
|
378
|
+
data: images.map((image) => ({
|
|
379
|
+
b64_json: image.result ?? "",
|
|
380
|
+
...image.revised_prompt ? { revised_prompt: image.revised_prompt } : {}
|
|
381
|
+
})),
|
|
382
|
+
background: normalizeReturnedBackground(first.background),
|
|
383
|
+
output_format: normalizeReturnedFormat(first.output_format),
|
|
384
|
+
quality: normalizeReturnedQuality(first.quality),
|
|
385
|
+
size: normalizeReturnedSize(first.size, request.size),
|
|
386
|
+
usage: extractImageUsage(raw)
|
|
387
|
+
};
|
|
388
|
+
console.info("[gateway:image] upstream response", {
|
|
324
389
|
...requestSummary,
|
|
325
|
-
|
|
390
|
+
attempt,
|
|
391
|
+
imageCount: imageResult.data.length,
|
|
392
|
+
firstImageBase64Length: imageResult.data[0]?.b64_json.length ?? 0,
|
|
393
|
+
outputFormat: imageResult.output_format ?? request.outputFormat ?? "unknown",
|
|
394
|
+
quality: imageResult.quality ?? request.quality ?? "unknown",
|
|
395
|
+
size: imageResult.size ?? request.size ?? "unknown",
|
|
326
396
|
debug: debugSummary
|
|
327
397
|
});
|
|
328
|
-
|
|
329
|
-
throw new Error(`\u4E0A\u6E38\u56FE\u7247\u751F\u6210\u5931\u8D25: ${upstreamFailure}`);
|
|
330
|
-
}
|
|
331
|
-
throw new Error("\u56FE\u7247\u751F\u6210\u8BF7\u6C42\u5DF2\u5B8C\u6210\uFF0C\u4F46\u6CA1\u6709\u89E3\u6790\u51FA image_generation_call \u7ED3\u679C\u3002");
|
|
398
|
+
return imageResult;
|
|
332
399
|
}
|
|
333
|
-
|
|
334
|
-
const imageResult = {
|
|
335
|
-
created: typeof response?.created_at === "number" ? response.created_at : Math.floor(Date.now() / 1e3),
|
|
336
|
-
data: images.map((image) => ({
|
|
337
|
-
b64_json: image.result ?? "",
|
|
338
|
-
...image.revised_prompt ? { revised_prompt: image.revised_prompt } : {}
|
|
339
|
-
})),
|
|
340
|
-
background: normalizeReturnedBackground(first.background),
|
|
341
|
-
output_format: normalizeReturnedFormat(first.output_format),
|
|
342
|
-
quality: normalizeReturnedQuality(first.quality),
|
|
343
|
-
size: normalizeReturnedSize(first.size, request.size),
|
|
344
|
-
usage: extractImageUsage(raw)
|
|
345
|
-
};
|
|
346
|
-
console.info("[gateway:image] upstream response", {
|
|
347
|
-
...requestSummary,
|
|
348
|
-
imageCount: imageResult.data.length,
|
|
349
|
-
firstImageBase64Length: imageResult.data[0]?.b64_json.length ?? 0,
|
|
350
|
-
outputFormat: imageResult.output_format ?? request.outputFormat ?? "unknown",
|
|
351
|
-
quality: imageResult.quality ?? request.quality ?? "unknown",
|
|
352
|
-
size: imageResult.size ?? request.size ?? "unknown",
|
|
353
|
-
debug: debugSummary
|
|
354
|
-
});
|
|
355
|
-
return imageResult;
|
|
400
|
+
throw createError("\u56FE\u7247\u751F\u6210\u5931\u8D25\uFF1A\u8D85\u8FC7\u6700\u5927\u91CD\u8BD5\u6B21\u6570\u3002", 503);
|
|
356
401
|
}
|
|
357
402
|
}
|
|
358
403
|
export {
|