ai-zero-token 2.0.4 → 2.0.5

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 (26) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +18 -3
  4. package/admin-ui/dist/assets/{accounts-CTjk9c4F.js → accounts-ABMyXo4H.js} +1 -1
  5. package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs-Dh0aFha_.js} +1 -1
  6. package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-C1M7-0q1.js} +1 -1
  7. package/admin-ui/dist/assets/{index-rgcJgVAu.js → index--rNjdmzf.js} +2 -2
  8. package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
  9. package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-pB7YlWFI.js} +1 -1
  10. package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-B7McijSi.js} +1 -1
  11. package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-Bx3XmXPk.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-X_WodIqE.js → overview-CV0H2Nsq.js} +1 -1
  13. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
  14. package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-BG-up8qP.js} +1 -1
  15. package/admin-ui/dist/index.html +2 -2
  16. package/dist/core/providers/http-client.js +228 -3
  17. package/dist/core/providers/openai-codex/chat.js +83 -23
  18. package/dist/core/services/auth-service.js +14 -5
  19. package/dist/core/services/config-service.js +15 -5
  20. package/dist/core/store/codex-auth-store.js +295 -4
  21. package/dist/core/store/settings-store.js +54 -24
  22. package/dist/server/app.js +410 -49
  23. package/docs/API_USAGE.md +18 -1
  24. package/docs/DESKTOP_RELEASE.md +12 -1
  25. package/package.json +1 -1
  26. package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
@@ -5,10 +5,10 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/assets/app-mark-nsRs4vo7.svg" />
7
7
  <title>AI Zero Token</title>
8
- <script type="module" crossorigin src="/assets/index-rgcJgVAu.js"></script>
8
+ <script type="module" crossorigin src="/assets/index--rNjdmzf.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-DqpGtLhh.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/profiles-DMOjJORP.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-By4r-wy3.css">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DjtN30PC.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { PassThrough, Readable } from "node:stream";
3
7
  import { loadSettings } from "../store/settings-store.js";
4
8
  const CURL_STATUS_MARKER = "\n__CURL_STATUS__:";
5
9
  const CURL_HEADERS_MARKER = "\n__CURL_HEADERS__:";
@@ -38,6 +42,9 @@ function logHttpTiming(params) {
38
42
  totalMs: params.timing.totalMs
39
43
  });
40
44
  }
45
+ function isElectronRuntime() {
46
+ return typeof process.versions.electron === "string";
47
+ }
41
48
  function normalizeHeaders(headers) {
42
49
  const normalized = {};
43
50
  headers.forEach((value, key) => {
@@ -62,6 +69,33 @@ function normalizeCurlHeaders(value) {
62
69
  })
63
70
  );
64
71
  }
72
+ function parseCurlHeaderDump(raw) {
73
+ const blocks = raw.replace(/\r\n/g, "\n").split(/\n\n+/).map((block2) => block2.trim()).filter((block2) => /^HTTP\//i.test(block2));
74
+ const block = blocks[blocks.length - 1];
75
+ if (!block) {
76
+ return null;
77
+ }
78
+ const lines = block.split("\n");
79
+ const statusMatch = /^HTTP(?:\/\S+)?\s+(\d{3})/i.exec(lines[0]?.trim() ?? "");
80
+ const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : NaN;
81
+ if (!Number.isFinite(status)) {
82
+ return null;
83
+ }
84
+ const headers = {};
85
+ for (const line of lines.slice(1)) {
86
+ const separator = line.indexOf(":");
87
+ if (separator <= 0) {
88
+ continue;
89
+ }
90
+ const key = line.slice(0, separator).trim().toLowerCase();
91
+ const value = line.slice(separator + 1).trim();
92
+ if (!key || !value) {
93
+ continue;
94
+ }
95
+ headers[key] = headers[key] ? `${headers[key]}, ${value}` : value;
96
+ }
97
+ return { status, headers };
98
+ }
65
99
  async function runCurlRequest(init, params) {
66
100
  const requestId = params?.requestId ?? nextRequestId();
67
101
  const startedAt = performance.now();
@@ -89,13 +123,16 @@ async function runCurlRequest(init, params) {
89
123
  for (const [key, value] of Object.entries(init.headers ?? {})) {
90
124
  args.push("--header", `${key}: ${value}`);
91
125
  }
92
- if (typeof init.body === "string") {
93
- args.push("--data-raw", init.body);
126
+ const hasBody = typeof init.body === "string";
127
+ if (hasBody) {
128
+ args.push("--data-binary", "@-");
94
129
  }
95
130
  const child = spawn("curl", args, {
96
131
  env: process.env,
97
- stdio: ["ignore", "pipe", "pipe"]
132
+ stdio: ["pipe", "pipe", "pipe"]
98
133
  });
134
+ child.stdin.on("error", () => void 0);
135
+ child.stdin.end(hasBody ? init.body : void 0);
99
136
  const phases = {
100
137
  spawnCurlMs: performance.now() - startedAt
101
138
  };
@@ -166,6 +203,133 @@ async function runCurlRequest(init, params) {
166
203
  headers
167
204
  };
168
205
  }
206
+ async function waitForCurlHeaders(params) {
207
+ const startedAt = performance.now();
208
+ while (performance.now() - startedAt < params.timeoutMs) {
209
+ try {
210
+ const raw = await fs.readFile(params.headerPath, "utf8");
211
+ const parsed = parseCurlHeaderDump(raw);
212
+ if (parsed) {
213
+ return parsed;
214
+ }
215
+ } catch {
216
+ }
217
+ if (params.isClosed()) {
218
+ throw new Error(params.stderr().trim() || "curl stream \u8BF7\u6C42\u5728\u8FD4\u56DE\u54CD\u5E94\u5934\u524D\u7ED3\u675F\u3002");
219
+ }
220
+ await new Promise((resolve) => setTimeout(resolve, 25));
221
+ }
222
+ throw new Error("\u7B49\u5F85 curl stream \u54CD\u5E94\u5934\u8D85\u65F6\u3002");
223
+ }
224
+ async function runCurlStream(init, params) {
225
+ if (init.signal?.aborted) {
226
+ throw new Error("stream \u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
227
+ }
228
+ const requestId = params?.requestId ?? nextRequestId();
229
+ const startedAt = performance.now();
230
+ const timeoutSeconds = typeof params?.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.max(1, Math.ceil(params.timeoutMs / 1e3)) : void 0;
231
+ const headerPath = path.join(os.tmpdir(), `azt-curl-headers-${process.pid}-${requestId}.txt`);
232
+ const args = [
233
+ "--silent",
234
+ "--show-error",
235
+ "--location",
236
+ "--no-buffer",
237
+ "--request",
238
+ init.method,
239
+ init.url,
240
+ "--dump-header",
241
+ headerPath
242
+ ];
243
+ if (typeof timeoutSeconds === "number") {
244
+ args.push("--connect-timeout", String(Math.min(timeoutSeconds, 10)));
245
+ args.push("--max-time", String(timeoutSeconds));
246
+ } else {
247
+ args.push("--connect-timeout", "10");
248
+ }
249
+ if (params?.proxy?.enabled && params.proxy.url.trim()) {
250
+ args.push("--proxy", params.proxy.url.trim());
251
+ if (params.proxy.noProxy.trim()) {
252
+ args.push("--noproxy", params.proxy.noProxy.trim());
253
+ }
254
+ }
255
+ for (const [key, value] of Object.entries(init.headers ?? {})) {
256
+ args.push("--header", `${key}: ${value}`);
257
+ }
258
+ const hasBody = typeof init.body === "string";
259
+ if (hasBody) {
260
+ args.push("--data-binary", "@-");
261
+ }
262
+ const child = spawn("curl", args, {
263
+ env: process.env,
264
+ stdio: ["pipe", "pipe", "pipe"]
265
+ });
266
+ child.stdin.on("error", () => void 0);
267
+ child.stdin.end(hasBody ? init.body : void 0);
268
+ const body = new PassThrough();
269
+ const phases = {
270
+ spawnCurlMs: performance.now() - startedAt
271
+ };
272
+ let stderr = "";
273
+ let closed = false;
274
+ let exitCode = 0;
275
+ const abort = () => {
276
+ child.kill("SIGTERM");
277
+ };
278
+ init.signal?.addEventListener("abort", abort, { once: true });
279
+ child.stdout.pipe(body);
280
+ child.stderr.setEncoding("utf8");
281
+ child.stderr.on("data", (chunk) => {
282
+ stderr += chunk;
283
+ });
284
+ child.on("error", (error) => {
285
+ closed = true;
286
+ stderr = stderr || error.message;
287
+ body.destroy(error);
288
+ });
289
+ child.on("close", (code) => {
290
+ closed = true;
291
+ exitCode = code ?? 1;
292
+ init.signal?.removeEventListener("abort", abort);
293
+ void fs.unlink(headerPath).catch(() => void 0);
294
+ if (exitCode !== 0 && !init.signal?.aborted) {
295
+ body.destroy(new Error(stderr.trim() || `curl stream \u8BF7\u6C42\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode}`));
296
+ }
297
+ });
298
+ let parsed;
299
+ try {
300
+ parsed = await waitForCurlHeaders({
301
+ headerPath,
302
+ isClosed: () => closed,
303
+ stderr: () => stderr,
304
+ timeoutMs: typeof params?.timeoutMs === "number" ? Math.min(params.timeoutMs, 3e4) : 3e4
305
+ });
306
+ } catch (error) {
307
+ child.kill("SIGTERM");
308
+ init.signal?.removeEventListener("abort", abort);
309
+ void fs.unlink(headerPath).catch(() => void 0);
310
+ body.destroy(error instanceof Error ? error : new Error(String(error)));
311
+ throw error;
312
+ }
313
+ phases.waitForHeadersMs = performance.now() - startedAt - phases.spawnCurlMs;
314
+ const timing = finalizeTiming(startedAt, phases);
315
+ logHttpTiming({
316
+ requestId,
317
+ method: init.method,
318
+ url: init.url,
319
+ status: parsed.status,
320
+ transport: "curl",
321
+ timing,
322
+ fallbackFrom: params?.fallbackFrom
323
+ });
324
+ return {
325
+ body: Readable.toWeb(body),
326
+ status: parsed.status,
327
+ transport: "curl",
328
+ timing,
329
+ requestId,
330
+ headers: parsed.headers
331
+ };
332
+ }
169
333
  async function loadNetworkProxySettings() {
170
334
  try {
171
335
  const settings = await loadSettings();
@@ -236,6 +400,67 @@ async function requestText(init) {
236
400
  timeoutMs: remainingTimeoutMs
237
401
  });
238
402
  }
403
+ async function requestStream(init) {
404
+ const requestId = nextRequestId();
405
+ const requestStartedAt = performance.now();
406
+ const proxy = init.ignoreProxy ? void 0 : init.proxyOverride ?? await loadNetworkProxySettings();
407
+ const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === "1" || isElectronRuntime();
408
+ const useConfiguredProxy = !!proxy?.enabled && !!proxy.url.trim();
409
+ const timeoutMs = init.timeoutMs;
410
+ if (!useCurlOnly && !useConfiguredProxy) {
411
+ const phases = {};
412
+ try {
413
+ const fetchStartedAt = performance.now();
414
+ const response = await fetch(init.url, {
415
+ method: init.method,
416
+ headers: init.headers,
417
+ body: init.body,
418
+ signal: init.signal
419
+ });
420
+ phases.waitForHeadersMs = performance.now() - fetchStartedAt;
421
+ const timing = finalizeTiming(requestStartedAt, phases);
422
+ logHttpTiming({
423
+ requestId,
424
+ method: init.method,
425
+ url: init.url,
426
+ status: response.status,
427
+ transport: "fetch",
428
+ timing
429
+ });
430
+ if (!response.body) {
431
+ throw new Error("fetch stream \u54CD\u5E94\u7F3A\u5C11 body\u3002");
432
+ }
433
+ return {
434
+ body: response.body,
435
+ status: response.status,
436
+ transport: "fetch",
437
+ timing,
438
+ requestId,
439
+ headers: normalizeHeaders(response.headers)
440
+ };
441
+ } catch (error) {
442
+ if (init.signal?.aborted) {
443
+ throw error;
444
+ }
445
+ const message = error instanceof Error ? error.message : String(error);
446
+ safeConsole("warn", "[http] fetch stream attempt failed", {
447
+ requestId,
448
+ method: init.method,
449
+ url: init.url,
450
+ elapsedMs: roundMs(performance.now() - requestStartedAt),
451
+ error: message
452
+ });
453
+ }
454
+ }
455
+ const remainingTimeoutMs = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1e3, timeoutMs - (performance.now() - requestStartedAt)) : void 0;
456
+ return runCurlStream(init, {
457
+ requestId,
458
+ fallbackFrom: useCurlOnly || useConfiguredProxy ? void 0 : "fetch",
459
+ proxy,
460
+ timeoutMs: remainingTimeoutMs
461
+ });
462
+ }
239
463
  export {
464
+ requestStream,
240
465
  requestText
241
466
  };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { DEFAULT_CODEX_MODEL } from "../../models/openai-codex-models.js";
3
- import { requestText } from "../http-client.js";
3
+ import { requestStream, requestText } from "../http-client.js";
4
4
  const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
5
5
  const URL_KEY_RE = /(url|uri|href|download|preview|thumbnail|image|asset|file)/i;
6
6
  const REFERENCE_KEY_RE = /(image|asset|file|media|blob|artifact|download|preview|thumbnail)/i;
@@ -39,21 +39,65 @@ function parseOptionalText(value) {
39
39
  function parseUpstreamErrorBody(body) {
40
40
  try {
41
41
  const parsed = JSON.parse(body);
42
- const error = parsed.error;
43
- if (!error || typeof error !== "object") {
42
+ const record = asRecord(parsed.error);
43
+ if (!record) {
44
44
  return void 0;
45
45
  }
46
- const record = error;
46
+ const eligiblePromo = asRecord(record.eligible_promo);
47
47
  return {
48
48
  message: typeof record.message === "string" ? record.message : void 0,
49
49
  type: typeof record.type === "string" ? record.type : void 0,
50
50
  code: typeof record.code === "string" ? record.code : void 0,
51
- param: typeof record.param === "string" || record.param === null ? record.param : void 0
51
+ param: typeof record.param === "string" || record.param === null ? record.param : void 0,
52
+ planType: typeof record.plan_type === "string" ? record.plan_type : void 0,
53
+ resetsAt: typeof record.resets_at === "number" && Number.isFinite(record.resets_at) ? record.resets_at : void 0,
54
+ resetsInSeconds: typeof record.resets_in_seconds === "number" && Number.isFinite(record.resets_in_seconds) ? record.resets_in_seconds : void 0,
55
+ promoCampaignId: typeof eligiblePromo?.campaign_id === "string" ? eligiblePromo.campaign_id : void 0,
56
+ promoMessage: typeof eligiblePromo?.message === "string" ? eligiblePromo.message : void 0
52
57
  };
53
58
  } catch {
54
59
  return void 0;
55
60
  }
56
61
  }
62
+ function buildCodexRequestHeaders(profile) {
63
+ return {
64
+ Accept: "text/event-stream",
65
+ "Content-Type": "application/json",
66
+ Authorization: `Bearer ${profile.access}`,
67
+ "ChatGPT-Account-Id": profile.accountId,
68
+ "OpenAI-Beta": "responses=experimental",
69
+ Originator: "pi",
70
+ "User-Agent": "pi (bun demo)"
71
+ };
72
+ }
73
+ function createCodexUpstreamError(status, body, transport, quota, requestId) {
74
+ const upstreamError = parseUpstreamErrorBody(body);
75
+ const error = new Error(`\u8C03\u7528 Responses API \u5931\u8D25: HTTP ${status} via ${transport} ${body}`);
76
+ error.quota = quota ?? createUsageLimitQuotaSnapshot(status, upstreamError, requestId);
77
+ error.upstreamStatus = status;
78
+ error.upstreamErrorCode = upstreamError?.code;
79
+ error.upstreamErrorType = upstreamError?.type;
80
+ error.upstreamErrorMessage = upstreamError?.message;
81
+ return error;
82
+ }
83
+ function createUsageLimitQuotaSnapshot(status, upstreamError, requestId) {
84
+ const errorKind = `${upstreamError?.type ?? ""} ${upstreamError?.code ?? ""}`.toLowerCase();
85
+ if (status !== 429 && !errorKind.includes("usage_limit_reached")) {
86
+ return void 0;
87
+ }
88
+ return {
89
+ capturedAt: Date.now(),
90
+ sourceRequestId: requestId,
91
+ planType: upstreamError?.planType,
92
+ primaryUsedPercent: 100,
93
+ primaryResetAt: upstreamError?.resetsAt,
94
+ primaryResetAfterSeconds: upstreamError?.resetsInSeconds,
95
+ creditsHasCredits: false,
96
+ creditsBalance: "0",
97
+ promoCampaignId: upstreamError?.promoCampaignId,
98
+ promoMessage: upstreamError?.promoMessage
99
+ };
100
+ }
57
101
  function extractCodexQuotaSnapshot(headers, requestId) {
58
102
  const activeLimit = parseOptionalText(headers["x-codex-active-limit"]);
59
103
  const planType = parseOptionalText(headers["x-codex-plan-type"]);
@@ -362,34 +406,50 @@ async function askOpenAICodex(params) {
362
406
  const response = await requestText({
363
407
  method: "POST",
364
408
  url: CODEX_RESPONSES_URL,
365
- headers: {
366
- Accept: "text/event-stream",
367
- "Content-Type": "application/json",
368
- Authorization: `Bearer ${params.profile.access}`,
369
- "ChatGPT-Account-Id": params.profile.accountId,
370
- "OpenAI-Beta": "responses=experimental",
371
- Originator: "pi",
372
- "User-Agent": "pi (bun demo)"
373
- },
409
+ headers: buildCodexRequestHeaders(params.profile),
374
410
  body: JSON.stringify(requestBody)
375
411
  });
376
412
  const quota = extractCodexQuotaSnapshot(response.headers, response.requestId);
377
413
  if (response.status < 200 || response.status >= 300) {
378
- const upstreamError = parseUpstreamErrorBody(response.body);
379
- const error = new Error(`\u8C03\u7528 Responses API \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
380
- error.quota = quota;
381
- error.upstreamStatus = response.status;
382
- error.upstreamErrorCode = upstreamError?.code;
383
- error.upstreamErrorType = upstreamError?.type;
384
- error.upstreamErrorMessage = upstreamError?.message;
385
- throw error;
414
+ throw createCodexUpstreamError(response.status, response.body, response.transport, quota, response.requestId);
386
415
  }
387
416
  return {
388
417
  ...extractCodexText(response.body, requestBody),
389
418
  quota
390
419
  };
391
420
  }
421
+ async function streamOpenAICodex(params) {
422
+ const requestBody = params.passthroughBody ? { ...params.bodyOverride ?? {} } : {
423
+ ...buildDefaultRequestBody(params),
424
+ ...params.bodyOverride ?? {}
425
+ };
426
+ if (!params.passthroughBody && typeof requestBody.input === "undefined") {
427
+ 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");
428
+ }
429
+ const response = await requestStream({
430
+ method: "POST",
431
+ url: CODEX_RESPONSES_URL,
432
+ headers: buildCodexRequestHeaders(params.profile),
433
+ body: JSON.stringify(requestBody),
434
+ signal: params.signal
435
+ });
436
+ const headers = response.headers;
437
+ const requestId = headers["x-request-id"] ?? response.requestId;
438
+ const quota = extractCodexQuotaSnapshot(headers, requestId);
439
+ if (response.status < 200 || response.status >= 300) {
440
+ const body = await new Response(response.body).text();
441
+ throw createCodexUpstreamError(response.status, body, response.transport, quota, requestId);
442
+ }
443
+ return {
444
+ body: response.body,
445
+ headers,
446
+ quota,
447
+ requestId,
448
+ status: response.status
449
+ };
450
+ }
392
451
  export {
393
452
  askOpenAICodex,
394
- extractCodexQuotaSnapshot
453
+ extractCodexQuotaSnapshot,
454
+ streamOpenAICodex
395
455
  };
@@ -15,8 +15,10 @@ import {
15
15
  } from "../providers/openai-codex/oauth.js";
16
16
  import { askOpenAICodex } from "../providers/openai-codex/chat.js";
17
17
  import {
18
+ applyGatewayToCodexProviderConfig,
18
19
  applyProfileToCodexAuth,
19
- getCodexAuthStatus
20
+ getCodexAuthStatus,
21
+ removeGatewayFromCodexProviderConfig
20
22
  } from "../store/codex-auth-store.js";
21
23
  import {
22
24
  exportProfilesToJson,
@@ -209,7 +211,8 @@ class AuthService {
209
211
  }
210
212
  async maybeAutoSwitchProfile(profile, provider) {
211
213
  const settings = await this.configService.getSettings();
212
- if (!settings.autoSwitch.enabled || !this.isQuotaExhausted(profile)) {
214
+ const excludedProfileIds = new Set(settings.autoSwitch.excludedProfileIds);
215
+ if (!settings.autoSwitch.enabled || excludedProfileIds.has(profile.profileId) || !this.isQuotaExhausted(profile)) {
213
216
  return profile;
214
217
  }
215
218
  const [profiles, codexStatus] = await Promise.all([
@@ -222,7 +225,7 @@ class AuthService {
222
225
  profile: item,
223
226
  index,
224
227
  distance: currentIndex >= 0 ? (index - currentIndex + profiles.length) % profiles.length : index + 1
225
- })).filter((item) => item.profile.provider === provider && item.profile.profileId !== profile.profileId).filter((item) => this.hasKnownAvailableQuota(item.profile)).sort((left, right) => {
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) => {
226
229
  const leftCodexConflict = codexAccountId && left.profile.accountId === codexAccountId ? 1 : 0;
227
230
  const rightCodexConflict = codexAccountId && right.profile.accountId === codexAccountId ? 1 : 0;
228
231
  const codexDiff = leftCodexConflict - rightCodexConflict;
@@ -326,6 +329,12 @@ class AuthService {
326
329
  const profile = await this.requireFreshProfileWithIdToken(profileId, provider);
327
330
  return applyProfileToCodexAuth(profile);
328
331
  }
332
+ async applyGatewayToCodexProvider(params) {
333
+ return applyGatewayToCodexProviderConfig(params);
334
+ }
335
+ async removeGatewayFromCodexProvider(params) {
336
+ return removeGatewayFromCodexProviderConfig(params);
337
+ }
329
338
  async getActiveProfile(provider = "openai-codex") {
330
339
  const profile = await getActiveProfile();
331
340
  if (!profile || profile.provider !== provider) {
@@ -517,9 +526,9 @@ class AuthService {
517
526
  async recordProfileRequestFailure(profileId, error, quota, provider = "openai-codex", options) {
518
527
  const authStatus = this.createAuthStatusFromError(error);
519
528
  if (!quota && !authStatus) {
520
- return;
529
+ return null;
521
530
  }
522
- await this.applyProfileRuntimeUpdate(
531
+ return this.applyProfileRuntimeUpdate(
523
532
  profileId,
524
533
  provider,
525
534
  (profile) => ({
@@ -31,14 +31,22 @@ function normalizeNetworkProxy(settings, params) {
31
31
  noProxy
32
32
  };
33
33
  }
34
+ function normalizeProfileIdList(value, fallback = []) {
35
+ if (!value) {
36
+ return fallback;
37
+ }
38
+ return Array.from(
39
+ new Set(
40
+ value.map((item) => item.trim()).filter(Boolean)
41
+ )
42
+ );
43
+ }
34
44
  class ConfigService {
35
45
  async getSettings() {
36
46
  return this.ensureSettings();
37
47
  }
38
48
  async ensureSettings() {
39
- const settings = await loadSettings();
40
- await saveSettings(settings);
41
- return settings;
49
+ return loadSettings();
42
50
  }
43
51
  async getDefaultProvider() {
44
52
  const settings = await this.getSettings();
@@ -81,7 +89,8 @@ class ConfigService {
81
89
  const next = {
82
90
  ...settings,
83
91
  autoSwitch: {
84
- enabled: params.enabled
92
+ enabled: params.enabled ?? settings.autoSwitch.enabled,
93
+ excludedProfileIds: normalizeProfileIdList(params.excludedProfileIds, settings.autoSwitch.excludedProfileIds)
85
94
  }
86
95
  };
87
96
  await saveSettings(next);
@@ -138,7 +147,8 @@ class ConfigService {
138
147
  next = {
139
148
  ...next,
140
149
  autoSwitch: {
141
- enabled: params.autoSwitch.enabled
150
+ enabled: params.autoSwitch.enabled ?? next.autoSwitch.enabled,
151
+ excludedProfileIds: normalizeProfileIdList(params.autoSwitch.excludedProfileIds, next.autoSwitch.excludedProfileIds)
142
152
  }
143
153
  };
144
154
  }