ai-zero-token 2.0.6 → 2.0.8

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 (37) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +5 -5
  3. package/README.zh-CN.md +5 -5
  4. package/admin-ui/dist/assets/accounts-p9bqmijS.js +4 -0
  5. package/admin-ui/dist/assets/{docs--eK_2fzC.js → docs-BQaF_ZMr.js} +1 -1
  6. package/admin-ui/dist/assets/{image-bed-7wBZ1GhS.js → image-bed-D4w1m7k6.js} +1 -1
  7. package/admin-ui/dist/assets/index-BRQrU_AA.css +1 -0
  8. package/admin-ui/dist/assets/{index-CdFYy5j6.js → index-_5Ny0cZf.js} +3 -3
  9. package/admin-ui/dist/assets/{launch-BiD1Khtg.js → launch-BEDxgkQf.js} +1 -1
  10. package/admin-ui/dist/assets/{logs-BdoKDqh2.js → logs-BcL0n0Ld.js} +1 -1
  11. package/admin-ui/dist/assets/{network-detect-BvKns5nQ.js → network-detect-lEfklmIy.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-wm6M45fu.js → overview-DsUMffIU.js} +1 -1
  13. package/admin-ui/dist/assets/{profiles-DMOjJORP.js → profiles-C5SmQvju.js} +1 -1
  14. package/admin-ui/dist/assets/settings-a3HxExcC.js +8 -0
  15. package/admin-ui/dist/assets/{tester-NrARmlis.js → tester-Ca4JOgAq.js} +1 -1
  16. package/admin-ui/dist/assets/usage-hMH0gMZ5.js +1 -0
  17. package/admin-ui/dist/index.html +3 -3
  18. package/dist/cli/commands/help.js +1 -1
  19. package/dist/cli/commands/models.js +3 -2
  20. package/dist/core/context.js +1 -1
  21. package/dist/core/models/openai-codex-models.js +106 -1
  22. package/dist/core/providers/http-client.js +142 -12
  23. package/dist/core/providers/openai-codex/chat.js +139 -8
  24. package/dist/core/services/auth-service.js +104 -7
  25. package/dist/core/services/chat-service.js +16 -18
  26. package/dist/core/services/model-service.js +22 -8
  27. package/dist/core/services/usage-service.js +402 -31
  28. package/dist/core/store/codex-auth-store.js +82 -7
  29. package/dist/server/app.js +410 -34
  30. package/dist/server/index.js +1 -1
  31. package/docs/API_USAGE.md +1 -1
  32. package/docs/DESKTOP_RELEASE.md +12 -1
  33. package/package.json +1 -1
  34. package/admin-ui/dist/assets/accounts-bCDKXGg9.js +0 -4
  35. package/admin-ui/dist/assets/index-C22_3Mxq.css +0 -1
  36. package/admin-ui/dist/assets/settings-DOOu7Kd8.js +0 -5
  37. package/admin-ui/dist/assets/usage-CdWRVMDV.js +0 -1
@@ -56,6 +56,60 @@ function logHttpTiming(params) {
56
56
  totalMs: params.timing.totalMs
57
57
  });
58
58
  }
59
+ function createHttpTransportError(message, params) {
60
+ const error = new Error(message);
61
+ error.code = params.code;
62
+ error.elapsedMs = roundMs(params.elapsedMs);
63
+ error.isTransient = params.transient ?? true;
64
+ error.method = params.method;
65
+ error.requestId = params.requestId;
66
+ error.statusCode = params.statusCode;
67
+ error.transport = params.transport;
68
+ error.upstreamConnectionError = params.upstreamConnectionError ?? true;
69
+ error.url = params.url;
70
+ return error;
71
+ }
72
+ function classifyCurlRequestError(stderr, exitCode) {
73
+ const normalized = stderr.toLowerCase();
74
+ if (exitCode === 35 || normalized.includes("ssl_connect") || normalized.includes("ssl_error_syscall")) {
75
+ return {
76
+ code: "curl_request_tls_failed",
77
+ statusCode: 502
78
+ };
79
+ }
80
+ if (exitCode === 6 || normalized.includes("could not resolve host") || normalized.includes("name lookup timed out")) {
81
+ return {
82
+ code: "curl_request_dns_failed",
83
+ statusCode: 502
84
+ };
85
+ }
86
+ if (exitCode === 7 || normalized.includes("failed to connect") || normalized.includes("connection refused")) {
87
+ return {
88
+ code: "curl_request_connect_failed",
89
+ statusCode: 502
90
+ };
91
+ }
92
+ if (exitCode === 28 || normalized.includes("operation timed out") || normalized.includes("timed out")) {
93
+ return {
94
+ code: "curl_request_timeout",
95
+ statusCode: 504
96
+ };
97
+ }
98
+ return {
99
+ code: "curl_request_failed",
100
+ statusCode: 502
101
+ };
102
+ }
103
+ function isTransientHttpError(error) {
104
+ if (!error || typeof error !== "object") {
105
+ return false;
106
+ }
107
+ const details = error;
108
+ if (details.isTransient === true || details.upstreamConnectionError === true) {
109
+ return true;
110
+ }
111
+ return typeof details.code === "string" && (details.code === "curl_stream_closed_before_headers" || details.code === "curl_stream_header_timeout" || details.code === "curl_stream_body_failed" || details.code === "curl_request_tls_failed" || details.code === "curl_request_dns_failed" || details.code === "curl_request_connect_failed" || details.code === "curl_request_timeout" || details.code === "curl_request_failed");
112
+ }
59
113
  function isElectronRuntime() {
60
114
  return typeof process.versions.electron === "string";
61
115
  }
@@ -83,12 +137,7 @@ function normalizeCurlHeaders(value) {
83
137
  })
84
138
  );
85
139
  }
86
- function parseCurlHeaderDump(raw) {
87
- const blocks = raw.replace(/\r\n/g, "\n").split(/\n\n+/).map((block2) => block2.trim()).filter((block2) => /^HTTP\//i.test(block2));
88
- const block = blocks[blocks.length - 1];
89
- if (!block) {
90
- return null;
91
- }
140
+ function parseCurlHeaderBlock(block) {
92
141
  const lines = block.split("\n");
93
142
  const statusMatch = /^HTTP(?:\/\S+)?\s+(\d{3})/i.exec(lines[0]?.trim() ?? "");
94
143
  const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : NaN;
@@ -110,6 +159,33 @@ function parseCurlHeaderDump(raw) {
110
159
  }
111
160
  return { status, headers };
112
161
  }
162
+ function parseCurlHeaderDump(raw) {
163
+ const normalized = raw.replace(/\r\n/g, "\n");
164
+ const blocks = [];
165
+ let blockStart = 0;
166
+ while (blockStart < normalized.length) {
167
+ const separatorIndex = normalized.indexOf("\n\n", blockStart);
168
+ if (separatorIndex === -1) {
169
+ break;
170
+ }
171
+ const block = normalized.slice(blockStart, separatorIndex).trim();
172
+ if (/^HTTP\//i.test(block)) {
173
+ blocks.push(block);
174
+ }
175
+ blockStart = separatorIndex + 2;
176
+ while (normalized[blockStart] === "\n") {
177
+ blockStart += 1;
178
+ }
179
+ }
180
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
181
+ const parsed = parseCurlHeaderBlock(blocks[index]);
182
+ if (!parsed || parsed.status >= 100 && parsed.status < 200) {
183
+ continue;
184
+ }
185
+ return parsed;
186
+ }
187
+ return null;
188
+ }
113
189
  async function runCurlRequest(init, params) {
114
190
  const requestId = params?.requestId ?? nextRequestId();
115
191
  const startedAt = performance.now();
@@ -166,7 +242,20 @@ async function runCurlRequest(init, params) {
166
242
  });
167
243
  phases.waitForCurlMs = performance.now() - startedAt - phases.spawnCurlMs;
168
244
  if (exitCode !== 0) {
169
- throw new Error(stderr.trim() || `curl \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}`);
245
+ const stderrText = stderr.trim();
246
+ const classification = classifyCurlRequestError(stderrText, exitCode);
247
+ throw createHttpTransportError(
248
+ stderrText || `curl \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}\uFF08requestId=${requestId}\uFF09\u3002`,
249
+ {
250
+ code: classification.code,
251
+ elapsedMs: performance.now() - startedAt,
252
+ method: init.method,
253
+ requestId,
254
+ statusCode: classification.statusCode,
255
+ transport: "curl",
256
+ url: init.url
257
+ }
258
+ );
170
259
  }
171
260
  const parseStartedAt = performance.now();
172
261
  const statusMarkerIndex = stdout.lastIndexOf(CURL_STATUS_MARKER);
@@ -224,18 +313,44 @@ async function waitForCurlHeaders(params) {
224
313
  const raw = await fs.readFile(params.headerPath, "utf8");
225
314
  const parsed = parseCurlHeaderDump(raw);
226
315
  if (parsed) {
316
+ if (parsed.status >= 300 && parsed.status < 400 && !params.isClosed()) {
317
+ await new Promise((resolve) => setTimeout(resolve, 25));
318
+ continue;
319
+ }
227
320
  return parsed;
228
321
  }
229
322
  } catch {
230
323
  }
231
324
  if (params.isClosed()) {
232
- throw new Error(params.stderr().trim() || "curl stream \u8BF7\u6C42\u5728\u8FD4\u56DE\u54CD\u5E94\u5934\u524D\u7ED3\u675F\u3002");
325
+ const stderr2 = params.stderr().trim();
326
+ const exitCode = params.exitCode();
327
+ throw createHttpTransportError(
328
+ stderr2 || `curl stream \u8BF7\u6C42\u5728\u8FD4\u56DE\u54CD\u5E94\u5934\u524D\u7ED3\u675F\uFF08requestId=${params.requestId}${exitCode ? `, exitCode=${exitCode}` : ""}\uFF09\u3002`,
329
+ {
330
+ code: "curl_stream_closed_before_headers",
331
+ elapsedMs: performance.now() - startedAt,
332
+ method: params.method,
333
+ requestId: params.requestId,
334
+ statusCode: 502,
335
+ transport: "curl",
336
+ url: params.url
337
+ }
338
+ );
233
339
  }
234
340
  await new Promise((resolve) => setTimeout(resolve, 25));
235
341
  }
236
342
  const stderr = params.stderr().trim();
237
- throw new Error(
238
- `\u7B49\u5F85 curl stream \u54CD\u5E94\u5934\u8D85\u65F6\uFF08${Math.round(params.timeoutMs / 1e3)} \u79D2\uFF0CrequestId=${params.requestId}\uFF09\u3002${stderr ? ` ${stderr}` : ""}`
343
+ throw createHttpTransportError(
344
+ `\u7B49\u5F85 curl stream \u54CD\u5E94\u5934\u8D85\u65F6\uFF08${Math.round(params.timeoutMs / 1e3)} \u79D2\uFF0CrequestId=${params.requestId}\uFF09\u3002${stderr ? ` ${stderr}` : ""}`,
345
+ {
346
+ code: "curl_stream_header_timeout",
347
+ elapsedMs: performance.now() - startedAt,
348
+ method: params.method,
349
+ requestId: params.requestId,
350
+ statusCode: 504,
351
+ transport: "curl",
352
+ url: params.url
353
+ }
239
354
  );
240
355
  }
241
356
  async function runCurlStream(init, params) {
@@ -310,17 +425,31 @@ async function runCurlStream(init, params) {
310
425
  init.signal?.removeEventListener("abort", abort);
311
426
  void fs.unlink(headerPath).catch(() => void 0);
312
427
  if (exitCode !== 0 && !init.signal?.aborted) {
313
- body.destroy(new Error(stderr.trim() || `curl stream \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}`));
428
+ body.destroy(createHttpTransportError(
429
+ stderr.trim() || `curl stream \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}\uFF08requestId=${requestId}\uFF09\u3002`,
430
+ {
431
+ code: "curl_stream_body_failed",
432
+ elapsedMs: performance.now() - startedAt,
433
+ method: init.method,
434
+ requestId,
435
+ statusCode: 502,
436
+ transport: "curl",
437
+ url: init.url
438
+ }
439
+ ));
314
440
  }
315
441
  });
316
442
  let parsed;
317
443
  try {
318
444
  parsed = await waitForCurlHeaders({
319
445
  headerPath,
446
+ method: init.method,
320
447
  isClosed: () => closed,
448
+ exitCode: () => exitCode,
321
449
  stderr: () => stderr,
322
450
  requestId,
323
- timeoutMs: getCurlStreamHeaderTimeoutMs(params?.timeoutMs)
451
+ timeoutMs: getCurlStreamHeaderTimeoutMs(params?.timeoutMs),
452
+ url: init.url
324
453
  });
325
454
  } catch (error) {
326
455
  child.kill("SIGTERM");
@@ -480,6 +609,7 @@ async function requestStream(init) {
480
609
  });
481
610
  }
482
611
  export {
612
+ isTransientHttpError,
483
613
  requestStream,
484
614
  requestText
485
615
  };
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
2
3
  import { DEFAULT_CODEX_MODEL } from "../../models/openai-codex-models.js";
3
4
  import { requestStream, requestText } from "../http-client.js";
4
5
  const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
5
6
  const CODEX_RESPONSES_COMPACT_URL = `${CODEX_RESPONSES_URL}/compact`;
7
+ const COMPAT_PROMPT_CACHE_KEY_PREFIX = "compat_cc_";
8
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
9
  const URL_KEY_RE = /(url|uri|href|download|preview|thumbnail|image|asset|file)/i;
7
10
  const REFERENCE_KEY_RE = /(image|asset|file|media|blob|artifact|download|preview|thumbnail)/i;
8
11
  const REFERENCE_VALUE_RE = /^(file|asset|image|img|media|blob)-[\w-]+$/i;
@@ -60,8 +63,125 @@ function parseUpstreamErrorBody(body) {
60
63
  return void 0;
61
64
  }
62
65
  }
63
- function buildCodexRequestHeaders(profile) {
64
- return {
66
+ function hashShort(value) {
67
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
68
+ }
69
+ function stableStringify(value) {
70
+ if (typeof value === "undefined") {
71
+ return "";
72
+ }
73
+ if (value === null || typeof value !== "object") {
74
+ return JSON.stringify(value) ?? String(value);
75
+ }
76
+ if (Array.isArray(value)) {
77
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
78
+ }
79
+ const record = value;
80
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
81
+ }
82
+ function shouldAutoInjectPromptCacheKeyForCompat(model) {
83
+ const normalized = typeof model === "string" && model.trim() ? model.trim().toLowerCase() : DEFAULT_CODEX_MODEL.toLowerCase();
84
+ return normalized.includes("gpt-5") || normalized.includes("codex");
85
+ }
86
+ function extractInputContentText(content) {
87
+ if (typeof content === "string") {
88
+ return content.trim();
89
+ }
90
+ if (!Array.isArray(content)) {
91
+ const record = asRecord(content);
92
+ return typeof record?.text === "string" ? record.text.trim() : "";
93
+ }
94
+ return content.map((part) => {
95
+ const record = asRecord(part);
96
+ if (!record) {
97
+ return "";
98
+ }
99
+ if (typeof record.text === "string") {
100
+ return record.text.trim();
101
+ }
102
+ if (typeof record.input_text === "string") {
103
+ return record.input_text.trim();
104
+ }
105
+ return "";
106
+ }).filter(Boolean).join("\n").trim();
107
+ }
108
+ function extractFirstInputTextByRole(input, roles) {
109
+ if (typeof input === "string") {
110
+ return roles.has("user") ? input.trim() : "";
111
+ }
112
+ if (!Array.isArray(input)) {
113
+ return "";
114
+ }
115
+ for (const item of input) {
116
+ const record = asRecord(item);
117
+ if (!record) {
118
+ continue;
119
+ }
120
+ const role = typeof record.role === "string" ? record.role.trim().toLowerCase() : "user";
121
+ if (!roles.has(role)) {
122
+ continue;
123
+ }
124
+ const text = extractInputContentText(record.content);
125
+ if (text) {
126
+ return text;
127
+ }
128
+ }
129
+ return "";
130
+ }
131
+ function deriveCompatPromptCacheKey(body) {
132
+ const model = typeof body.model === "string" && body.model.trim() ? body.model.trim() : DEFAULT_CODEX_MODEL;
133
+ if (!shouldAutoInjectPromptCacheKeyForCompat(model)) {
134
+ return "";
135
+ }
136
+ const seedParts = [`model=${model}`];
137
+ const reasoning = asRecord(body.reasoning);
138
+ if (typeof reasoning?.effort === "string" && reasoning.effort.trim()) {
139
+ seedParts.push(`reasoning_effort=${reasoning.effort.trim()}`);
140
+ }
141
+ if (typeof body.tool_choice !== "undefined") {
142
+ seedParts.push(`tool_choice=${stableStringify(body.tool_choice)}`);
143
+ }
144
+ if (Array.isArray(body.tools) && body.tools.length > 0) {
145
+ seedParts.push(`tools=${stableStringify(body.tools)}`);
146
+ }
147
+ if (typeof body.instructions === "string" && body.instructions.trim()) {
148
+ seedParts.push(`instructions=${body.instructions.trim()}`);
149
+ }
150
+ const systemText = extractFirstInputTextByRole(body.input, /* @__PURE__ */ new Set(["system", "developer"]));
151
+ if (systemText) {
152
+ seedParts.push(`system=${systemText}`);
153
+ }
154
+ const firstUserText = extractFirstInputTextByRole(body.input, /* @__PURE__ */ new Set(["user"]));
155
+ if (firstUserText) {
156
+ seedParts.push(`first_user=${firstUserText}`);
157
+ }
158
+ return `${COMPAT_PROMPT_CACHE_KEY_PREFIX}${hashShort(seedParts.join("|"))}`;
159
+ }
160
+ function withCompatPromptCacheKey(body) {
161
+ const existing = typeof body.prompt_cache_key === "string" ? body.prompt_cache_key.trim() : "";
162
+ if (existing) {
163
+ return body;
164
+ }
165
+ const promptCacheKey = deriveCompatPromptCacheKey(body);
166
+ return promptCacheKey ? { ...body, prompt_cache_key: promptCacheKey } : body;
167
+ }
168
+ function deterministicSessionUUID(seed) {
169
+ const hash = Buffer.from(createHash("sha256").update(seed).digest().subarray(0, 16));
170
+ hash[6] = hash[6] & 15 | 64;
171
+ hash[8] = hash[8] & 63 | 128;
172
+ const hex = hash.toString("hex");
173
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
174
+ }
175
+ function normalizeSessionIdentifier(value) {
176
+ const trimmed = value.trim();
177
+ return UUID_RE.test(trimmed) ? trimmed : deterministicSessionUUID(trimmed);
178
+ }
179
+ function requestBodyString(body, key) {
180
+ const value = body?.[key];
181
+ return typeof value === "string" ? value.trim() : "";
182
+ }
183
+ function buildCodexRequestHeaders(profile, requestBody) {
184
+ const headers = {
65
185
  Accept: "text/event-stream",
66
186
  "Content-Type": "application/json",
67
187
  Authorization: `Bearer ${profile.access}`,
@@ -70,6 +190,17 @@ function buildCodexRequestHeaders(profile) {
70
190
  Originator: "pi",
71
191
  "User-Agent": "pi (bun demo)"
72
192
  };
193
+ const explicitSessionId = requestBodyString(requestBody, "session_id");
194
+ const conversationId = requestBodyString(requestBody, "conversation_id");
195
+ const promptCacheKey = requestBodyString(requestBody, "prompt_cache_key");
196
+ const sessionSeed = explicitSessionId || promptCacheKey || conversationId;
197
+ if (sessionSeed) {
198
+ headers.session_id = normalizeSessionIdentifier(sessionSeed);
199
+ }
200
+ if (conversationId) {
201
+ headers.conversation_id = normalizeSessionIdentifier(conversationId);
202
+ }
203
+ return headers;
73
204
  }
74
205
  function createCodexUpstreamError(status, body, transport, quota, requestId) {
75
206
  const upstreamError = parseUpstreamErrorBody(body);
@@ -397,17 +528,17 @@ function extractCodexText(body, requestBody) {
397
528
  };
398
529
  }
399
530
  async function askOpenAICodex(params) {
400
- const requestBody = {
531
+ const requestBody = withCompatPromptCacheKey({
401
532
  ...buildDefaultRequestBody(params),
402
533
  ...params.bodyOverride ?? {}
403
- };
534
+ });
404
535
  if (typeof requestBody.input === "undefined") {
405
536
  throw new Error("Codex \u8BF7\u6C42\u7F3A\u5C11 input\u3002\u8BF7\u63D0\u4F9B prompt \u6216\u5728\u5B9E\u9A8C\u8BF7\u6C42\u4F53\u91CC\u663E\u5F0F\u4F20\u5165 input\u3002");
406
537
  }
407
538
  const response = await requestText({
408
539
  method: "POST",
409
540
  url: CODEX_RESPONSES_URL,
410
- headers: buildCodexRequestHeaders(params.profile),
541
+ headers: buildCodexRequestHeaders(params.profile, requestBody),
411
542
  body: JSON.stringify(requestBody)
412
543
  });
413
544
  const quota = extractCodexQuotaSnapshot(response.headers, response.requestId);
@@ -420,17 +551,17 @@ async function askOpenAICodex(params) {
420
551
  };
421
552
  }
422
553
  async function streamOpenAICodex(params) {
423
- const requestBody = params.passthroughBody ? { ...params.bodyOverride ?? {} } : {
554
+ const requestBody = params.passthroughBody ? { ...params.bodyOverride ?? {} } : withCompatPromptCacheKey({
424
555
  ...buildDefaultRequestBody(params),
425
556
  ...params.bodyOverride ?? {}
426
- };
557
+ });
427
558
  if (!params.passthroughBody && typeof requestBody.input === "undefined") {
428
559
  throw new Error("Codex \u8BF7\u6C42\u7F3A\u5C11 input\u3002\u8BF7\u63D0\u4F9B prompt \u6216\u5728\u5B9E\u9A8C\u8BF7\u6C42\u4F53\u91CC\u663E\u5F0F\u4F20\u5165 input\u3002");
429
560
  }
430
561
  const response = await requestStream({
431
562
  method: "POST",
432
563
  url: params.endpoint === "responses/compact" ? CODEX_RESPONSES_COMPACT_URL : CODEX_RESPONSES_URL,
433
- headers: buildCodexRequestHeaders(params.profile),
564
+ headers: buildCodexRequestHeaders(params.profile, requestBody),
434
565
  body: JSON.stringify(requestBody),
435
566
  signal: params.signal
436
567
  });
@@ -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,
@@ -151,6 +152,17 @@ class AuthService {
151
152
  const percents = this.getQuotaPercents(profile);
152
153
  return percents.length > 0 && percents.some((value) => value >= 100);
153
154
  }
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
+ }
154
166
  timestampToMillis(value) {
155
167
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
156
168
  return void 0;
@@ -201,17 +213,38 @@ class AuthService {
201
213
  }
202
214
  const percents = this.getQuotaPercents(profile);
203
215
  if (percents.length === 0) {
204
- return false;
216
+ return true;
205
217
  }
206
218
  return percents.every((value) => value < 100) || this.hasResetElapsedForExhaustedQuota(profile);
207
219
  }
208
220
  hasInvalidAuthStatus(profile) {
209
221
  return profile.authStatus?.state === "token_invalidated" || profile.authStatus?.state === "auth_error";
210
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
+ }
211
244
  getQuotaUsageScore(profile) {
212
245
  const percents = this.getQuotaPercents(profile);
213
246
  if (percents.length === 0) {
214
- return 100;
247
+ return 50;
215
248
  }
216
249
  return Math.max(...percents);
217
250
  }
@@ -222,12 +255,12 @@ class AuthService {
222
255
  }
223
256
  return updater(profile);
224
257
  });
225
- 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)) {
226
259
  return updated;
227
260
  }
228
261
  const activeProfile = await this.getActiveProfile(provider);
229
262
  if (activeProfile?.profileId === updated.profileId) {
230
- await this.maybeAutoSwitchProfile(updated, provider);
263
+ return this.maybeAutoSwitchProfile(updated, provider);
231
264
  }
232
265
  return updated;
233
266
  }
@@ -259,7 +292,7 @@ class AuthService {
259
292
  async maybeAutoSwitchProfile(profile, provider) {
260
293
  const settings = await this.configService.getSettings();
261
294
  const excludedProfileIds = new Set(settings.autoSwitch.excludedProfileIds);
262
- if (!settings.autoSwitch.enabled || excludedProfileIds.has(profile.profileId) || !this.isQuotaExhausted(profile)) {
295
+ if (!settings.autoSwitch.enabled || excludedProfileIds.has(profile.profileId) || !this.shouldLeaveActiveProfile(profile)) {
263
296
  return profile;
264
297
  }
265
298
  const [profiles, codexStatus] = await Promise.all([
@@ -302,8 +335,9 @@ class AuthService {
302
335
  if (!activated) {
303
336
  return profile;
304
337
  }
305
- console.info("[auth] auto switched active profile after quota exhaustion", {
338
+ console.info("[auth] auto switched active profile", {
306
339
  provider,
340
+ reason: this.hasInvalidAuthStatus(profile) ? "auth_error" : "quota_exhausted",
307
341
  fromProfileId: profile.profileId,
308
342
  toProfileId: activated.profileId,
309
343
  avoidedCodexAccount: Boolean(codexAccountId && activated.accountId !== codexAccountId)
@@ -448,6 +482,69 @@ class AuthService {
448
482
  }
449
483
  return this.refreshStoredProfile(profile, provider);
450
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
+ }
451
548
  async requireFreshProfileWithIdToken(profileId, provider = "openai-codex") {
452
549
  const profiles = await listProfiles();
453
550
  const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
@@ -585,7 +682,7 @@ class AuthService {
585
682
  }),
586
683
  {
587
684
  skipAutoSwitch: options?.skipAutoSwitch,
588
- checkAutoSwitch: Boolean(quota)
685
+ checkAutoSwitch: Boolean(quota || authStatus)
589
686
  }
590
687
  );
591
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 {