ai-zero-token 2.0.3 → 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 (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +34 -2
  3. package/README.zh-CN.md +35 -3
  4. package/admin-ui/dist/assets/{accounts-DymL4WIa.js → accounts-ABMyXo4H.js} +3 -1
  5. package/admin-ui/dist/assets/{docs-DtO-AOWU.js → docs-Dh0aFha_.js} +3 -3
  6. package/admin-ui/dist/assets/{image-bed-yIVQ4dKs.js → image-bed-C1M7-0q1.js} +1 -1
  7. package/admin-ui/dist/assets/index--rNjdmzf.js +10 -0
  8. package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
  9. package/admin-ui/dist/assets/{launch-CQXYrl-h.js → launch-pB7YlWFI.js} +1 -1
  10. package/admin-ui/dist/assets/logs-B7McijSi.js +1 -0
  11. package/admin-ui/dist/assets/{network-detect-sSrnwZqf.js → network-detect-Bx3XmXPk.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-BbSON0Jl.js → overview-CV0H2Nsq.js} +1 -1
  13. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
  14. package/admin-ui/dist/assets/{tester-CftPgRE9.js → tester-BG-up8qP.js} +1 -1
  15. package/admin-ui/dist/index.html +2 -2
  16. package/build/tray-icon-template.png +0 -0
  17. package/dist/core/providers/http-client.js +228 -3
  18. package/dist/core/providers/openai-codex/chat.js +160 -23
  19. package/dist/core/services/auth-service.js +14 -5
  20. package/dist/core/services/chat-service.js +1 -0
  21. package/dist/core/services/config-service.js +15 -5
  22. package/dist/core/store/codex-auth-store.js +295 -4
  23. package/dist/core/store/settings-store.js +54 -24
  24. package/dist/desktop/main.js +616 -15
  25. package/dist/server/app.js +859 -91
  26. package/dist/server/index.js +2 -1
  27. package/docs/API_USAGE.md +82 -1
  28. package/docs/DESKTOP_RELEASE.md +24 -0
  29. package/package.json +3 -1
  30. package/admin-ui/dist/assets/index-DRe-tByu.js +0 -10
  31. package/admin-ui/dist/assets/logs-awABDg1C.js +0 -1
  32. package/admin-ui/dist/assets/settings-DvRiHS7i.js +0 -1
Binary file
@@ -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"]);
@@ -138,6 +182,80 @@ function extractOutputText(payload) {
138
182
  }
139
183
  return "";
140
184
  }
185
+ function asRecord(value) {
186
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
187
+ }
188
+ function optionalString(value) {
189
+ return typeof value === "string" && value ? value : void 0;
190
+ }
191
+ function stringifyToolArguments(value) {
192
+ if (typeof value === "string") {
193
+ return value;
194
+ }
195
+ if (typeof value === "undefined" || value === null) {
196
+ return "";
197
+ }
198
+ try {
199
+ return JSON.stringify(value);
200
+ } catch {
201
+ return String(value);
202
+ }
203
+ }
204
+ function upsertFunctionCall(items, value) {
205
+ const record = asRecord(value);
206
+ if (!record || record.type !== "function_call") {
207
+ return;
208
+ }
209
+ const name = optionalString(record.name);
210
+ const id = optionalString(record.call_id) ?? optionalString(record.id);
211
+ if (!name || !id) {
212
+ return;
213
+ }
214
+ items.set(id, {
215
+ id,
216
+ type: "function",
217
+ function: {
218
+ name,
219
+ arguments: stringifyToolArguments(record.arguments)
220
+ }
221
+ });
222
+ }
223
+ function appendFunctionCallArgumentsDelta(items, argumentDeltas, event) {
224
+ if (event.type !== "response.function_call_arguments.delta") {
225
+ return;
226
+ }
227
+ const itemId = optionalString(event.item_id) ?? optionalString(event.call_id);
228
+ if (!itemId || typeof event.delta !== "string") {
229
+ return;
230
+ }
231
+ argumentDeltas.set(itemId, `${argumentDeltas.get(itemId) ?? ""}${event.delta}`);
232
+ const existing = items.get(itemId);
233
+ if (existing) {
234
+ existing.function.arguments = argumentDeltas.get(itemId) ?? existing.function.arguments;
235
+ }
236
+ }
237
+ function extractToolCalls(responsePayload, events) {
238
+ const calls = /* @__PURE__ */ new Map();
239
+ const argumentDeltas = /* @__PURE__ */ new Map();
240
+ const response = asRecord(responsePayload);
241
+ const output = Array.isArray(response?.output) ? response.output : [];
242
+ for (const item of output) {
243
+ upsertFunctionCall(calls, item);
244
+ }
245
+ for (const event of events) {
246
+ if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
247
+ upsertFunctionCall(calls, event.item);
248
+ }
249
+ appendFunctionCallArgumentsDelta(calls, argumentDeltas, event);
250
+ }
251
+ for (const [id, argumentsDelta] of argumentDeltas) {
252
+ const existing = calls.get(id);
253
+ if (existing && !existing.function.arguments) {
254
+ existing.function.arguments = argumentsDelta;
255
+ }
256
+ }
257
+ return Array.from(calls.values()).filter((call) => call.function.name);
258
+ }
141
259
  function parseSseEvents(body) {
142
260
  const events = [];
143
261
  for (const chunk of body.split("\n\n")) {
@@ -249,6 +367,7 @@ function extractCodexText(body, requestBody) {
249
367
  }
250
368
  }
251
369
  const completedText = extractOutputText(responsePayload);
370
+ const toolCalls = extractToolCalls(responsePayload, events);
252
371
  const artifacts = [
253
372
  ...collectArtifactCandidates(responsePayload, "response"),
254
373
  ...collectArtifactCandidates(events, "event")
@@ -256,6 +375,7 @@ function extractCodexText(body, requestBody) {
256
375
  if (completedText) {
257
376
  return {
258
377
  text: completedText,
378
+ toolCalls,
259
379
  raw: {
260
380
  request: requestBody,
261
381
  response: responsePayload ?? null,
@@ -266,6 +386,7 @@ function extractCodexText(body, requestBody) {
266
386
  }
267
387
  return {
268
388
  text: accumulated.trim(),
389
+ toolCalls,
269
390
  raw: {
270
391
  request: requestBody,
271
392
  response: responsePayload ?? null,
@@ -285,34 +406,50 @@ async function askOpenAICodex(params) {
285
406
  const response = await requestText({
286
407
  method: "POST",
287
408
  url: CODEX_RESPONSES_URL,
288
- headers: {
289
- Accept: "text/event-stream",
290
- "Content-Type": "application/json",
291
- Authorization: `Bearer ${params.profile.access}`,
292
- "ChatGPT-Account-Id": params.profile.accountId,
293
- "OpenAI-Beta": "responses=experimental",
294
- Originator: "pi",
295
- "User-Agent": "pi (bun demo)"
296
- },
409
+ headers: buildCodexRequestHeaders(params.profile),
297
410
  body: JSON.stringify(requestBody)
298
411
  });
299
412
  const quota = extractCodexQuotaSnapshot(response.headers, response.requestId);
300
413
  if (response.status < 200 || response.status >= 300) {
301
- const upstreamError = parseUpstreamErrorBody(response.body);
302
- const error = new Error(`\u8C03\u7528 Responses API \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
303
- error.quota = quota;
304
- error.upstreamStatus = response.status;
305
- error.upstreamErrorCode = upstreamError?.code;
306
- error.upstreamErrorType = upstreamError?.type;
307
- error.upstreamErrorMessage = upstreamError?.message;
308
- throw error;
414
+ throw createCodexUpstreamError(response.status, response.body, response.transport, quota, response.requestId);
309
415
  }
310
416
  return {
311
417
  ...extractCodexText(response.body, requestBody),
312
418
  quota
313
419
  };
314
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
+ }
315
451
  export {
316
452
  askOpenAICodex,
317
- extractCodexQuotaSnapshot
453
+ extractCodexQuotaSnapshot,
454
+ streamOpenAICodex
318
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) => ({
@@ -23,6 +23,7 @@ class ChatService {
23
23
  provider,
24
24
  model,
25
25
  text: result.text,
26
+ toolCalls: result.toolCalls,
26
27
  raw: result.raw,
27
28
  artifacts: result.artifacts
28
29
  };
@@ -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
  }