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.
Files changed (70) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +235 -69
  3. package/dist/api.js +0 -1
  4. package/dist/cli/commands/ask.js +131 -5
  5. package/dist/cli/commands/clear.js +0 -1
  6. package/dist/cli/commands/help.js +17 -11
  7. package/dist/cli/commands/login.js +0 -1
  8. package/dist/cli/commands/models.js +14 -4
  9. package/dist/cli/commands/serve.js +41 -4
  10. package/dist/cli/commands/start.js +10 -0
  11. package/dist/cli/commands/status.js +1 -1
  12. package/dist/cli/index.js +5 -2
  13. package/dist/cli/shared.js +57 -6
  14. package/dist/cli.js +0 -1
  15. package/dist/core/context.js +10 -2
  16. package/dist/core/models/openai-codex-models.js +89 -1
  17. package/dist/core/providers/http-client.js +137 -14
  18. package/dist/core/providers/openai-codex/chat.js +217 -24
  19. package/dist/core/providers/openai-codex/oauth.js +15 -4
  20. package/dist/core/providers/openai-codex/pkce.js +0 -1
  21. package/dist/core/services/auth-service.js +125 -16
  22. package/dist/core/services/chat-service.js +24 -14
  23. package/dist/core/services/config-service.js +4 -5
  24. package/dist/core/services/image-service.js +405 -0
  25. package/dist/core/services/model-service.js +35 -8
  26. package/dist/core/services/version-service.js +97 -0
  27. package/dist/core/store/profile-store.js +79 -6
  28. package/dist/core/store/settings-store.js +1 -2
  29. package/dist/core/types.js +0 -1
  30. package/dist/http.js +0 -1
  31. package/dist/models.js +0 -1
  32. package/dist/oauth.js +0 -1
  33. package/dist/pkce.js +0 -1
  34. package/dist/server/admin-page.js +3165 -0
  35. package/dist/server/app.js +599 -40
  36. package/dist/server/index.js +0 -1
  37. package/dist/store.js +0 -1
  38. package/docs/API_USAGE.md +120 -0
  39. package/package.json +14 -3
  40. package/dist/api.js.map +0 -1
  41. package/dist/cli/commands/ask.js.map +0 -1
  42. package/dist/cli/commands/clear.js.map +0 -1
  43. package/dist/cli/commands/help.js.map +0 -1
  44. package/dist/cli/commands/login.js.map +0 -1
  45. package/dist/cli/commands/models.js.map +0 -1
  46. package/dist/cli/commands/serve.js.map +0 -1
  47. package/dist/cli/commands/status.js.map +0 -1
  48. package/dist/cli/index.js.map +0 -1
  49. package/dist/cli/shared.js.map +0 -1
  50. package/dist/cli.js.map +0 -1
  51. package/dist/core/context.js.map +0 -1
  52. package/dist/core/models/openai-codex-models.js.map +0 -1
  53. package/dist/core/providers/http-client.js.map +0 -1
  54. package/dist/core/providers/openai-codex/chat.js.map +0 -1
  55. package/dist/core/providers/openai-codex/oauth.js.map +0 -1
  56. package/dist/core/providers/openai-codex/pkce.js.map +0 -1
  57. package/dist/core/services/auth-service.js.map +0 -1
  58. package/dist/core/services/chat-service.js.map +0 -1
  59. package/dist/core/services/config-service.js.map +0 -1
  60. package/dist/core/services/model-service.js.map +0 -1
  61. package/dist/core/store/profile-store.js.map +0 -1
  62. package/dist/core/store/settings-store.js.map +0 -1
  63. package/dist/core/types.js.map +0 -1
  64. package/dist/http.js.map +0 -1
  65. package/dist/models.js.map +0 -1
  66. package/dist/oauth.js.map +0 -1
  67. package/dist/pkce.js.map +0 -1
  68. package/dist/server/app.js.map +0 -1
  69. package/dist/server/index.js.map +0 -1
  70. 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
- CODEX_MODEL_INFOS,
4
- isSupportedCodexModel
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 this.configService.getDefaultModel(provider);
15
- return CODEX_MODEL_INFOS.map((model) => ({
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 (!isSupportedCodexModel(requested)) {
34
- throw new Error(`\u5F53\u524D demo \u672A\u5185\u7F6E\u6A21\u578B: ${requested}`);
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
- ...profile,
29
- mode: "oauth_account"
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