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