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.
- package/CHANGELOG.md +19 -0
- package/README.md +34 -2
- package/README.zh-CN.md +35 -3
- package/admin-ui/dist/assets/{accounts-DymL4WIa.js → accounts-ABMyXo4H.js} +3 -1
- package/admin-ui/dist/assets/{docs-DtO-AOWU.js → docs-Dh0aFha_.js} +3 -3
- package/admin-ui/dist/assets/{image-bed-yIVQ4dKs.js → image-bed-C1M7-0q1.js} +1 -1
- package/admin-ui/dist/assets/index--rNjdmzf.js +10 -0
- package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
- package/admin-ui/dist/assets/{launch-CQXYrl-h.js → launch-pB7YlWFI.js} +1 -1
- package/admin-ui/dist/assets/logs-B7McijSi.js +1 -0
- package/admin-ui/dist/assets/{network-detect-sSrnwZqf.js → network-detect-Bx3XmXPk.js} +1 -1
- package/admin-ui/dist/assets/{overview-BbSON0Jl.js → overview-CV0H2Nsq.js} +1 -1
- package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
- package/admin-ui/dist/assets/{tester-CftPgRE9.js → tester-BG-up8qP.js} +1 -1
- package/admin-ui/dist/index.html +2 -2
- package/build/tray-icon-template.png +0 -0
- package/dist/core/providers/http-client.js +228 -3
- package/dist/core/providers/openai-codex/chat.js +160 -23
- package/dist/core/services/auth-service.js +14 -5
- package/dist/core/services/chat-service.js +1 -0
- 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/desktop/main.js +616 -15
- package/dist/server/app.js +859 -91
- package/dist/server/index.js +2 -1
- package/docs/API_USAGE.md +82 -1
- package/docs/DESKTOP_RELEASE.md +24 -0
- package/package.json +3 -1
- package/admin-ui/dist/assets/index-DRe-tByu.js +0 -10
- package/admin-ui/dist/assets/logs-awABDg1C.js +0 -1
- 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
|
-
|
|
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"]);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|