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
package/dist/server/app.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import Fastify from "fastify";
|
|
7
8
|
import cors from "@fastify/cors";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { createGatewayContext } from "../core/context.js";
|
|
10
11
|
import { requestText } from "../core/providers/http-client.js";
|
|
12
|
+
import { streamOpenAICodex } from "../core/providers/openai-codex/chat.js";
|
|
11
13
|
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
12
14
|
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
13
15
|
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
16
|
+
const MAX_GATEWAY_REQUEST_LOGS = 100;
|
|
14
17
|
const assetContentTypes = {
|
|
15
18
|
".css": "text/css; charset=utf-8",
|
|
16
19
|
".gif": "image/gif",
|
|
@@ -44,17 +47,9 @@ async function readAdminUiAsset(assetPath) {
|
|
|
44
47
|
return null;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
const inputPartSchema = z.object({
|
|
48
|
-
type: z.string().optional(),
|
|
49
|
-
text: z.string().optional()
|
|
50
|
-
}).passthrough();
|
|
51
|
-
const inputMessageSchema = z.object({
|
|
52
|
-
role: z.string().optional(),
|
|
53
|
-
content: z.array(inputPartSchema).optional()
|
|
54
|
-
}).passthrough();
|
|
55
50
|
const responsesBodySchema = z.object({
|
|
56
51
|
model: z.string().optional(),
|
|
57
|
-
input: z.
|
|
52
|
+
input: z.unknown().optional(),
|
|
58
53
|
instructions: z.string().optional(),
|
|
59
54
|
stream: z.boolean().optional(),
|
|
60
55
|
tools: z.array(z.unknown()).optional(),
|
|
@@ -68,7 +63,7 @@ const responsesBodySchema = z.object({
|
|
|
68
63
|
allow_unknown_model: z.boolean().optional(),
|
|
69
64
|
include_raw: z.boolean().optional()
|
|
70
65
|
}).passthrough().optional()
|
|
71
|
-
});
|
|
66
|
+
}).passthrough();
|
|
72
67
|
const chatCompletionContentPartSchema = z.object({
|
|
73
68
|
type: z.string().optional(),
|
|
74
69
|
text: z.string().optional(),
|
|
@@ -113,7 +108,8 @@ const settingsUpdateSchema = z.object({
|
|
|
113
108
|
noProxy: z.string().optional()
|
|
114
109
|
}).optional(),
|
|
115
110
|
autoSwitch: z.object({
|
|
116
|
-
enabled: z.boolean()
|
|
111
|
+
enabled: z.boolean().optional(),
|
|
112
|
+
excludedProfileIds: z.array(z.string()).optional()
|
|
117
113
|
}).optional(),
|
|
118
114
|
runtime: z.object({
|
|
119
115
|
quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
|
|
@@ -149,6 +145,10 @@ const profileExportSchema = z.object({
|
|
|
149
145
|
const codexApplySchema = z.object({
|
|
150
146
|
profileId: z.string().min(1)
|
|
151
147
|
});
|
|
148
|
+
const codexProviderConfigSchema = z.object({
|
|
149
|
+
baseUrl: z.string().min(1).optional(),
|
|
150
|
+
providerId: z.string().min(1).optional()
|
|
151
|
+
});
|
|
152
152
|
const githubImageBedConfigSchema = z.object({
|
|
153
153
|
token: z.string().min(1)
|
|
154
154
|
});
|
|
@@ -198,13 +198,21 @@ const imageEditsBodySchema = z.object({
|
|
|
198
198
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
199
199
|
user: z.string().optional()
|
|
200
200
|
}).passthrough();
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
function extractTextFromInputContent(content) {
|
|
202
|
+
if (typeof content === "string" && content.trim()) {
|
|
203
|
+
return [content.trim()];
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(content)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return content.flatMap((part) => {
|
|
209
|
+
if (!part || typeof part !== "object") {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const record = part;
|
|
213
|
+
return typeof record.text === "string" && record.text.trim() ? [record.text.trim()] : [];
|
|
214
|
+
});
|
|
215
|
+
}
|
|
208
216
|
function extractTextInput(input) {
|
|
209
217
|
if (typeof input === "undefined") {
|
|
210
218
|
return "";
|
|
@@ -213,12 +221,14 @@ function extractTextInput(input) {
|
|
|
213
221
|
return input;
|
|
214
222
|
}
|
|
215
223
|
const chunks = [];
|
|
224
|
+
if (!Array.isArray(input)) {
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
216
227
|
for (const item of input) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
chunks.push(part.text.trim());
|
|
220
|
-
}
|
|
228
|
+
if (!item || typeof item !== "object") {
|
|
229
|
+
continue;
|
|
221
230
|
}
|
|
231
|
+
chunks.push(...extractTextFromInputContent(item.content));
|
|
222
232
|
}
|
|
223
233
|
return chunks.join("\n").trim();
|
|
224
234
|
}
|
|
@@ -242,7 +252,20 @@ function normalizeChatRole(role) {
|
|
|
242
252
|
}
|
|
243
253
|
return role ?? "user";
|
|
244
254
|
}
|
|
245
|
-
function
|
|
255
|
+
function safeJsonStringify(value) {
|
|
256
|
+
if (typeof value === "string") {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
if (typeof value === "undefined" || value === null) {
|
|
260
|
+
return "";
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
return JSON.stringify(value);
|
|
264
|
+
} catch {
|
|
265
|
+
return String(value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function normalizeChatContentPart(part, textType) {
|
|
246
269
|
if (part.type === "image_url") {
|
|
247
270
|
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
248
271
|
if (!url) {
|
|
@@ -258,37 +281,270 @@ function normalizeChatContentPart(part) {
|
|
|
258
281
|
}
|
|
259
282
|
const text = typeof part.text === "string" ? part.text : "";
|
|
260
283
|
return {
|
|
261
|
-
type:
|
|
284
|
+
type: textType,
|
|
262
285
|
text
|
|
263
286
|
};
|
|
264
287
|
}
|
|
265
|
-
function normalizeChatContent(content) {
|
|
288
|
+
function normalizeChatContent(content, role) {
|
|
289
|
+
const textType = role === "assistant" ? "output_text" : "input_text";
|
|
266
290
|
if (typeof content === "string") {
|
|
267
|
-
return [{ type:
|
|
291
|
+
return [{ type: textType, text: content }];
|
|
268
292
|
}
|
|
269
293
|
if (!Array.isArray(content) || content.length === 0) {
|
|
270
|
-
return [{ type:
|
|
294
|
+
return [{ type: textType, text: "" }];
|
|
271
295
|
}
|
|
272
|
-
return content.map((part) => normalizeChatContentPart(part));
|
|
296
|
+
return content.map((part) => normalizeChatContentPart(part, textType));
|
|
273
297
|
}
|
|
274
298
|
function normalizeChatMessages(messages) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
299
|
+
const normalized = [];
|
|
300
|
+
for (const message of messages) {
|
|
301
|
+
const record = message;
|
|
302
|
+
if (message.role === "tool") {
|
|
303
|
+
normalized.push({
|
|
304
|
+
type: "function_call_output",
|
|
305
|
+
call_id: message.tool_call_id,
|
|
306
|
+
output: typeof message.content === "string" ? message.content : safeJsonStringify(message.content)
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
normalized.push({
|
|
311
|
+
role: normalizeChatRole(message.role),
|
|
312
|
+
content: normalizeChatContent(message.content, message.role),
|
|
313
|
+
...message.name ? { name: message.name } : {},
|
|
314
|
+
...message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}
|
|
315
|
+
});
|
|
316
|
+
const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
|
|
317
|
+
for (const toolCall of toolCalls) {
|
|
318
|
+
const call = toolCall && typeof toolCall === "object" ? toolCall : {};
|
|
319
|
+
const fn = call.function && typeof call.function === "object" ? call.function : {};
|
|
320
|
+
const name = typeof fn.name === "string" ? fn.name : void 0;
|
|
321
|
+
if (!name) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
normalized.push({
|
|
325
|
+
type: "function_call",
|
|
326
|
+
call_id: typeof call.id === "string" ? call.id : `call_${normalized.length}`,
|
|
327
|
+
name,
|
|
328
|
+
arguments: safeJsonStringify(fn.arguments)
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return normalized;
|
|
333
|
+
}
|
|
334
|
+
function normalizeChatTools(tools) {
|
|
335
|
+
if (!tools) {
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
return tools.map((tool) => {
|
|
339
|
+
if (!tool || typeof tool !== "object") {
|
|
340
|
+
return tool;
|
|
341
|
+
}
|
|
342
|
+
const record = tool;
|
|
343
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
344
|
+
if (record.type !== "function" || !fn) {
|
|
345
|
+
return tool;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
type: "function",
|
|
349
|
+
name: fn.name,
|
|
350
|
+
description: fn.description,
|
|
351
|
+
parameters: fn.parameters
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function normalizeChatToolChoice(toolChoice) {
|
|
356
|
+
if (!toolChoice || typeof toolChoice !== "object") {
|
|
357
|
+
return toolChoice;
|
|
358
|
+
}
|
|
359
|
+
const record = toolChoice;
|
|
360
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
361
|
+
if (record.type === "function" && fn && typeof fn.name === "string") {
|
|
362
|
+
return {
|
|
363
|
+
type: "function",
|
|
364
|
+
name: fn.name
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return toolChoice;
|
|
368
|
+
}
|
|
369
|
+
function normalizeReasoningEffort(value) {
|
|
370
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (value === "minimal") {
|
|
374
|
+
return "low";
|
|
375
|
+
}
|
|
376
|
+
if (value === "xhigh") {
|
|
377
|
+
return "high";
|
|
378
|
+
}
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
function normalizeChatReasoning(data) {
|
|
382
|
+
const record = data;
|
|
383
|
+
const existing = record.reasoning;
|
|
384
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
385
|
+
return existing;
|
|
386
|
+
}
|
|
387
|
+
const effort = normalizeReasoningEffort(record.reasoning_effort);
|
|
388
|
+
return effort ? { effort } : void 0;
|
|
389
|
+
}
|
|
390
|
+
function truncateForLog(value, maxLength = 300) {
|
|
391
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
392
|
+
if (normalized.length <= maxLength) {
|
|
393
|
+
return normalized;
|
|
394
|
+
}
|
|
395
|
+
return `${normalized.slice(0, maxLength)}...`;
|
|
396
|
+
}
|
|
397
|
+
function extractChatMessageText(message) {
|
|
398
|
+
if (typeof message.content === "string") {
|
|
399
|
+
return message.content;
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(message.content)) {
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
return message.content.map((part) => typeof part.text === "string" ? part.text : part.image_url ? "[image]" : "").filter(Boolean).join("\n");
|
|
405
|
+
}
|
|
406
|
+
function countRoles(messages) {
|
|
407
|
+
const counts = {};
|
|
408
|
+
for (const message of messages) {
|
|
409
|
+
const role = message.role ?? "user";
|
|
410
|
+
counts[role] = (counts[role] ?? 0) + 1;
|
|
411
|
+
}
|
|
412
|
+
return counts;
|
|
413
|
+
}
|
|
414
|
+
function summarizeRecentMessages(messages) {
|
|
415
|
+
return messages.slice(-8).map((message) => ({
|
|
416
|
+
role: message.role ?? "user",
|
|
417
|
+
textPreview: truncateForLog(extractChatMessageText(message), 180),
|
|
418
|
+
toolCallId: message.tool_call_id
|
|
280
419
|
}));
|
|
281
420
|
}
|
|
421
|
+
function summarizeToolNames(tools) {
|
|
422
|
+
if (!tools) {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
return tools.map((tool) => {
|
|
426
|
+
if (!tool || typeof tool !== "object") {
|
|
427
|
+
return "";
|
|
428
|
+
}
|
|
429
|
+
const record = tool;
|
|
430
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
431
|
+
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : "";
|
|
432
|
+
}).filter(Boolean);
|
|
433
|
+
}
|
|
434
|
+
function summarizeResponsesRequest(data) {
|
|
435
|
+
const input = data.input;
|
|
436
|
+
const toolNames = summarizeToolNames(Array.isArray(data.tools) ? data.tools : void 0);
|
|
437
|
+
return {
|
|
438
|
+
endpoint: "/v1/responses",
|
|
439
|
+
model: data.model ?? "default",
|
|
440
|
+
stream: data.stream ?? false,
|
|
441
|
+
inputKind: typeof input === "string" ? "string" : Array.isArray(input) ? "array" : "override",
|
|
442
|
+
inputItems: Array.isArray(input) ? input.length : void 0,
|
|
443
|
+
inputTextPreview: typeof input === "string" ? truncateForLog(input) : "",
|
|
444
|
+
instructionsLength: typeof data.instructions === "string" ? data.instructions.length : void 0,
|
|
445
|
+
toolCount: Array.isArray(data.tools) ? data.tools.length : 0,
|
|
446
|
+
toolNames: toolNames.slice(0, 50),
|
|
447
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
448
|
+
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
449
|
+
parallelToolCalls: data.parallel_tool_calls,
|
|
450
|
+
hasReasoning: Boolean(data.reasoning)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function createResponsesCodexBody(data) {
|
|
454
|
+
const experimentalBody = data.experimental_codex?.body ?? {};
|
|
455
|
+
const body = {
|
|
456
|
+
...experimentalBody,
|
|
457
|
+
...data
|
|
458
|
+
};
|
|
459
|
+
delete body.experimental_codex;
|
|
460
|
+
const normalizedInput = normalizeResponseInput(data.input);
|
|
461
|
+
if (typeof normalizedInput !== "undefined") {
|
|
462
|
+
body.input = normalizedInput;
|
|
463
|
+
}
|
|
464
|
+
return body;
|
|
465
|
+
}
|
|
466
|
+
function createCodexPassthroughBody(data, model) {
|
|
467
|
+
const body = {
|
|
468
|
+
...data,
|
|
469
|
+
model
|
|
470
|
+
};
|
|
471
|
+
delete body.experimental_codex;
|
|
472
|
+
return body;
|
|
473
|
+
}
|
|
474
|
+
function summarizeChatCompletionsRequest(data) {
|
|
475
|
+
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
476
|
+
const toolNames = summarizeToolNames(data.tools);
|
|
477
|
+
return {
|
|
478
|
+
endpoint: "/v1/chat/completions",
|
|
479
|
+
model: data.model ?? "default",
|
|
480
|
+
stream: data.stream ?? false,
|
|
481
|
+
messageCount: data.messages.length,
|
|
482
|
+
roleCounts: countRoles(data.messages),
|
|
483
|
+
recentMessages: summarizeRecentMessages(data.messages),
|
|
484
|
+
lastUserTextPreview: lastUserMessage ? truncateForLog(extractChatMessageText(lastUserMessage)) : "",
|
|
485
|
+
toolCount: data.tools?.length ?? 0,
|
|
486
|
+
toolNames: toolNames.slice(0, 50),
|
|
487
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
488
|
+
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
489
|
+
parallelToolCalls: data.parallel_tool_calls,
|
|
490
|
+
hasReasoning: Boolean(data.reasoning || data.reasoning_effort),
|
|
491
|
+
maxTokens: data.max_completion_tokens ?? data.max_tokens
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function summarizeCodexChatBody(body) {
|
|
495
|
+
const toolNames = summarizeToolNames(Array.isArray(body.tools) ? body.tools : void 0);
|
|
496
|
+
return {
|
|
497
|
+
keys: Object.keys(body).sort(),
|
|
498
|
+
model: body.model ?? "default",
|
|
499
|
+
stream: body.stream,
|
|
500
|
+
store: body.store,
|
|
501
|
+
inputItems: Array.isArray(body.input) ? body.input.length : void 0,
|
|
502
|
+
tools: Array.isArray(body.tools) ? body.tools.length : void 0,
|
|
503
|
+
toolNames: toolNames.slice(0, 50),
|
|
504
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
505
|
+
toolChoice: typeof body.tool_choice === "undefined" ? "default" : typeof body.tool_choice,
|
|
506
|
+
parallelToolCalls: body.parallel_tool_calls,
|
|
507
|
+
hasReasoning: Boolean(body.reasoning)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function profileLogLabel(profile) {
|
|
511
|
+
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
512
|
+
}
|
|
513
|
+
function requestSourceFromUserAgent(userAgent) {
|
|
514
|
+
if (typeof userAgent !== "string") {
|
|
515
|
+
return "API";
|
|
516
|
+
}
|
|
517
|
+
const normalized = userAgent.toLowerCase();
|
|
518
|
+
if (normalized.includes("codex")) {
|
|
519
|
+
return "Codex";
|
|
520
|
+
}
|
|
521
|
+
if (normalized.includes("openclaw")) {
|
|
522
|
+
return "OpenClaw";
|
|
523
|
+
}
|
|
524
|
+
return "API";
|
|
525
|
+
}
|
|
282
526
|
function createChatCompletionsCodexBody(data) {
|
|
283
|
-
const body =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
body.
|
|
290
|
-
}
|
|
291
|
-
|
|
527
|
+
const body = {
|
|
528
|
+
store: false,
|
|
529
|
+
stream: true,
|
|
530
|
+
input: normalizeChatMessages(data.messages)
|
|
531
|
+
};
|
|
532
|
+
if (data.model) {
|
|
533
|
+
body.model = data.model;
|
|
534
|
+
}
|
|
535
|
+
if (typeof data.parallel_tool_calls === "boolean") {
|
|
536
|
+
body.parallel_tool_calls = data.parallel_tool_calls;
|
|
537
|
+
}
|
|
538
|
+
if (data.tools) {
|
|
539
|
+
body.tools = normalizeChatTools(data.tools);
|
|
540
|
+
}
|
|
541
|
+
if (typeof data.tool_choice !== "undefined") {
|
|
542
|
+
body.tool_choice = normalizeChatToolChoice(data.tool_choice);
|
|
543
|
+
}
|
|
544
|
+
const reasoning = normalizeChatReasoning(data);
|
|
545
|
+
if (reasoning) {
|
|
546
|
+
body.reasoning = reasoning;
|
|
547
|
+
}
|
|
292
548
|
return body;
|
|
293
549
|
}
|
|
294
550
|
function summarizeImageRequestForLog(body) {
|
|
@@ -373,6 +629,7 @@ function buildResponseApiBody(result, includeRaw) {
|
|
|
373
629
|
return responseBody;
|
|
374
630
|
}
|
|
375
631
|
function buildChatCompletionsBody(result) {
|
|
632
|
+
const hasToolCalls = result.toolCalls.length > 0;
|
|
376
633
|
const body = {
|
|
377
634
|
id: `chatcmpl_${randomUUID().replace(/-/g, "")}`,
|
|
378
635
|
object: "chat.completion",
|
|
@@ -381,10 +638,11 @@ function buildChatCompletionsBody(result) {
|
|
|
381
638
|
choices: [
|
|
382
639
|
{
|
|
383
640
|
index: 0,
|
|
384
|
-
finish_reason: "stop",
|
|
641
|
+
finish_reason: hasToolCalls ? "tool_calls" : "stop",
|
|
385
642
|
message: {
|
|
386
643
|
role: "assistant",
|
|
387
|
-
content: result.text
|
|
644
|
+
content: hasToolCalls ? result.text || null : result.text,
|
|
645
|
+
...hasToolCalls ? { tool_calls: result.toolCalls } : {}
|
|
388
646
|
}
|
|
389
647
|
}
|
|
390
648
|
]
|
|
@@ -394,6 +652,79 @@ function buildChatCompletionsBody(result) {
|
|
|
394
652
|
}
|
|
395
653
|
return body;
|
|
396
654
|
}
|
|
655
|
+
function writeChatCompletionsSseEvent(reply, data) {
|
|
656
|
+
reply.raw.write(`data: ${JSON.stringify(data)}
|
|
657
|
+
|
|
658
|
+
`);
|
|
659
|
+
}
|
|
660
|
+
function buildChatCompletionChunk(params) {
|
|
661
|
+
return {
|
|
662
|
+
id: params.id,
|
|
663
|
+
object: "chat.completion.chunk",
|
|
664
|
+
created: params.created,
|
|
665
|
+
model: params.model,
|
|
666
|
+
choices: [
|
|
667
|
+
{
|
|
668
|
+
index: 0,
|
|
669
|
+
delta: params.delta,
|
|
670
|
+
finish_reason: params.finishReason ?? null
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function sendChatCompletionsStream(reply, result) {
|
|
676
|
+
const id = `chatcmpl_${randomUUID().replace(/-/g, "")}`;
|
|
677
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
678
|
+
reply.raw.writeHead(200, {
|
|
679
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
680
|
+
"Cache-Control": "no-cache, no-transform",
|
|
681
|
+
Connection: "keep-alive",
|
|
682
|
+
"X-Accel-Buffering": "no"
|
|
683
|
+
});
|
|
684
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
685
|
+
id,
|
|
686
|
+
created,
|
|
687
|
+
model: result.model,
|
|
688
|
+
delta: { role: "assistant" }
|
|
689
|
+
}));
|
|
690
|
+
if (result.text) {
|
|
691
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
692
|
+
id,
|
|
693
|
+
created,
|
|
694
|
+
model: result.model,
|
|
695
|
+
delta: { content: result.text }
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
result.toolCalls.forEach((toolCall, index) => {
|
|
699
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
700
|
+
id,
|
|
701
|
+
created,
|
|
702
|
+
model: result.model,
|
|
703
|
+
delta: {
|
|
704
|
+
tool_calls: [
|
|
705
|
+
{
|
|
706
|
+
index,
|
|
707
|
+
id: toolCall.id,
|
|
708
|
+
type: toolCall.type,
|
|
709
|
+
function: {
|
|
710
|
+
name: toolCall.function.name,
|
|
711
|
+
arguments: toolCall.function.arguments
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
}
|
|
716
|
+
}));
|
|
717
|
+
});
|
|
718
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
719
|
+
id,
|
|
720
|
+
created,
|
|
721
|
+
model: result.model,
|
|
722
|
+
delta: {},
|
|
723
|
+
finishReason: result.toolCalls.length > 0 ? "tool_calls" : "stop"
|
|
724
|
+
}));
|
|
725
|
+
reply.raw.write("data: [DONE]\n\n");
|
|
726
|
+
reply.raw.end();
|
|
727
|
+
}
|
|
397
728
|
function validateImageRequest(data) {
|
|
398
729
|
if (data.response_format === "url") {
|
|
399
730
|
return "\u5F53\u524D\u7F51\u5173\u4EC5\u652F\u6301 response_format=b64_json\uFF0C\u6682\u4E0D\u652F\u6301\u8FD4\u56DE\u6258\u7BA1\u56FE\u7247 URL\u3002";
|
|
@@ -484,7 +815,7 @@ function getErrorStatusCode(error) {
|
|
|
484
815
|
return normalized.statusCode;
|
|
485
816
|
}
|
|
486
817
|
const upstreamStatus = normalized.upstreamStatus;
|
|
487
|
-
if (upstreamStatus ===
|
|
818
|
+
if (typeof upstreamStatus === "number" && upstreamStatus >= 400 && upstreamStatus < 600) {
|
|
488
819
|
return upstreamStatus;
|
|
489
820
|
}
|
|
490
821
|
const message = normalized.message;
|
|
@@ -496,12 +827,74 @@ function getErrorStatusCode(error) {
|
|
|
496
827
|
}
|
|
497
828
|
return 500;
|
|
498
829
|
}
|
|
830
|
+
function isQuotaLimitError(error) {
|
|
831
|
+
const normalized = normalizeError(error);
|
|
832
|
+
const marker = `${normalized.upstreamErrorCode ?? ""} ${normalized.upstreamErrorType ?? ""} ${normalized.message}`.toLowerCase();
|
|
833
|
+
return normalized.upstreamStatus === 429 || marker.includes("usage_limit_reached");
|
|
834
|
+
}
|
|
835
|
+
function createSseStreamStats() {
|
|
836
|
+
return {
|
|
837
|
+
buffer: "",
|
|
838
|
+
bytes: 0,
|
|
839
|
+
completed: false
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function trackSseChunk(stats, chunk) {
|
|
843
|
+
const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : String(chunk);
|
|
844
|
+
stats.bytes += Buffer.byteLength(text);
|
|
845
|
+
stats.buffer += text.replace(/\r\n/g, "\n");
|
|
846
|
+
let separatorIndex = stats.buffer.indexOf("\n\n");
|
|
847
|
+
while (separatorIndex !== -1) {
|
|
848
|
+
const block = stats.buffer.slice(0, separatorIndex);
|
|
849
|
+
stats.buffer = stats.buffer.slice(separatorIndex + 2);
|
|
850
|
+
const eventName = block.split("\n").find((line) => line.startsWith("event:"))?.slice("event:".length).trim();
|
|
851
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trim()).join("\n");
|
|
852
|
+
let eventType = eventName;
|
|
853
|
+
if (data && data !== "[DONE]") {
|
|
854
|
+
try {
|
|
855
|
+
const parsed = JSON.parse(data);
|
|
856
|
+
if (typeof parsed.type === "string") {
|
|
857
|
+
eventType = parsed.type;
|
|
858
|
+
}
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (eventType === "response.completed") {
|
|
863
|
+
stats.completed = true;
|
|
864
|
+
stats.terminalEvent = eventType;
|
|
865
|
+
} else if (eventType === "response.failed" || eventType === "response.incomplete") {
|
|
866
|
+
stats.terminalEvent = eventType;
|
|
867
|
+
}
|
|
868
|
+
separatorIndex = stats.buffer.indexOf("\n\n");
|
|
869
|
+
}
|
|
870
|
+
if (stats.buffer.length > 65536) {
|
|
871
|
+
stats.buffer = stats.buffer.slice(-65536);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
499
874
|
function createApp(params) {
|
|
500
875
|
const app = Fastify({
|
|
501
876
|
logger: false,
|
|
502
877
|
bodyLimit: params?.bodyLimit
|
|
503
878
|
});
|
|
504
879
|
const ctx = createGatewayContext();
|
|
880
|
+
const gatewayRequestLogs = [];
|
|
881
|
+
function pushGatewayRequestLog(log) {
|
|
882
|
+
gatewayRequestLogs.unshift({
|
|
883
|
+
id: log.id ?? randomUUID(),
|
|
884
|
+
time: log.time ?? Date.now(),
|
|
885
|
+
method: log.method,
|
|
886
|
+
endpoint: log.endpoint,
|
|
887
|
+
account: log.account,
|
|
888
|
+
model: log.model,
|
|
889
|
+
statusCode: log.statusCode,
|
|
890
|
+
durationMs: log.durationMs,
|
|
891
|
+
source: log.source,
|
|
892
|
+
details: log.details
|
|
893
|
+
});
|
|
894
|
+
if (gatewayRequestLogs.length > MAX_GATEWAY_REQUEST_LOGS) {
|
|
895
|
+
gatewayRequestLogs.length = MAX_GATEWAY_REQUEST_LOGS;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
505
898
|
void app.register(cors, {
|
|
506
899
|
origin: params?.corsOrigin ?? true,
|
|
507
900
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
@@ -524,6 +917,9 @@ function createApp(params) {
|
|
|
524
917
|
}
|
|
525
918
|
};
|
|
526
919
|
});
|
|
920
|
+
app.get("/_gateway/admin/request-logs", async () => ({
|
|
921
|
+
data: gatewayRequestLogs
|
|
922
|
+
}));
|
|
527
923
|
async function buildAdminConfig(request) {
|
|
528
924
|
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
529
925
|
ctx.authService.getStatus(),
|
|
@@ -547,7 +943,9 @@ function createApp(params) {
|
|
|
547
943
|
codex: codexStatus,
|
|
548
944
|
adminUrl: `${origin}/`,
|
|
549
945
|
baseUrl: `${origin}/v1`,
|
|
946
|
+
codexBaseUrl: `${origin}/codex/v1`,
|
|
550
947
|
restartSupported: Boolean(params?.onRestart),
|
|
948
|
+
codexRestartSupported: Boolean(params?.onRestartCodex),
|
|
551
949
|
supportedEndpoints: [
|
|
552
950
|
{
|
|
553
951
|
method: "GET",
|
|
@@ -559,6 +957,11 @@ function createApp(params) {
|
|
|
559
957
|
path: "/v1/responses",
|
|
560
958
|
description: "OpenAI responses \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
561
959
|
},
|
|
960
|
+
{
|
|
961
|
+
method: "POST",
|
|
962
|
+
path: "/codex/v1/responses",
|
|
963
|
+
description: "Codex custom provider \u4E13\u7528 Responses SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
964
|
+
},
|
|
562
965
|
{
|
|
563
966
|
method: "POST",
|
|
564
967
|
path: "/v1/chat/completions",
|
|
@@ -805,6 +1208,45 @@ function createApp(params) {
|
|
|
805
1208
|
config: await buildAdminConfig(request)
|
|
806
1209
|
};
|
|
807
1210
|
});
|
|
1211
|
+
app.post("/_gateway/admin/codex/configure-provider", async (request, reply) => {
|
|
1212
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1213
|
+
if (!parsed.success) {
|
|
1214
|
+
reply.code(400);
|
|
1215
|
+
return {
|
|
1216
|
+
error: {
|
|
1217
|
+
type: "validation_error",
|
|
1218
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
const origin = resolveOrigin(request);
|
|
1223
|
+
const baseUrl = parsed.data.baseUrl ?? `${origin}/codex/v1`;
|
|
1224
|
+
return {
|
|
1225
|
+
codexProvider: await ctx.authService.applyGatewayToCodexProvider({
|
|
1226
|
+
baseUrl,
|
|
1227
|
+
providerId: parsed.data.providerId
|
|
1228
|
+
}),
|
|
1229
|
+
config: await buildAdminConfig(request)
|
|
1230
|
+
};
|
|
1231
|
+
});
|
|
1232
|
+
app.post("/_gateway/admin/codex/remove-provider", async (request, reply) => {
|
|
1233
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1234
|
+
if (!parsed.success) {
|
|
1235
|
+
reply.code(400);
|
|
1236
|
+
return {
|
|
1237
|
+
error: {
|
|
1238
|
+
type: "validation_error",
|
|
1239
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
codexProvider: await ctx.authService.removeGatewayFromCodexProvider({
|
|
1245
|
+
providerId: parsed.data.providerId
|
|
1246
|
+
}),
|
|
1247
|
+
config: await buildAdminConfig(request)
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
808
1250
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
809
1251
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
810
1252
|
if (!parsed.success) {
|
|
@@ -839,6 +1281,22 @@ function createApp(params) {
|
|
|
839
1281
|
restarting: true
|
|
840
1282
|
};
|
|
841
1283
|
});
|
|
1284
|
+
app.post("/_gateway/admin/desktop/restart-codex", async (_request, reply) => {
|
|
1285
|
+
if (!params?.onRestartCodex) {
|
|
1286
|
+
reply.code(501);
|
|
1287
|
+
return {
|
|
1288
|
+
error: {
|
|
1289
|
+
type: "not_supported",
|
|
1290
|
+
message: "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u91CD\u542F Codex\u3002"
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
await params.onRestartCodex();
|
|
1295
|
+
return {
|
|
1296
|
+
ok: true,
|
|
1297
|
+
restarted: true
|
|
1298
|
+
};
|
|
1299
|
+
});
|
|
842
1300
|
app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
|
|
843
1301
|
const parsed = proxyTestSchema.safeParse(request.body);
|
|
844
1302
|
if (!parsed.success) {
|
|
@@ -959,9 +1417,171 @@ function createApp(params) {
|
|
|
959
1417
|
owned_by: model.provider
|
|
960
1418
|
}))
|
|
961
1419
|
}));
|
|
962
|
-
|
|
1420
|
+
async function handleCodexResponsesPassthrough(request, reply, data, startedAt) {
|
|
1421
|
+
const abortController = new AbortController();
|
|
1422
|
+
let streamFinished = false;
|
|
1423
|
+
let headersCommitted = false;
|
|
1424
|
+
let profile = null;
|
|
1425
|
+
let retryCount = 0;
|
|
1426
|
+
let failureRecorded = false;
|
|
1427
|
+
reply.raw.on("close", () => {
|
|
1428
|
+
if (!streamFinished) {
|
|
1429
|
+
abortController.abort();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
try {
|
|
1433
|
+
const model = await ctx.modelService.resolveModel("openai-codex", data.model, {
|
|
1434
|
+
allowUnknown: data.experimental_codex?.allow_unknown_model
|
|
1435
|
+
});
|
|
1436
|
+
const codexBody = createCodexPassthroughBody(data, model);
|
|
1437
|
+
let upstream = null;
|
|
1438
|
+
const maxProfileAttempts = 5;
|
|
1439
|
+
for (let attempt = 0; attempt < maxProfileAttempts; attempt += 1) {
|
|
1440
|
+
profile = await ctx.authService.requireUsableProfile("openai-codex");
|
|
1441
|
+
try {
|
|
1442
|
+
upstream = await streamOpenAICodex({
|
|
1443
|
+
profile,
|
|
1444
|
+
model,
|
|
1445
|
+
bodyOverride: codexBody,
|
|
1446
|
+
passthroughBody: true,
|
|
1447
|
+
signal: abortController.signal
|
|
1448
|
+
});
|
|
1449
|
+
break;
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
const quota = error.quota;
|
|
1452
|
+
const switchedProfile = await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1453
|
+
failureRecorded = true;
|
|
1454
|
+
if (attempt < maxProfileAttempts - 1 && isQuotaLimitError(error) && switchedProfile && switchedProfile.profileId !== profile.profileId && !abortController.signal.aborted) {
|
|
1455
|
+
retryCount += 1;
|
|
1456
|
+
failureRecorded = false;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
throw error;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (!upstream || !profile) {
|
|
1463
|
+
throw new Error("Codex stream \u672A\u80FD\u5EFA\u7ACB\u3002");
|
|
1464
|
+
}
|
|
1465
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, upstream.quota, "openai-codex");
|
|
1466
|
+
const headers = {
|
|
1467
|
+
"Content-Type": upstream.headers["content-type"] ?? "text/event-stream; charset=utf-8",
|
|
1468
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1469
|
+
Connection: "keep-alive",
|
|
1470
|
+
"X-Accel-Buffering": "no"
|
|
1471
|
+
};
|
|
1472
|
+
for (const [key, value] of Object.entries(upstream.headers)) {
|
|
1473
|
+
if (key.startsWith("x-codex-") || key === "x-request-id") {
|
|
1474
|
+
headers[key] = value;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
reply.raw.writeHead(upstream.status, headers);
|
|
1478
|
+
headersCommitted = true;
|
|
1479
|
+
reply.raw.flushHeaders?.();
|
|
1480
|
+
const streamStats = createSseStreamStats();
|
|
1481
|
+
for await (const chunk of Readable.fromWeb(upstream.body)) {
|
|
1482
|
+
trackSseChunk(streamStats, chunk);
|
|
1483
|
+
if (!reply.raw.write(chunk)) {
|
|
1484
|
+
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
streamFinished = true;
|
|
1488
|
+
reply.raw.end();
|
|
1489
|
+
if (!streamStats.completed) {
|
|
1490
|
+
console.warn("[gateway:codex:stream] upstream stream ended without response.completed", {
|
|
1491
|
+
requestId: request.id,
|
|
1492
|
+
upstreamRequestId: upstream.requestId,
|
|
1493
|
+
account: profileLogLabel(profile),
|
|
1494
|
+
model,
|
|
1495
|
+
bytes: streamStats.bytes,
|
|
1496
|
+
terminalEvent: streamStats.terminalEvent
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
pushGatewayRequestLog({
|
|
1500
|
+
method: request.method,
|
|
1501
|
+
endpoint: request.url,
|
|
1502
|
+
account: profileLogLabel(profile),
|
|
1503
|
+
model,
|
|
1504
|
+
statusCode: upstream.status,
|
|
1505
|
+
durationMs: performance.now() - startedAt,
|
|
1506
|
+
source: "Codex",
|
|
1507
|
+
details: {
|
|
1508
|
+
requestId: request.id,
|
|
1509
|
+
upstreamRequestId: upstream.requestId,
|
|
1510
|
+
remoteAddress: request.ip,
|
|
1511
|
+
userAgent: request.headers["user-agent"],
|
|
1512
|
+
request: summarizeResponsesRequest(data),
|
|
1513
|
+
response: {
|
|
1514
|
+
stream: true,
|
|
1515
|
+
passthrough: true,
|
|
1516
|
+
retryCount,
|
|
1517
|
+
completed: streamStats.completed,
|
|
1518
|
+
terminalEvent: streamStats.terminalEvent,
|
|
1519
|
+
bytes: streamStats.bytes
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
return reply;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const quota = error.quota;
|
|
1526
|
+
if (profile && !failureRecorded) {
|
|
1527
|
+
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1528
|
+
}
|
|
1529
|
+
const normalized = normalizeError(error);
|
|
1530
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
1531
|
+
pushGatewayRequestLog({
|
|
1532
|
+
method: request.method,
|
|
1533
|
+
endpoint: request.url,
|
|
1534
|
+
account: profileLogLabel(profile),
|
|
1535
|
+
model: data.model ?? "default",
|
|
1536
|
+
statusCode,
|
|
1537
|
+
durationMs: performance.now() - startedAt,
|
|
1538
|
+
source: "Codex",
|
|
1539
|
+
details: {
|
|
1540
|
+
requestId: request.id,
|
|
1541
|
+
remoteAddress: request.ip,
|
|
1542
|
+
userAgent: request.headers["user-agent"],
|
|
1543
|
+
request: summarizeResponsesRequest(data),
|
|
1544
|
+
response: {
|
|
1545
|
+
retryCount
|
|
1546
|
+
},
|
|
1547
|
+
error: {
|
|
1548
|
+
message: normalized.message,
|
|
1549
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
1550
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1551
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
if (headersCommitted) {
|
|
1556
|
+
streamFinished = true;
|
|
1557
|
+
reply.raw.end();
|
|
1558
|
+
return reply;
|
|
1559
|
+
}
|
|
1560
|
+
throw error;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
app.post("/codex/v1/responses", async (request, reply) => {
|
|
1564
|
+
const startedAt = performance.now();
|
|
963
1565
|
const parsed = responsesBodySchema.safeParse(request.body);
|
|
964
1566
|
if (!parsed.success) {
|
|
1567
|
+
pushGatewayRequestLog({
|
|
1568
|
+
method: request.method,
|
|
1569
|
+
endpoint: request.url,
|
|
1570
|
+
account: "-",
|
|
1571
|
+
model: "-",
|
|
1572
|
+
statusCode: 400,
|
|
1573
|
+
durationMs: performance.now() - startedAt,
|
|
1574
|
+
source: "Codex",
|
|
1575
|
+
details: {
|
|
1576
|
+
requestId: request.id,
|
|
1577
|
+
remoteAddress: request.ip,
|
|
1578
|
+
userAgent: request.headers["user-agent"],
|
|
1579
|
+
error: {
|
|
1580
|
+
type: "validation_error",
|
|
1581
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
965
1585
|
reply.code(400);
|
|
966
1586
|
return {
|
|
967
1587
|
error: {
|
|
@@ -970,18 +1590,61 @@ function createApp(params) {
|
|
|
970
1590
|
}
|
|
971
1591
|
};
|
|
972
1592
|
}
|
|
973
|
-
|
|
974
|
-
|
|
1593
|
+
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt);
|
|
1594
|
+
});
|
|
1595
|
+
app.post("/v1/responses", async (request, reply) => {
|
|
1596
|
+
const startedAt = performance.now();
|
|
1597
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
1598
|
+
if (!parsed.success) {
|
|
1599
|
+
pushGatewayRequestLog({
|
|
1600
|
+
method: request.method,
|
|
1601
|
+
endpoint: request.url,
|
|
1602
|
+
account: "-",
|
|
1603
|
+
model: "-",
|
|
1604
|
+
statusCode: 400,
|
|
1605
|
+
durationMs: performance.now() - startedAt,
|
|
1606
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1607
|
+
details: {
|
|
1608
|
+
requestId: request.id,
|
|
1609
|
+
remoteAddress: request.ip,
|
|
1610
|
+
userAgent: request.headers["user-agent"],
|
|
1611
|
+
error: {
|
|
1612
|
+
type: "validation_error",
|
|
1613
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
reply.code(400);
|
|
975
1618
|
return {
|
|
976
1619
|
error: {
|
|
977
|
-
type: "
|
|
978
|
-
message: "\
|
|
1620
|
+
type: "validation_error",
|
|
1621
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
979
1622
|
}
|
|
980
1623
|
};
|
|
981
1624
|
}
|
|
1625
|
+
const wantsEventStream = typeof request.headers.accept === "string" && request.headers.accept.toLowerCase().includes("text/event-stream");
|
|
982
1626
|
const input = extractTextInput(parsed.data.input);
|
|
983
1627
|
const hasInput = typeof parsed.data.input !== "undefined" || typeof parsed.data.experimental_codex?.body?.input !== "undefined";
|
|
984
1628
|
if (!hasInput) {
|
|
1629
|
+
pushGatewayRequestLog({
|
|
1630
|
+
method: request.method,
|
|
1631
|
+
endpoint: request.url,
|
|
1632
|
+
account: "-",
|
|
1633
|
+
model: parsed.data.model ?? "default",
|
|
1634
|
+
statusCode: 400,
|
|
1635
|
+
durationMs: performance.now() - startedAt,
|
|
1636
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1637
|
+
details: {
|
|
1638
|
+
requestId: request.id,
|
|
1639
|
+
remoteAddress: request.ip,
|
|
1640
|
+
userAgent: request.headers["user-agent"],
|
|
1641
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
1642
|
+
error: {
|
|
1643
|
+
type: "validation_error",
|
|
1644
|
+
message: "\u6CA1\u6709\u63D0\u4F9B input\uFF0C\u4E5F\u6CA1\u6709\u5728 experimental_codex.body \u91CC\u900F\u4F20 input"
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
985
1648
|
reply.code(400);
|
|
986
1649
|
return {
|
|
987
1650
|
error: {
|
|
@@ -990,34 +1653,35 @@ function createApp(params) {
|
|
|
990
1653
|
}
|
|
991
1654
|
};
|
|
992
1655
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1656
|
+
if (parsed.data.stream || wantsEventStream) {
|
|
1657
|
+
pushGatewayRequestLog({
|
|
1658
|
+
method: request.method,
|
|
1659
|
+
endpoint: request.url,
|
|
1660
|
+
account: "-",
|
|
1661
|
+
model: parsed.data.model ?? "default",
|
|
1662
|
+
statusCode: 501,
|
|
1663
|
+
durationMs: performance.now() - startedAt,
|
|
1664
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1665
|
+
details: {
|
|
1666
|
+
requestId: request.id,
|
|
1667
|
+
remoteAddress: request.ip,
|
|
1668
|
+
userAgent: request.headers["user-agent"],
|
|
1669
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
1670
|
+
error: {
|
|
1671
|
+
type: "not_supported",
|
|
1672
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
reply.code(501);
|
|
1677
|
+
return {
|
|
1678
|
+
error: {
|
|
1679
|
+
type: "not_supported",
|
|
1680
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1020
1683
|
}
|
|
1684
|
+
const codexBody = createResponsesCodexBody(parsed.data);
|
|
1021
1685
|
const result = await ctx.chatService.chat({
|
|
1022
1686
|
model: parsed.data.model,
|
|
1023
1687
|
input: input || void 0,
|
|
@@ -1030,8 +1694,27 @@ function createApp(params) {
|
|
|
1030
1694
|
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
1031
1695
|
});
|
|
1032
1696
|
app.post("/v1/chat/completions", async (request, reply) => {
|
|
1697
|
+
const startedAt = performance.now();
|
|
1033
1698
|
const parsed = chatCompletionsBodySchema.safeParse(request.body);
|
|
1034
1699
|
if (!parsed.success) {
|
|
1700
|
+
pushGatewayRequestLog({
|
|
1701
|
+
method: request.method,
|
|
1702
|
+
endpoint: request.url,
|
|
1703
|
+
account: "-",
|
|
1704
|
+
model: "-",
|
|
1705
|
+
statusCode: 400,
|
|
1706
|
+
durationMs: performance.now() - startedAt,
|
|
1707
|
+
source: "API",
|
|
1708
|
+
details: {
|
|
1709
|
+
requestId: request.id,
|
|
1710
|
+
remoteAddress: request.ip,
|
|
1711
|
+
userAgent: request.headers["user-agent"],
|
|
1712
|
+
error: {
|
|
1713
|
+
type: "validation_error",
|
|
1714
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1035
1718
|
reply.code(400);
|
|
1036
1719
|
return {
|
|
1037
1720
|
error: {
|
|
@@ -1040,16 +1723,24 @@ function createApp(params) {
|
|
|
1040
1723
|
}
|
|
1041
1724
|
};
|
|
1042
1725
|
}
|
|
1043
|
-
if (parsed.data.stream) {
|
|
1044
|
-
reply.code(501);
|
|
1045
|
-
return {
|
|
1046
|
-
error: {
|
|
1047
|
-
type: "not_supported",
|
|
1048
|
-
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 chat.completions \u7684 stream=true"
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
1726
|
if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
|
|
1727
|
+
pushGatewayRequestLog({
|
|
1728
|
+
method: request.method,
|
|
1729
|
+
endpoint: request.url,
|
|
1730
|
+
account: "-",
|
|
1731
|
+
model: parsed.data.model ?? "default",
|
|
1732
|
+
statusCode: 501,
|
|
1733
|
+
durationMs: performance.now() - startedAt,
|
|
1734
|
+
source: "API",
|
|
1735
|
+
details: {
|
|
1736
|
+
requestId: request.id,
|
|
1737
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1738
|
+
error: {
|
|
1739
|
+
type: "not_supported",
|
|
1740
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301\u4E00\u6B21\u8FD4\u56DE\u591A\u4E2A choices\uFF08n > 1\uFF09"
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1053
1744
|
reply.code(501);
|
|
1054
1745
|
return {
|
|
1055
1746
|
error: {
|
|
@@ -1059,16 +1750,93 @@ function createApp(params) {
|
|
|
1059
1750
|
};
|
|
1060
1751
|
}
|
|
1061
1752
|
const codexBody = createChatCompletionsCodexBody(parsed.data);
|
|
1753
|
+
console.info("[gateway:chat:request]", {
|
|
1754
|
+
requestId: request.id,
|
|
1755
|
+
remoteAddress: request.ip,
|
|
1756
|
+
userAgent: request.headers["user-agent"],
|
|
1757
|
+
...summarizeChatCompletionsRequest(parsed.data),
|
|
1758
|
+
codex: summarizeCodexChatBody(codexBody)
|
|
1759
|
+
});
|
|
1062
1760
|
const fallbackInput = parsed.data.messages.map(
|
|
1063
1761
|
(message) => typeof message.content === "string" ? message.content : (message.content ?? []).map((part) => typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n")
|
|
1064
1762
|
).filter(Boolean).join("\n").trim();
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1763
|
+
let result;
|
|
1764
|
+
try {
|
|
1765
|
+
result = await ctx.chatService.chat({
|
|
1766
|
+
model: parsed.data.model,
|
|
1767
|
+
input: fallbackInput || void 0,
|
|
1768
|
+
experimental: {
|
|
1769
|
+
codexBody
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
const normalized = normalizeError(error);
|
|
1774
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
1775
|
+
pushGatewayRequestLog({
|
|
1776
|
+
method: request.method,
|
|
1777
|
+
endpoint: request.url,
|
|
1778
|
+
account: profileLogLabel(await ctx.authService.getActiveProfile()),
|
|
1779
|
+
model: parsed.data.model ?? "default",
|
|
1780
|
+
statusCode,
|
|
1781
|
+
durationMs: performance.now() - startedAt,
|
|
1782
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1783
|
+
details: {
|
|
1784
|
+
requestId: request.id,
|
|
1785
|
+
remoteAddress: request.ip,
|
|
1786
|
+
userAgent: request.headers["user-agent"],
|
|
1787
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1788
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
1789
|
+
error: {
|
|
1790
|
+
message: normalized.message,
|
|
1791
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
1792
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1793
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
throw error;
|
|
1798
|
+
}
|
|
1799
|
+
pushGatewayRequestLog({
|
|
1800
|
+
method: request.method,
|
|
1801
|
+
endpoint: request.url,
|
|
1802
|
+
account: profileLogLabel(await ctx.authService.getActiveProfile()),
|
|
1803
|
+
model: result.model,
|
|
1804
|
+
statusCode: 200,
|
|
1805
|
+
durationMs: performance.now() - startedAt,
|
|
1806
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1807
|
+
details: {
|
|
1808
|
+
requestId: request.id,
|
|
1809
|
+
remoteAddress: request.ip,
|
|
1810
|
+
userAgent: request.headers["user-agent"],
|
|
1811
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1812
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
1813
|
+
response: {
|
|
1814
|
+
textPreview: truncateForLog(result.text),
|
|
1815
|
+
textLength: result.text.length,
|
|
1816
|
+
toolCallCount: result.toolCalls.length,
|
|
1817
|
+
toolCalls: result.toolCalls.map((toolCall) => ({
|
|
1818
|
+
id: toolCall.id,
|
|
1819
|
+
name: toolCall.function.name,
|
|
1820
|
+
argumentsPreview: truncateForLog(toolCall.function.arguments)
|
|
1821
|
+
})),
|
|
1822
|
+
artifactCount: result.artifacts.length,
|
|
1823
|
+
stream: parsed.data.stream ?? false
|
|
1824
|
+
}
|
|
1070
1825
|
}
|
|
1071
1826
|
});
|
|
1827
|
+
console.info("[gateway:chat:response]", {
|
|
1828
|
+
requestId: request.id,
|
|
1829
|
+
model: result.model,
|
|
1830
|
+
stream: parsed.data.stream ?? false,
|
|
1831
|
+
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
1832
|
+
textLength: result.text.length,
|
|
1833
|
+
toolCallCount: result.toolCalls.length,
|
|
1834
|
+
artifactCount: result.artifacts.length
|
|
1835
|
+
});
|
|
1836
|
+
if (parsed.data.stream) {
|
|
1837
|
+
sendChatCompletionsStream(reply, result);
|
|
1838
|
+
return reply;
|
|
1839
|
+
}
|
|
1072
1840
|
return buildChatCompletionsBody(result);
|
|
1073
1841
|
});
|
|
1074
1842
|
app.post("/v1/images/generations", async (request, reply) => {
|