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.
- package/CHANGELOG.md +10 -0
- package/README.md +17 -2
- package/README.zh-CN.md +18 -3
- package/admin-ui/dist/assets/{accounts-CTjk9c4F.js → accounts-ABMyXo4H.js} +1 -1
- package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs-Dh0aFha_.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-C1M7-0q1.js} +1 -1
- package/admin-ui/dist/assets/{index-rgcJgVAu.js → index--rNjdmzf.js} +2 -2
- package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
- package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-pB7YlWFI.js} +1 -1
- package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-B7McijSi.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-Bx3XmXPk.js} +1 -1
- package/admin-ui/dist/assets/{overview-X_WodIqE.js → overview-CV0H2Nsq.js} +1 -1
- package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
- package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-BG-up8qP.js} +1 -1
- package/admin-ui/dist/index.html +2 -2
- package/dist/core/providers/http-client.js +228 -3
- package/dist/core/providers/openai-codex/chat.js +83 -23
- package/dist/core/services/auth-service.js +14 -5
- package/dist/core/services/config-service.js +15 -5
- package/dist/core/store/codex-auth-store.js +295 -4
- package/dist/core/store/settings-store.js +54 -24
- package/dist/server/app.js +410 -49
- package/docs/API_USAGE.md +18 -1
- package/docs/DESKTOP_RELEASE.md +12 -1
- package/package.json +1 -1
- package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
package/admin-ui/dist/index.html
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
-
|
|
93
|
-
|
|
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: ["
|
|
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
|
|
43
|
-
if (!
|
|
42
|
+
const record = asRecord(parsed.error);
|
|
43
|
+
if (!record) {
|
|
44
44
|
return void 0;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|