ai-zero-token 1.0.1 → 1.0.3
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 +14 -0
- package/README.md +235 -69
- package/dist/api.js +0 -1
- package/dist/cli/commands/ask.js +131 -5
- package/dist/cli/commands/clear.js +0 -1
- package/dist/cli/commands/help.js +17 -11
- package/dist/cli/commands/login.js +0 -1
- package/dist/cli/commands/models.js +14 -4
- package/dist/cli/commands/serve.js +41 -4
- package/dist/cli/commands/start.js +10 -0
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/index.js +5 -2
- package/dist/cli/shared.js +57 -6
- package/dist/cli.js +0 -1
- package/dist/core/context.js +10 -2
- package/dist/core/models/openai-codex-models.js +89 -1
- package/dist/core/providers/http-client.js +137 -14
- package/dist/core/providers/openai-codex/chat.js +217 -24
- package/dist/core/providers/openai-codex/oauth.js +15 -4
- package/dist/core/providers/openai-codex/pkce.js +0 -1
- package/dist/core/services/auth-service.js +125 -16
- package/dist/core/services/chat-service.js +24 -14
- package/dist/core/services/config-service.js +4 -5
- package/dist/core/services/image-service.js +405 -0
- package/dist/core/services/model-service.js +35 -8
- package/dist/core/services/version-service.js +97 -0
- package/dist/core/store/profile-store.js +79 -6
- package/dist/core/store/settings-store.js +1 -2
- package/dist/core/types.js +0 -1
- package/dist/http.js +0 -1
- package/dist/models.js +0 -1
- package/dist/oauth.js +0 -1
- package/dist/pkce.js +0 -1
- package/dist/server/admin-page.js +3165 -0
- package/dist/server/app.js +599 -40
- package/dist/server/index.js +0 -1
- package/dist/store.js +0 -1
- package/docs/API_USAGE.md +120 -0
- package/package.json +14 -3
- package/dist/api.js.map +0 -1
- package/dist/cli/commands/ask.js.map +0 -1
- package/dist/cli/commands/clear.js.map +0 -1
- package/dist/cli/commands/help.js.map +0 -1
- package/dist/cli/commands/login.js.map +0 -1
- package/dist/cli/commands/models.js.map +0 -1
- package/dist/cli/commands/serve.js.map +0 -1
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/shared.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/core/context.js.map +0 -1
- package/dist/core/models/openai-codex-models.js.map +0 -1
- package/dist/core/providers/http-client.js.map +0 -1
- package/dist/core/providers/openai-codex/chat.js.map +0 -1
- package/dist/core/providers/openai-codex/oauth.js.map +0 -1
- package/dist/core/providers/openai-codex/pkce.js.map +0 -1
- package/dist/core/services/auth-service.js.map +0 -1
- package/dist/core/services/chat-service.js.map +0 -1
- package/dist/core/services/config-service.js.map +0 -1
- package/dist/core/services/model-service.js.map +0 -1
- package/dist/core/store/profile-store.js.map +0 -1
- package/dist/core/store/settings-store.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/models.js.map +0 -1
- package/dist/oauth.js.map +0 -1
- package/dist/pkce.js.map +0 -1
- package/dist/server/app.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/store.js.map +0 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { askOpenAICodex } from "../providers/openai-codex/chat.js";
|
|
3
|
+
const SUPPORTED_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
4
|
+
"gpt-image-1",
|
|
5
|
+
"gpt-image-1-mini",
|
|
6
|
+
"gpt-image-1.5",
|
|
7
|
+
"gpt-image-2"
|
|
8
|
+
]);
|
|
9
|
+
const SUPPORTED_IMAGE_SIZES = /* @__PURE__ */ new Set([
|
|
10
|
+
"1024x1024",
|
|
11
|
+
"1024x1536",
|
|
12
|
+
"1536x1024"
|
|
13
|
+
]);
|
|
14
|
+
const SUPPORTED_IMAGE_QUALITIES = /* @__PURE__ */ new Set([
|
|
15
|
+
"low",
|
|
16
|
+
"medium",
|
|
17
|
+
"high"
|
|
18
|
+
]);
|
|
19
|
+
const SUPPORTED_IMAGE_FORMATS = /* @__PURE__ */ new Set([
|
|
20
|
+
"png",
|
|
21
|
+
"webp",
|
|
22
|
+
"jpeg"
|
|
23
|
+
]);
|
|
24
|
+
const SUPPORTED_IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
|
|
25
|
+
"transparent",
|
|
26
|
+
"opaque"
|
|
27
|
+
]);
|
|
28
|
+
const IMAGE_GENERATION_MAX_ATTEMPTS = 3;
|
|
29
|
+
const IMAGE_GENERATION_RETRY_DELAYS_MS = [1500, 4e3];
|
|
30
|
+
function truncateForLog(value, max = 160) {
|
|
31
|
+
if (value.length <= max) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return `${value.slice(0, max)}...`;
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function toImageGenerationOutput(value) {
|
|
40
|
+
if (!isRecord(value) || value.type !== "image_generation_call") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function toImageGenerationEventOutput(value) {
|
|
46
|
+
if (!isRecord(value) || typeof value.type !== "string") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (value.type.startsWith("response.output_item.") && isRecord(value.item)) {
|
|
50
|
+
return toImageGenerationOutput(value.item);
|
|
51
|
+
}
|
|
52
|
+
if (value.type === "response.image_generation_call.partial_image") {
|
|
53
|
+
return {
|
|
54
|
+
id: typeof value.item_id === "string" ? value.item_id : void 0,
|
|
55
|
+
type: "image_generation_call",
|
|
56
|
+
partial_image_b64: typeof value.partial_image_b64 === "string" ? value.partial_image_b64 : void 0,
|
|
57
|
+
background: typeof value.background === "string" ? value.background : void 0,
|
|
58
|
+
output_format: typeof value.output_format === "string" ? value.output_format : void 0,
|
|
59
|
+
size: typeof value.size === "string" ? value.size : void 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function normalizeReturnedSize(size, fallback) {
|
|
65
|
+
if (typeof size === "string" && SUPPORTED_IMAGE_SIZES.has(size)) {
|
|
66
|
+
return size;
|
|
67
|
+
}
|
|
68
|
+
if (typeof fallback === "string" && SUPPORTED_IMAGE_SIZES.has(fallback)) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
function normalizeReturnedQuality(quality) {
|
|
74
|
+
if (typeof quality === "string" && SUPPORTED_IMAGE_QUALITIES.has(quality)) {
|
|
75
|
+
return quality;
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
function normalizeReturnedFormat(format) {
|
|
80
|
+
if (typeof format === "string" && SUPPORTED_IMAGE_FORMATS.has(format)) {
|
|
81
|
+
return format;
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function normalizeReturnedBackground(background) {
|
|
86
|
+
if (typeof background === "string" && SUPPORTED_IMAGE_BACKGROUNDS.has(background)) {
|
|
87
|
+
return background;
|
|
88
|
+
}
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
function collectImageGenerationOutputs(raw) {
|
|
92
|
+
if (!isRecord(raw)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const finalItems = /* @__PURE__ */ new Map();
|
|
96
|
+
const partialItems = /* @__PURE__ */ new Map();
|
|
97
|
+
const response = isRecord(raw.response) ? raw.response : null;
|
|
98
|
+
const events = Array.isArray(raw.events) ? raw.events : [];
|
|
99
|
+
if (response && Array.isArray(response.output)) {
|
|
100
|
+
for (const output of response.output) {
|
|
101
|
+
const image = toImageGenerationOutput(output);
|
|
102
|
+
if (!image || !image.id) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (typeof image.result === "string" && image.result.length > 0) {
|
|
106
|
+
finalItems.set(image.id, image);
|
|
107
|
+
} else if (typeof image.partial_image_b64 === "string" && image.partial_image_b64.length > 0) {
|
|
108
|
+
partialItems.set(image.id, image);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const event of events) {
|
|
113
|
+
const image = toImageGenerationEventOutput(event);
|
|
114
|
+
if (!image || !image.id) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (typeof image.result === "string" && image.result.length > 0) {
|
|
118
|
+
finalItems.set(image.id, image);
|
|
119
|
+
} else if (typeof image.partial_image_b64 === "string" && image.partial_image_b64.length > 0) {
|
|
120
|
+
partialItems.set(image.id, image);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (finalItems.size > 0) {
|
|
124
|
+
return Array.from(finalItems.values());
|
|
125
|
+
}
|
|
126
|
+
return Array.from(partialItems.values()).map((item) => ({
|
|
127
|
+
...item,
|
|
128
|
+
result: item.partial_image_b64
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
function summarizeImageDebug(raw) {
|
|
132
|
+
if (!isRecord(raw)) {
|
|
133
|
+
return {
|
|
134
|
+
rawType: typeof raw
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const response = isRecord(raw.response) ? raw.response : null;
|
|
138
|
+
const events = Array.isArray(raw.events) ? raw.events : [];
|
|
139
|
+
const imageEvents = events.filter((event) => isRecord(event) && typeof event.type === "string" && event.type.includes("image_generation")).slice(0, 12).map((event) => {
|
|
140
|
+
const safeEvent = event;
|
|
141
|
+
return {
|
|
142
|
+
type: safeEvent.type,
|
|
143
|
+
item_id: typeof safeEvent.item_id === "string" ? safeEvent.item_id : void 0,
|
|
144
|
+
output_index: typeof safeEvent.output_index === "number" ? safeEvent.output_index : void 0,
|
|
145
|
+
partial_image_b64_length: typeof safeEvent.partial_image_b64 === "string" ? safeEvent.partial_image_b64.length : void 0
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
response_status: typeof response?.status === "string" ? response.status : void 0,
|
|
150
|
+
response_error: isRecord(response?.error) ? {
|
|
151
|
+
code: typeof response.error.code === "string" ? response.error.code : void 0,
|
|
152
|
+
message: typeof response.error.message === "string" ? response.error.message : void 0,
|
|
153
|
+
type: typeof response.error.type === "string" ? response.error.type : void 0
|
|
154
|
+
} : void 0,
|
|
155
|
+
response_output_length: Array.isArray(response?.output) ? response.output.length : 0,
|
|
156
|
+
event_count: events.length,
|
|
157
|
+
event_types: events.filter((event) => isRecord(event) && typeof event.type === "string").slice(0, 20).map((event) => event.type),
|
|
158
|
+
error_events: events.filter((event) => isRecord(event) && (event.type === "error" || event.type === "response.failed")).slice(0, 5).map((event) => {
|
|
159
|
+
const safeEvent = event;
|
|
160
|
+
const eventError = isRecord(safeEvent.error) ? safeEvent.error : null;
|
|
161
|
+
const eventResponse = isRecord(safeEvent.response) ? safeEvent.response : null;
|
|
162
|
+
const responseError = eventResponse && isRecord(eventResponse.error) ? eventResponse.error : null;
|
|
163
|
+
return {
|
|
164
|
+
type: safeEvent.type,
|
|
165
|
+
code: typeof eventError?.code === "string" ? eventError.code : typeof responseError?.code === "string" ? responseError.code : void 0,
|
|
166
|
+
message: typeof eventError?.message === "string" ? eventError.message : typeof responseError?.message === "string" ? responseError.message : void 0
|
|
167
|
+
};
|
|
168
|
+
}),
|
|
169
|
+
image_events: imageEvents
|
|
170
|
+
};
|
|
171
|
+
}
|
|
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) {
|
|
190
|
+
if (!isRecord(raw)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const response = isRecord(raw.response) ? raw.response : null;
|
|
194
|
+
if (response) {
|
|
195
|
+
const responseError = isRecord(response.error) ? response.error : null;
|
|
196
|
+
const responseStatus = typeof response.status === "string" ? response.status : void 0;
|
|
197
|
+
const details = createImageFailureDetails(responseError?.code, responseError?.message);
|
|
198
|
+
if (responseStatus === "failed" && details) {
|
|
199
|
+
return details;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const events = Array.isArray(raw.events) ? raw.events : [];
|
|
203
|
+
for (const event of events) {
|
|
204
|
+
if (!isRecord(event)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (event.type === "error") {
|
|
208
|
+
const eventError = isRecord(event.error) ? event.error : event;
|
|
209
|
+
const details = createImageFailureDetails(eventError.code, eventError.message);
|
|
210
|
+
if (details) {
|
|
211
|
+
return details;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (event.type === "response.failed" && isRecord(event.response)) {
|
|
215
|
+
const responseError = isRecord(event.response.error) ? event.response.error : null;
|
|
216
|
+
const details = createImageFailureDetails(responseError?.code, responseError?.message);
|
|
217
|
+
if (details) {
|
|
218
|
+
return details;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
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
|
+
}
|
|
232
|
+
function extractImageUsage(raw) {
|
|
233
|
+
if (!isRecord(raw) || !isRecord(raw.response)) {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
const toolUsage = isRecord(raw.response.tool_usage) ? raw.response.tool_usage : null;
|
|
237
|
+
const imageGen = toolUsage && isRecord(toolUsage.image_gen) ? toolUsage.image_gen : null;
|
|
238
|
+
if (!imageGen || typeof imageGen.input_tokens !== "number" || typeof imageGen.output_tokens !== "number" || typeof imageGen.total_tokens !== "number") {
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
input_tokens: imageGen.input_tokens,
|
|
243
|
+
input_tokens_details: isRecord(imageGen.input_tokens_details) ? {
|
|
244
|
+
image_tokens: Number(imageGen.input_tokens_details.image_tokens ?? 0),
|
|
245
|
+
text_tokens: Number(imageGen.input_tokens_details.text_tokens ?? 0)
|
|
246
|
+
} : void 0,
|
|
247
|
+
output_tokens: imageGen.output_tokens,
|
|
248
|
+
output_tokens_details: isRecord(imageGen.output_tokens_details) ? {
|
|
249
|
+
image_tokens: Number(imageGen.output_tokens_details.image_tokens ?? 0),
|
|
250
|
+
text_tokens: Number(imageGen.output_tokens_details.text_tokens ?? 0)
|
|
251
|
+
} : void 0,
|
|
252
|
+
total_tokens: imageGen.total_tokens
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
class ImageService {
|
|
256
|
+
constructor(deps) {
|
|
257
|
+
this.deps = deps;
|
|
258
|
+
}
|
|
259
|
+
resolveRequestedImageModel(model) {
|
|
260
|
+
if (!model) {
|
|
261
|
+
return "gpt-image-2";
|
|
262
|
+
}
|
|
263
|
+
if (!SUPPORTED_IMAGE_MODELS.has(model)) {
|
|
264
|
+
throw new Error(`\u5F53\u524D\u7F51\u5173\u4EC5\u652F\u6301\u8FD9\u4E9B\u751F\u56FE\u6A21\u578B: ${Array.from(SUPPORTED_IMAGE_MODELS).join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
return model;
|
|
267
|
+
}
|
|
268
|
+
isFreePlan(profile) {
|
|
269
|
+
return profile.quota?.planType === "free";
|
|
270
|
+
}
|
|
271
|
+
async generate(request) {
|
|
272
|
+
const profile = await this.deps.authService.requireUsableProfile("openai-codex");
|
|
273
|
+
if (this.isFreePlan(profile)) {
|
|
274
|
+
throw new Error("\u5F53\u524D\u8D26\u53F7\u4E3A free \u5957\u9910\uFF0C\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\u3002\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002");
|
|
275
|
+
}
|
|
276
|
+
const orchestratorModel = await this.deps.configService.getDefaultModel();
|
|
277
|
+
const requestedImageModel = this.resolveRequestedImageModel(request.model);
|
|
278
|
+
const requestSummary = {
|
|
279
|
+
requestedImageModel,
|
|
280
|
+
orchestratorModel,
|
|
281
|
+
promptLength: request.prompt.length,
|
|
282
|
+
promptPreview: truncateForLog(request.prompt),
|
|
283
|
+
size: request.size ?? "default",
|
|
284
|
+
quality: request.quality ?? "default",
|
|
285
|
+
background: request.background ?? "default",
|
|
286
|
+
outputFormat: request.outputFormat ?? "default",
|
|
287
|
+
outputCompression: typeof request.outputCompression === "number" ? request.outputCompression : void 0,
|
|
288
|
+
moderation: request.moderation ?? "default"
|
|
289
|
+
};
|
|
290
|
+
console.info("[gateway:image] upstream request", requestSummary);
|
|
291
|
+
const tool = {
|
|
292
|
+
type: "image_generation",
|
|
293
|
+
model: requestedImageModel
|
|
294
|
+
};
|
|
295
|
+
if (request.size) {
|
|
296
|
+
tool.size = request.size;
|
|
297
|
+
}
|
|
298
|
+
if (request.quality) {
|
|
299
|
+
tool.quality = request.quality;
|
|
300
|
+
}
|
|
301
|
+
if (request.background) {
|
|
302
|
+
tool.background = request.background;
|
|
303
|
+
}
|
|
304
|
+
if (request.outputFormat) {
|
|
305
|
+
tool.output_format = request.outputFormat;
|
|
306
|
+
}
|
|
307
|
+
if (typeof request.outputCompression === "number") {
|
|
308
|
+
tool.output_compression = request.outputCompression;
|
|
309
|
+
}
|
|
310
|
+
if (request.moderation) {
|
|
311
|
+
tool.moderation = request.moderation;
|
|
312
|
+
}
|
|
313
|
+
for (let attempt = 1; attempt <= IMAGE_GENERATION_MAX_ATTEMPTS; attempt += 1) {
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
result = await askOpenAICodex({
|
|
317
|
+
profile,
|
|
318
|
+
model: orchestratorModel,
|
|
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;
|
|
368
|
+
}
|
|
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", {
|
|
389
|
+
...requestSummary,
|
|
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",
|
|
396
|
+
debug: debugSummary
|
|
397
|
+
});
|
|
398
|
+
return imageResult;
|
|
399
|
+
}
|
|
400
|
+
throw createError("\u56FE\u7247\u751F\u6210\u5931\u8D25\uFF1A\u8D85\u8FC7\u6700\u5927\u91CD\u8BD5\u6B21\u6570\u3002", 503);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
export {
|
|
404
|
+
ImageService
|
|
405
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
getCodexModelCatalog,
|
|
4
|
+
hasCodexModel
|
|
5
5
|
} from "../models/openai-codex-models.js";
|
|
6
6
|
class ModelService {
|
|
7
7
|
constructor(configService) {
|
|
@@ -11,27 +11,55 @@ class ModelService {
|
|
|
11
11
|
if (provider !== "openai-codex") {
|
|
12
12
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
13
13
|
}
|
|
14
|
-
const defaultModel = await
|
|
15
|
-
|
|
14
|
+
const [{ models }, defaultModel] = await Promise.all([
|
|
15
|
+
getCodexModelCatalog(),
|
|
16
|
+
this.configService.getDefaultModel(provider)
|
|
17
|
+
]);
|
|
18
|
+
return models.map((model) => ({
|
|
16
19
|
...model,
|
|
17
20
|
isDefault: model.id === defaultModel
|
|
18
21
|
}));
|
|
19
22
|
}
|
|
23
|
+
async getCatalog(provider = "openai-codex") {
|
|
24
|
+
if (provider !== "openai-codex") {
|
|
25
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
26
|
+
}
|
|
27
|
+
return (await getCodexModelCatalog()).catalog;
|
|
28
|
+
}
|
|
29
|
+
async refreshModels(provider = "openai-codex") {
|
|
30
|
+
if (provider !== "openai-codex") {
|
|
31
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
32
|
+
}
|
|
33
|
+
const [{ models, catalog }, defaultModel] = await Promise.all([
|
|
34
|
+
getCodexModelCatalog(),
|
|
35
|
+
this.configService.getDefaultModel(provider)
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
models: models.map((model) => ({
|
|
39
|
+
...model,
|
|
40
|
+
isDefault: model.id === defaultModel
|
|
41
|
+
})),
|
|
42
|
+
catalog
|
|
43
|
+
};
|
|
44
|
+
}
|
|
20
45
|
async getDefaultModel(provider = "openai-codex") {
|
|
21
46
|
if (provider !== "openai-codex") {
|
|
22
47
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
23
48
|
}
|
|
24
49
|
return this.configService.getDefaultModel(provider);
|
|
25
50
|
}
|
|
26
|
-
async resolveModel(provider = "openai-codex", requested) {
|
|
51
|
+
async resolveModel(provider = "openai-codex", requested, options) {
|
|
27
52
|
if (provider !== "openai-codex") {
|
|
28
53
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
29
54
|
}
|
|
30
55
|
if (!requested) {
|
|
31
56
|
return this.configService.getDefaultModel(provider);
|
|
32
57
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
58
|
+
if (options?.allowUnknown) {
|
|
59
|
+
return requested;
|
|
60
|
+
}
|
|
61
|
+
if (!await hasCodexModel(requested)) {
|
|
62
|
+
throw new Error(`\u5F53\u524D\u7F51\u5173\u672A\u627E\u5230\u53EF\u7528\u6A21\u578B: ${requested}`);
|
|
35
63
|
}
|
|
36
64
|
return requested;
|
|
37
65
|
}
|
|
@@ -39,4 +67,3 @@ class ModelService {
|
|
|
39
67
|
export {
|
|
40
68
|
ModelService
|
|
41
69
|
};
|
|
42
|
-
//# sourceMappingURL=model-service.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { requestText } from "../providers/http-client.js";
|
|
6
|
+
const VERSION_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
7
|
+
const packageJsonPath = path.dirname(fileURLToPath(new URL("../../../package.json", import.meta.url)));
|
|
8
|
+
function compareVersionPart(left, right) {
|
|
9
|
+
const leftNumber = Number.parseInt(left, 10);
|
|
10
|
+
const rightNumber = Number.parseInt(right, 10);
|
|
11
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
12
|
+
return leftNumber - rightNumber;
|
|
13
|
+
}
|
|
14
|
+
return left.localeCompare(right);
|
|
15
|
+
}
|
|
16
|
+
function compareSemver(left, right) {
|
|
17
|
+
const leftParts = left.split(/[.+-]/);
|
|
18
|
+
const rightParts = right.split(/[.+-]/);
|
|
19
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
20
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
21
|
+
const diff = compareVersionPart(leftParts[index] ?? "0", rightParts[index] ?? "0");
|
|
22
|
+
if (diff !== 0) {
|
|
23
|
+
return diff;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
async function readPackageManifest() {
|
|
29
|
+
const raw = await fs.readFile(path.join(packageJsonPath, "package.json"), "utf8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return {
|
|
32
|
+
name: parsed.name ?? "ai-zero-token",
|
|
33
|
+
version: parsed.version ?? "0.0.0"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
class VersionService {
|
|
37
|
+
cache = null;
|
|
38
|
+
inFlight = null;
|
|
39
|
+
async getVersionStatus(options) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (!options?.force && this.cache && now - this.cache.checkedAt < VERSION_CACHE_TTL_MS) {
|
|
42
|
+
return this.cache;
|
|
43
|
+
}
|
|
44
|
+
if (this.inFlight) {
|
|
45
|
+
return this.inFlight;
|
|
46
|
+
}
|
|
47
|
+
this.inFlight = this.fetchVersionStatus().then((status) => {
|
|
48
|
+
this.cache = status;
|
|
49
|
+
return status;
|
|
50
|
+
}).finally(() => {
|
|
51
|
+
this.inFlight = null;
|
|
52
|
+
});
|
|
53
|
+
return this.inFlight;
|
|
54
|
+
}
|
|
55
|
+
async fetchVersionStatus() {
|
|
56
|
+
const manifest = await readPackageManifest();
|
|
57
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(manifest.name)}/latest`;
|
|
58
|
+
try {
|
|
59
|
+
const response = await requestText({
|
|
60
|
+
method: "GET",
|
|
61
|
+
url: registryUrl,
|
|
62
|
+
timeoutMs: 5e3
|
|
63
|
+
});
|
|
64
|
+
if (response.status < 200 || response.status >= 300) {
|
|
65
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
const parsed = JSON.parse(response.body);
|
|
68
|
+
const latestVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : void 0;
|
|
69
|
+
if (!latestVersion) {
|
|
70
|
+
throw new Error("npm registry did not return a version");
|
|
71
|
+
}
|
|
72
|
+
const needsUpdate = compareSemver(manifest.version, latestVersion) < 0;
|
|
73
|
+
return {
|
|
74
|
+
packageName: manifest.name,
|
|
75
|
+
currentVersion: manifest.version,
|
|
76
|
+
latestVersion,
|
|
77
|
+
checkedAt: Date.now(),
|
|
78
|
+
needsUpdate,
|
|
79
|
+
registryUrl,
|
|
80
|
+
status: needsUpdate ? "update-available" : "ok"
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
packageName: manifest.name,
|
|
85
|
+
currentVersion: manifest.version,
|
|
86
|
+
checkedAt: Date.now(),
|
|
87
|
+
needsUpdate: false,
|
|
88
|
+
registryUrl,
|
|
89
|
+
status: "error",
|
|
90
|
+
error: error instanceof Error ? error.message : String(error)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
VersionService
|
|
97
|
+
};
|
|
@@ -5,12 +5,41 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
const projectDir = path.dirname(fileURLToPath(new URL("../../../package.json", import.meta.url)));
|
|
6
6
|
const stateDir = path.join(projectDir, ".state");
|
|
7
7
|
const storePath = path.join(stateDir, "store.json");
|
|
8
|
+
const PROFILE_CLAIM_PATH = "https://api.openai.com/profile";
|
|
8
9
|
function createEmptyStore() {
|
|
9
10
|
return {
|
|
10
11
|
version: 1,
|
|
11
12
|
profiles: {}
|
|
12
13
|
};
|
|
13
14
|
}
|
|
15
|
+
function decodeJwtPayload(token) {
|
|
16
|
+
try {
|
|
17
|
+
const parts = token.split(".");
|
|
18
|
+
if (parts.length !== 3) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const payload = parts[1] ?? "";
|
|
22
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
23
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
24
|
+
const decoded = Buffer.from(normalized + padding, "base64").toString("utf8");
|
|
25
|
+
return JSON.parse(decoded);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function extractEmailFromAccessToken(token) {
|
|
31
|
+
const payload = decodeJwtPayload(token);
|
|
32
|
+
const profileClaim = payload?.[PROFILE_CLAIM_PATH];
|
|
33
|
+
const email = profileClaim?.email;
|
|
34
|
+
if (typeof email === "string" && email.trim()) {
|
|
35
|
+
return email.trim();
|
|
36
|
+
}
|
|
37
|
+
const topLevelEmail = payload?.email;
|
|
38
|
+
if (typeof topLevelEmail === "string" && topLevelEmail.trim()) {
|
|
39
|
+
return topLevelEmail.trim();
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
14
43
|
function getStateDir() {
|
|
15
44
|
return stateDir;
|
|
16
45
|
}
|
|
@@ -24,10 +53,14 @@ async function loadStore() {
|
|
|
24
53
|
const normalizedProfiles = Object.fromEntries(
|
|
25
54
|
Object.entries(parsed.profiles ?? {}).map(([profileId, profile]) => [
|
|
26
55
|
profileId,
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
(() => {
|
|
57
|
+
const recoveredEmail = typeof profile.email === "string" && profile.email.trim() ? profile.email.trim() : extractEmailFromAccessToken(profile.access);
|
|
58
|
+
return {
|
|
59
|
+
...profile,
|
|
60
|
+
mode: "oauth_account",
|
|
61
|
+
email: recoveredEmail
|
|
62
|
+
};
|
|
63
|
+
})()
|
|
31
64
|
])
|
|
32
65
|
);
|
|
33
66
|
return {
|
|
@@ -50,6 +83,31 @@ async function saveProfile(profile) {
|
|
|
50
83
|
store.activeProfileId = profile.profileId;
|
|
51
84
|
await saveStore(store);
|
|
52
85
|
}
|
|
86
|
+
async function updateProfile(profileId, updater) {
|
|
87
|
+
const store = await loadStore();
|
|
88
|
+
const profile = store.profiles[profileId];
|
|
89
|
+
if (!profile) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const updated = updater(profile);
|
|
93
|
+
store.profiles[profileId] = updated;
|
|
94
|
+
await saveStore(store);
|
|
95
|
+
return updated;
|
|
96
|
+
}
|
|
97
|
+
async function listProfiles() {
|
|
98
|
+
const store = await loadStore();
|
|
99
|
+
return Object.values(store.profiles);
|
|
100
|
+
}
|
|
101
|
+
async function setActiveProfile(profileId) {
|
|
102
|
+
const store = await loadStore();
|
|
103
|
+
const profile = store.profiles[profileId];
|
|
104
|
+
if (!profile) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
store.activeProfileId = profileId;
|
|
108
|
+
await saveStore(store);
|
|
109
|
+
return profile;
|
|
110
|
+
}
|
|
53
111
|
async function getActiveProfile() {
|
|
54
112
|
const store = await loadStore();
|
|
55
113
|
const activeId = store.activeProfileId?.trim();
|
|
@@ -59,6 +117,18 @@ async function getActiveProfile() {
|
|
|
59
117
|
const first = Object.values(store.profiles)[0];
|
|
60
118
|
return first ?? null;
|
|
61
119
|
}
|
|
120
|
+
async function removeProfile(profileId) {
|
|
121
|
+
const store = await loadStore();
|
|
122
|
+
if (!store.profiles[profileId]) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
delete store.profiles[profileId];
|
|
126
|
+
if (store.activeProfileId === profileId) {
|
|
127
|
+
store.activeProfileId = Object.keys(store.profiles)[0];
|
|
128
|
+
}
|
|
129
|
+
await saveStore(store);
|
|
130
|
+
return store.activeProfileId ? store.profiles[store.activeProfileId] ?? null : null;
|
|
131
|
+
}
|
|
62
132
|
async function clearStore() {
|
|
63
133
|
await fs.rm(stateDir, { recursive: true, force: true });
|
|
64
134
|
}
|
|
@@ -67,8 +137,11 @@ export {
|
|
|
67
137
|
getActiveProfile,
|
|
68
138
|
getStateDir,
|
|
69
139
|
getStorePath,
|
|
140
|
+
listProfiles,
|
|
70
141
|
loadStore,
|
|
142
|
+
removeProfile,
|
|
71
143
|
saveProfile,
|
|
72
|
-
saveStore
|
|
144
|
+
saveStore,
|
|
145
|
+
setActiveProfile,
|
|
146
|
+
updateProfile
|
|
73
147
|
};
|
|
74
|
-
//# sourceMappingURL=profile-store.js.map
|