ai-zero-token 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +20 -17
  3. package/README.zh-CN.md +20 -17
  4. package/admin-ui/dist/assets/StatCard-7TEzqn2i.js +1 -0
  5. package/admin-ui/dist/assets/accounts-D3tsDc3k.js +4 -0
  6. package/admin-ui/dist/assets/{docs-Dh0aFha_.js → docs-BO-aSEzh.js} +1 -1
  7. package/admin-ui/dist/assets/{image-bed-C1M7-0q1.js → image-bed-Dql7Vqd9.js} +1 -1
  8. package/admin-ui/dist/assets/index-C22_3Mxq.css +1 -0
  9. package/admin-ui/dist/assets/index-CCiBaGwU.js +10 -0
  10. package/admin-ui/dist/assets/{launch-pB7YlWFI.js → launch-DXLo-NIM.js} +1 -1
  11. package/admin-ui/dist/assets/{logs-B7McijSi.js → logs-Cwn8-rDu.js} +1 -1
  12. package/admin-ui/dist/assets/{network-detect-Bx3XmXPk.js → network-detect-vzWfL-Tz.js} +1 -1
  13. package/admin-ui/dist/assets/overview-B_yad8ge.js +1 -0
  14. package/admin-ui/dist/assets/{profiles-DMOjJORP.js → profiles-C5SmQvju.js} +1 -1
  15. package/admin-ui/dist/assets/settings-BdRWcKJb.js +5 -0
  16. package/admin-ui/dist/assets/{tester-BG-up8qP.js → tester-BKoMSoCz.js} +1 -1
  17. package/admin-ui/dist/assets/usage-B-qQxXzQ.js +1 -0
  18. package/admin-ui/dist/index.html +3 -3
  19. package/dist/cli/commands/help.js +1 -1
  20. package/dist/cli/commands/models.js +3 -2
  21. package/dist/core/context.js +4 -1
  22. package/dist/core/models/openai-codex-models.js +106 -1
  23. package/dist/core/providers/http-client.js +160 -11
  24. package/dist/core/providers/openai-codex/chat.js +2 -1
  25. package/dist/core/providers/openai-codex/chatgpt-web-image.js +1404 -0
  26. package/dist/core/services/auth-service.js +154 -10
  27. package/dist/core/services/chat-service.js +16 -18
  28. package/dist/core/services/config-service.js +9 -0
  29. package/dist/core/services/image-service.js +31 -1
  30. package/dist/core/services/model-service.js +22 -8
  31. package/dist/core/services/usage-service.js +349 -0
  32. package/dist/core/store/codex-auth-store.js +149 -15
  33. package/dist/core/store/settings-store.js +8 -2
  34. package/dist/core/store/state-paths.js +17 -1
  35. package/dist/server/app.js +1023 -69
  36. package/dist/server/index.js +1 -1
  37. package/docs/API_USAGE.md +34 -4
  38. package/docs/DESKTOP_RELEASE.md +12 -1
  39. package/package.json +1 -1
  40. package/admin-ui/dist/assets/accounts-ABMyXo4H.js +0 -4
  41. package/admin-ui/dist/assets/index--rNjdmzf.js +0 -10
  42. package/admin-ui/dist/assets/index-DjtN30PC.css +0 -1
  43. package/admin-ui/dist/assets/overview-CV0H2Nsq.js +0 -1
  44. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +0 -7
@@ -14,6 +14,7 @@ import {
14
14
  refreshOpenAICodexToken
15
15
  } from "../providers/openai-codex/oauth.js";
16
16
  import { askOpenAICodex } from "../providers/openai-codex/chat.js";
17
+ import { isTransientHttpError } from "../providers/http-client.js";
17
18
  import {
18
19
  applyGatewayToCodexProviderConfig,
19
20
  applyProfileToCodexAuth,
@@ -27,7 +28,7 @@ import {
27
28
  importProfileFromJson,
28
29
  importProfilesFromJson
29
30
  } from "../store/profile-transfer.js";
30
- const DEFAULT_QUOTA_SYNC_CONCURRENCY = 16;
31
+ const DEFAULT_QUOTA_SYNC_CONCURRENCY = 3;
31
32
  function getQuotaSyncConcurrency(configured) {
32
33
  const raw = process.env.AZT_QUOTA_SYNC_CONCURRENCY;
33
34
  const parsed = raw ? Number.parseInt(raw, 10) : configured ?? DEFAULT_QUOTA_SYNC_CONCURRENCY;
@@ -151,20 +152,99 @@ class AuthService {
151
152
  const percents = this.getQuotaPercents(profile);
152
153
  return percents.length > 0 && percents.some((value) => value >= 100);
153
154
  }
154
- hasKnownAvailableQuota(profile) {
155
+ isQuotaSnapshotExhausted(quota) {
156
+ if (!quota) {
157
+ return false;
158
+ }
159
+ return [quota.primaryUsedPercent, quota.secondaryUsedPercent].some(
160
+ (value) => typeof value === "number" && Number.isFinite(value) && value >= 100
161
+ );
162
+ }
163
+ isQuotaBlocked(profile) {
164
+ return this.isQuotaExhausted(profile) && !this.hasResetElapsedForExhaustedQuota(profile);
165
+ }
166
+ timestampToMillis(value) {
167
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
168
+ return void 0;
169
+ }
170
+ return value < 1e12 ? value * 1e3 : value;
171
+ }
172
+ getQuotaResetAt(profile, slot) {
173
+ const quota = profile.quota;
174
+ if (!quota) {
175
+ return void 0;
176
+ }
177
+ const direct = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
178
+ const directMillis = this.timestampToMillis(direct);
179
+ if (directMillis) {
180
+ return directMillis;
181
+ }
182
+ const after = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
183
+ if (typeof after === "number" && Number.isFinite(after) && after > 0) {
184
+ const capturedAt = this.timestampToMillis(quota.capturedAt) ?? Date.now();
185
+ return capturedAt + after * 1e3;
186
+ }
187
+ return void 0;
188
+ }
189
+ hasResetElapsedForExhaustedQuota(profile) {
190
+ const quota = profile.quota;
191
+ if (!quota) {
192
+ return false;
193
+ }
194
+ const exhaustedSlots = [];
195
+ if (typeof quota.primaryUsedPercent === "number" && quota.primaryUsedPercent >= 100) {
196
+ exhaustedSlots.push("primary");
197
+ }
198
+ if (typeof quota.secondaryUsedPercent === "number" && quota.secondaryUsedPercent >= 100) {
199
+ exhaustedSlots.push("secondary");
200
+ }
201
+ if (exhaustedSlots.length === 0) {
202
+ return false;
203
+ }
204
+ const now = Date.now();
205
+ return exhaustedSlots.every((slot) => {
206
+ const resetAt = this.getQuotaResetAt(profile, slot);
207
+ return typeof resetAt === "number" && resetAt <= now;
208
+ });
209
+ }
210
+ canEnterAutoSwitchPool(profile) {
155
211
  if (profile.authStatus?.state === "token_invalidated" || profile.authStatus?.state === "auth_error") {
156
212
  return false;
157
213
  }
158
214
  const percents = this.getQuotaPercents(profile);
159
- return percents.length > 0 && percents.every((value) => value < 100);
215
+ if (percents.length === 0) {
216
+ return true;
217
+ }
218
+ return percents.every((value) => value < 100) || this.hasResetElapsedForExhaustedQuota(profile);
160
219
  }
161
220
  hasInvalidAuthStatus(profile) {
162
221
  return profile.authStatus?.state === "token_invalidated" || profile.authStatus?.state === "auth_error";
163
222
  }
223
+ shouldLeaveActiveProfile(profile) {
224
+ return this.hasInvalidAuthStatus(profile) || this.isQuotaBlocked(profile);
225
+ }
226
+ isRotationTrigger(error, quota) {
227
+ if (this.isQuotaSnapshotExhausted(quota)) {
228
+ return true;
229
+ }
230
+ if (isTransientHttpError(error)) {
231
+ return true;
232
+ }
233
+ const normalized = error instanceof Error ? error : new Error(String(error));
234
+ const details = normalized;
235
+ const status = typeof details.upstreamStatus === "number" ? details.upstreamStatus : void 0;
236
+ const marker = [
237
+ normalized.message,
238
+ details.upstreamErrorCode,
239
+ details.upstreamErrorType,
240
+ details.upstreamErrorMessage
241
+ ].filter((value) => typeof value === "string").join(" ").toLowerCase();
242
+ return status === 429 || status === 401 || status === 403 || marker.includes("usage_limit_reached") || marker.includes("token_invalidated") || marker.includes("authentication token has been invalidated") || marker.includes("\u5237\u65B0 token \u5931\u8D25");
243
+ }
164
244
  getQuotaUsageScore(profile) {
165
245
  const percents = this.getQuotaPercents(profile);
166
246
  if (percents.length === 0) {
167
- return 100;
247
+ return 50;
168
248
  }
169
249
  return Math.max(...percents);
170
250
  }
@@ -175,12 +255,12 @@ class AuthService {
175
255
  }
176
256
  return updater(profile);
177
257
  });
178
- if (!options?.checkAutoSwitch || options.skipAutoSwitch || !updated || updated.provider !== provider || !this.isQuotaExhausted(updated)) {
258
+ if (!options?.checkAutoSwitch || options.skipAutoSwitch || !updated || updated.provider !== provider || !this.shouldLeaveActiveProfile(updated)) {
179
259
  return updated;
180
260
  }
181
261
  const activeProfile = await this.getActiveProfile(provider);
182
262
  if (activeProfile?.profileId === updated.profileId) {
183
- await this.maybeAutoSwitchProfile(updated, provider);
263
+ return this.maybeAutoSwitchProfile(updated, provider);
184
264
  }
185
265
  return updated;
186
266
  }
@@ -212,7 +292,7 @@ class AuthService {
212
292
  async maybeAutoSwitchProfile(profile, provider) {
213
293
  const settings = await this.configService.getSettings();
214
294
  const excludedProfileIds = new Set(settings.autoSwitch.excludedProfileIds);
215
- if (!settings.autoSwitch.enabled || excludedProfileIds.has(profile.profileId) || !this.isQuotaExhausted(profile)) {
295
+ if (!settings.autoSwitch.enabled || excludedProfileIds.has(profile.profileId) || !this.shouldLeaveActiveProfile(profile)) {
216
296
  return profile;
217
297
  }
218
298
  const [profiles, codexStatus] = await Promise.all([
@@ -225,7 +305,7 @@ class AuthService {
225
305
  profile: item,
226
306
  index,
227
307
  distance: currentIndex >= 0 ? (index - currentIndex + profiles.length) % profiles.length : index + 1
228
- })).filter((item) => item.profile.provider === provider && item.profile.profileId !== profile.profileId).filter((item) => !excludedProfileIds.has(item.profile.profileId)).filter((item) => this.hasKnownAvailableQuota(item.profile)).sort((left, right) => {
308
+ })).filter((item) => item.profile.provider === provider && item.profile.profileId !== profile.profileId).filter((item) => !excludedProfileIds.has(item.profile.profileId)).filter((item) => this.canEnterAutoSwitchPool(item.profile)).sort((left, right) => {
229
309
  const leftCodexConflict = codexAccountId && left.profile.accountId === codexAccountId ? 1 : 0;
230
310
  const rightCodexConflict = codexAccountId && right.profile.accountId === codexAccountId ? 1 : 0;
231
311
  const codexDiff = leftCodexConflict - rightCodexConflict;
@@ -255,8 +335,9 @@ class AuthService {
255
335
  if (!activated) {
256
336
  return profile;
257
337
  }
258
- console.info("[auth] auto switched active profile after quota exhaustion", {
338
+ console.info("[auth] auto switched active profile", {
259
339
  provider,
340
+ reason: this.hasInvalidAuthStatus(profile) ? "auth_error" : "quota_exhausted",
260
341
  fromProfileId: profile.profileId,
261
342
  toProfileId: activated.profileId,
262
343
  avoidedCodexAccount: Boolean(codexAccountId && activated.accountId !== codexAccountId)
@@ -401,6 +482,69 @@ class AuthService {
401
482
  }
402
483
  return this.refreshStoredProfile(profile, provider);
403
484
  }
485
+ async requireUsableProfileById(profileId, provider = "openai-codex") {
486
+ const profiles = await listProfiles();
487
+ const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
488
+ if (!profile) {
489
+ throw new Error(`\u6CA1\u6709\u627E\u5230\u8D26\u53F7: ${profileId}`);
490
+ }
491
+ if (Date.now() < profile.expires) {
492
+ return this.toManagedProfile(profile);
493
+ }
494
+ return this.refreshStoredProfile(profile, provider);
495
+ }
496
+ async withProfileRotation(provider, runner, options) {
497
+ const maxAttempts = Math.max(1, Math.min(8, options?.maxAttempts ?? 2));
498
+ const attemptedProfileIds = /* @__PURE__ */ new Set();
499
+ let lastError;
500
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
501
+ let profile;
502
+ try {
503
+ profile = await this.requireUsableProfile(provider, {
504
+ skipAutoSwitch: options?.skipAutoSwitch
505
+ });
506
+ } catch (error) {
507
+ lastError = error;
508
+ if (options?.skipAutoSwitch || attempt >= maxAttempts - 1 || !this.isRotationTrigger(error)) {
509
+ throw error;
510
+ }
511
+ const activeProfile = await this.getActiveProfile(provider);
512
+ if (!activeProfile) {
513
+ throw error;
514
+ }
515
+ const switchedProfile = await this.recordProfileRequestFailure(activeProfile.profileId, error, void 0, provider);
516
+ if (!switchedProfile || switchedProfile.profileId === activeProfile.profileId) {
517
+ throw error;
518
+ }
519
+ continue;
520
+ }
521
+ if (attemptedProfileIds.has(profile.profileId)) {
522
+ break;
523
+ }
524
+ attemptedProfileIds.add(profile.profileId);
525
+ try {
526
+ const result = await runner(profile, attempt);
527
+ await this.recordProfileRequestSuccess(profile.profileId, result.quota, provider, {
528
+ skipAutoSwitch: options?.skipAutoSwitch
529
+ });
530
+ return {
531
+ profile,
532
+ result,
533
+ retryCount: attempt
534
+ };
535
+ } catch (error) {
536
+ lastError = error;
537
+ const quota = error.quota;
538
+ const switchedProfile = await this.recordProfileRequestFailure(profile.profileId, error, quota, provider, {
539
+ skipAutoSwitch: options?.skipAutoSwitch
540
+ });
541
+ if (options?.skipAutoSwitch || attempt >= maxAttempts - 1 || !this.isRotationTrigger(error, quota) || !switchedProfile || switchedProfile.profileId === profile.profileId || attemptedProfileIds.has(switchedProfile.profileId)) {
542
+ throw error;
543
+ }
544
+ }
545
+ }
546
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "\u6CA1\u6709\u53EF\u7528\u8D26\u53F7\u53EF\u8F6E\u6362\u3002"));
547
+ }
404
548
  async requireFreshProfileWithIdToken(profileId, provider = "openai-codex") {
405
549
  const profiles = await listProfiles();
406
550
  const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
@@ -538,7 +682,7 @@ class AuthService {
538
682
  }),
539
683
  {
540
684
  skipAutoSwitch: options?.skipAutoSwitch,
541
- checkAutoSwitch: Boolean(quota)
685
+ checkAutoSwitch: Boolean(quota || authStatus)
542
686
  }
543
687
  );
544
688
  }
@@ -9,29 +9,27 @@ class ChatService {
9
9
  const model = await this.deps.modelService.resolveModel(provider, request.model, {
10
10
  allowUnknown: request.experimental?.allowUnknownModel
11
11
  });
12
- const profile = await this.deps.authService.requireUsableProfile(provider);
13
- try {
14
- const result = await askOpenAICodex({
12
+ const rotation = await this.deps.authService.withProfileRotation(
13
+ provider,
14
+ (profile) => askOpenAICodex({
15
15
  profile,
16
16
  prompt: request.input,
17
17
  model,
18
18
  system: request.system,
19
19
  bodyOverride: request.experimental?.codexBody
20
- });
21
- await this.deps.authService.recordProfileRequestSuccess(profile.profileId, result.quota, provider);
22
- return {
23
- provider,
24
- model,
25
- text: result.text,
26
- toolCalls: result.toolCalls,
27
- raw: result.raw,
28
- artifacts: result.artifacts
29
- };
30
- } catch (error) {
31
- const quota = error.quota;
32
- await this.deps.authService.recordProfileRequestFailure(profile.profileId, error, quota, provider);
33
- throw error;
34
- }
20
+ })
21
+ );
22
+ const result = rotation.result;
23
+ return {
24
+ provider,
25
+ model,
26
+ profile: rotation.profile,
27
+ retryCount: rotation.retryCount,
28
+ text: result.text,
29
+ toolCalls: result.toolCalls,
30
+ raw: result.raw,
31
+ artifacts: result.artifacts
32
+ };
35
33
  }
36
34
  }
37
35
  export {
@@ -161,6 +161,15 @@ class ConfigService {
161
161
  }
162
162
  };
163
163
  }
164
+ if (params.image) {
165
+ next = {
166
+ ...next,
167
+ image: {
168
+ ...next.image,
169
+ freeAccountWebGenerationEnabled: params.image.freeAccountWebGenerationEnabled ?? next.image.freeAccountWebGenerationEnabled
170
+ }
171
+ };
172
+ }
164
173
  if (params.server) {
165
174
  next = {
166
175
  ...next,
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { askOpenAICodex } from "../providers/openai-codex/chat.js";
3
+ import { generateChatGPTWebImage } from "../providers/openai-codex/chatgpt-web-image.js";
3
4
  const SUPPORTED_IMAGE_MODELS = /* @__PURE__ */ new Set([
4
5
  "gpt-image-1",
5
6
  "gpt-image-1-mini",
@@ -248,6 +249,9 @@ function extractImageUsage(raw) {
248
249
  total_tokens: imageGen.total_tokens
249
250
  };
250
251
  }
252
+ function isFreePlan(profile) {
253
+ return profile.quota?.planType?.toLowerCase() === "free";
254
+ }
251
255
  class ImageService {
252
256
  constructor(deps) {
253
257
  this.deps = deps;
@@ -262,9 +266,12 @@ class ImageService {
262
266
  return model;
263
267
  }
264
268
  async generate(request) {
265
- const profile = await this.deps.authService.requireUsableProfile("openai-codex");
269
+ const profile = await this.deps.authService.requireUsableProfile("openai-codex", {
270
+ skipAutoSwitch: true
271
+ });
266
272
  const orchestratorModel = IMAGE_ORCHESTRATOR_MODEL;
267
273
  const requestedImageModel = this.resolveRequestedImageModel(request.model);
274
+ const settings = await this.deps.configService.getSettings();
268
275
  const requestSummary = {
269
276
  requestedImageModel,
270
277
  orchestratorModel,
@@ -279,6 +286,29 @@ class ImageService {
279
286
  inputImageCount: request.inputImages?.length ?? 0
280
287
  };
281
288
  console.info("[gateway:image] upstream request", requestSummary);
289
+ if (isFreePlan(profile) && settings.image.freeAccountWebGenerationEnabled) {
290
+ try {
291
+ console.info("[gateway:image] using ChatGPT web image route for Free profile", requestSummary);
292
+ const response = await generateChatGPTWebImage({
293
+ profile,
294
+ prompt: request.prompt,
295
+ model: requestedImageModel,
296
+ inputImages: request.inputImages,
297
+ size: request.size,
298
+ responseFormat: "b64_json"
299
+ });
300
+ await this.deps.authService.recordProfileRequestSuccess(profile.profileId, void 0, "openai-codex");
301
+ console.info("[gateway:image] ChatGPT web image response", {
302
+ ...requestSummary,
303
+ imageCount: response.data.length,
304
+ firstImageBase64Length: response.data[0]?.b64_json.length ?? 0
305
+ });
306
+ return response;
307
+ } catch (error) {
308
+ await this.deps.authService.recordProfileRequestFailure(profile.profileId, error, void 0, "openai-codex");
309
+ throw error;
310
+ }
311
+ }
282
312
  const tool = {
283
313
  type: "image_generation",
284
314
  model: requestedImageModel
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getCodexModelCatalog,
4
- hasCodexModel
4
+ hasCodexModel,
5
+ refreshCodexModelCatalogFromNetwork
5
6
  } from "../models/openai-codex-models.js";
6
7
  class ModelService {
7
- constructor(configService) {
8
+ constructor(configService, authService) {
8
9
  this.configService = configService;
10
+ this.authService = authService;
9
11
  }
10
12
  async listModels(provider = "openai-codex") {
11
13
  if (provider !== "openai-codex") {
@@ -30,16 +32,28 @@ class ModelService {
30
32
  if (provider !== "openai-codex") {
31
33
  throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
32
34
  }
33
- const [{ models, catalog }, defaultModel] = await Promise.all([
34
- getCodexModelCatalog(),
35
- this.configService.getDefaultModel(provider)
36
- ]);
35
+ const profile = await this.authService.requireUsableProfile(provider, {
36
+ skipAutoSwitch: true
37
+ });
38
+ let result;
39
+ try {
40
+ result = await refreshCodexModelCatalogFromNetwork(profile);
41
+ await this.authService.recordProfileRequestSuccess(profile.profileId, void 0, provider, {
42
+ skipAutoSwitch: true
43
+ });
44
+ } catch (error) {
45
+ await this.authService.recordProfileRequestFailure(profile.profileId, error, void 0, provider, {
46
+ skipAutoSwitch: true
47
+ });
48
+ throw error;
49
+ }
50
+ const defaultModel = await this.configService.getDefaultModel(provider);
37
51
  return {
38
- models: models.map((model) => ({
52
+ models: result.models.map((model) => ({
39
53
  ...model,
40
54
  isDefault: model.id === defaultModel
41
55
  })),
42
- catalog
56
+ catalog: result.catalog
43
57
  };
44
58
  }
45
59
  async getDefaultModel(provider = "openai-codex") {