ai-zero-token 2.0.3 → 2.0.4
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 +9 -0
- package/README.md +18 -1
- package/README.zh-CN.md +18 -1
- package/admin-ui/dist/assets/{accounts-DymL4WIa.js → accounts-CTjk9c4F.js} +3 -1
- package/admin-ui/dist/assets/{docs-DtO-AOWU.js → docs-oNIugCIL.js} +3 -3
- package/admin-ui/dist/assets/{image-bed-yIVQ4dKs.js → image-bed-CQtIhjg_.js} +1 -1
- package/admin-ui/dist/assets/index-rgcJgVAu.js +10 -0
- package/admin-ui/dist/assets/{launch-CQXYrl-h.js → launch-B-2Zdz9m.js} +1 -1
- package/admin-ui/dist/assets/logs-JFuSf56b.js +1 -0
- package/admin-ui/dist/assets/{network-detect-sSrnwZqf.js → network-detect-SfvK6uhx.js} +1 -1
- package/admin-ui/dist/assets/{overview-BbSON0Jl.js → overview-X_WodIqE.js} +1 -1
- package/admin-ui/dist/assets/{settings-DvRiHS7i.js → settings-0eXUAvcm.js} +1 -1
- package/admin-ui/dist/assets/{tester-CftPgRE9.js → tester-ocpF053C.js} +1 -1
- package/admin-ui/dist/index.html +1 -1
- package/build/tray-icon-template.png +0 -0
- package/dist/core/providers/openai-codex/chat.js +77 -0
- package/dist/core/services/chat-service.js +1 -0
- package/dist/desktop/main.js +616 -15
- package/dist/server/app.js +449 -42
- package/dist/server/index.js +2 -1
- package/docs/API_USAGE.md +65 -1
- package/docs/DESKTOP_RELEASE.md +13 -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/dist/server/app.js
CHANGED
|
@@ -11,6 +11,7 @@ import { requestText } from "../core/providers/http-client.js";
|
|
|
11
11
|
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
12
12
|
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
13
13
|
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
14
|
+
const MAX_GATEWAY_REQUEST_LOGS = 100;
|
|
14
15
|
const assetContentTypes = {
|
|
15
16
|
".css": "text/css; charset=utf-8",
|
|
16
17
|
".gif": "image/gif",
|
|
@@ -198,13 +199,6 @@ const imageEditsBodySchema = z.object({
|
|
|
198
199
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
199
200
|
user: z.string().optional()
|
|
200
201
|
}).passthrough();
|
|
201
|
-
const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
|
|
202
|
-
"messages",
|
|
203
|
-
"n",
|
|
204
|
-
"stream",
|
|
205
|
-
"max_tokens",
|
|
206
|
-
"max_completion_tokens"
|
|
207
|
-
]);
|
|
208
202
|
function extractTextInput(input) {
|
|
209
203
|
if (typeof input === "undefined") {
|
|
210
204
|
return "";
|
|
@@ -242,7 +236,20 @@ function normalizeChatRole(role) {
|
|
|
242
236
|
}
|
|
243
237
|
return role ?? "user";
|
|
244
238
|
}
|
|
245
|
-
function
|
|
239
|
+
function safeJsonStringify(value) {
|
|
240
|
+
if (typeof value === "string") {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
if (typeof value === "undefined" || value === null) {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
return JSON.stringify(value);
|
|
248
|
+
} catch {
|
|
249
|
+
return String(value);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function normalizeChatContentPart(part, textType) {
|
|
246
253
|
if (part.type === "image_url") {
|
|
247
254
|
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
248
255
|
if (!url) {
|
|
@@ -258,37 +265,220 @@ function normalizeChatContentPart(part) {
|
|
|
258
265
|
}
|
|
259
266
|
const text = typeof part.text === "string" ? part.text : "";
|
|
260
267
|
return {
|
|
261
|
-
type:
|
|
268
|
+
type: textType,
|
|
262
269
|
text
|
|
263
270
|
};
|
|
264
271
|
}
|
|
265
|
-
function normalizeChatContent(content) {
|
|
272
|
+
function normalizeChatContent(content, role) {
|
|
273
|
+
const textType = role === "assistant" ? "output_text" : "input_text";
|
|
266
274
|
if (typeof content === "string") {
|
|
267
|
-
return [{ type:
|
|
275
|
+
return [{ type: textType, text: content }];
|
|
268
276
|
}
|
|
269
277
|
if (!Array.isArray(content) || content.length === 0) {
|
|
270
|
-
return [{ type:
|
|
278
|
+
return [{ type: textType, text: "" }];
|
|
271
279
|
}
|
|
272
|
-
return content.map((part) => normalizeChatContentPart(part));
|
|
280
|
+
return content.map((part) => normalizeChatContentPart(part, textType));
|
|
273
281
|
}
|
|
274
282
|
function normalizeChatMessages(messages) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
const normalized = [];
|
|
284
|
+
for (const message of messages) {
|
|
285
|
+
const record = message;
|
|
286
|
+
if (message.role === "tool") {
|
|
287
|
+
normalized.push({
|
|
288
|
+
type: "function_call_output",
|
|
289
|
+
call_id: message.tool_call_id,
|
|
290
|
+
output: typeof message.content === "string" ? message.content : safeJsonStringify(message.content)
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
normalized.push({
|
|
295
|
+
role: normalizeChatRole(message.role),
|
|
296
|
+
content: normalizeChatContent(message.content, message.role),
|
|
297
|
+
...message.name ? { name: message.name } : {},
|
|
298
|
+
...message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}
|
|
299
|
+
});
|
|
300
|
+
const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
|
|
301
|
+
for (const toolCall of toolCalls) {
|
|
302
|
+
const call = toolCall && typeof toolCall === "object" ? toolCall : {};
|
|
303
|
+
const fn = call.function && typeof call.function === "object" ? call.function : {};
|
|
304
|
+
const name = typeof fn.name === "string" ? fn.name : void 0;
|
|
305
|
+
if (!name) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
normalized.push({
|
|
309
|
+
type: "function_call",
|
|
310
|
+
call_id: typeof call.id === "string" ? call.id : `call_${normalized.length}`,
|
|
311
|
+
name,
|
|
312
|
+
arguments: safeJsonStringify(fn.arguments)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
318
|
+
function normalizeChatTools(tools) {
|
|
319
|
+
if (!tools) {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
return tools.map((tool) => {
|
|
323
|
+
if (!tool || typeof tool !== "object") {
|
|
324
|
+
return tool;
|
|
325
|
+
}
|
|
326
|
+
const record = tool;
|
|
327
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
328
|
+
if (record.type !== "function" || !fn) {
|
|
329
|
+
return tool;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
type: "function",
|
|
333
|
+
name: fn.name,
|
|
334
|
+
description: fn.description,
|
|
335
|
+
parameters: fn.parameters
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function normalizeChatToolChoice(toolChoice) {
|
|
340
|
+
if (!toolChoice || typeof toolChoice !== "object") {
|
|
341
|
+
return toolChoice;
|
|
342
|
+
}
|
|
343
|
+
const record = toolChoice;
|
|
344
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
345
|
+
if (record.type === "function" && fn && typeof fn.name === "string") {
|
|
346
|
+
return {
|
|
347
|
+
type: "function",
|
|
348
|
+
name: fn.name
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return toolChoice;
|
|
352
|
+
}
|
|
353
|
+
function normalizeReasoningEffort(value) {
|
|
354
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
if (value === "minimal") {
|
|
358
|
+
return "low";
|
|
359
|
+
}
|
|
360
|
+
if (value === "xhigh") {
|
|
361
|
+
return "high";
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function normalizeChatReasoning(data) {
|
|
366
|
+
const record = data;
|
|
367
|
+
const existing = record.reasoning;
|
|
368
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
369
|
+
return existing;
|
|
370
|
+
}
|
|
371
|
+
const effort = normalizeReasoningEffort(record.reasoning_effort);
|
|
372
|
+
return effort ? { effort } : void 0;
|
|
373
|
+
}
|
|
374
|
+
function truncateForLog(value, maxLength = 300) {
|
|
375
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
376
|
+
if (normalized.length <= maxLength) {
|
|
377
|
+
return normalized;
|
|
378
|
+
}
|
|
379
|
+
return `${normalized.slice(0, maxLength)}...`;
|
|
380
|
+
}
|
|
381
|
+
function extractChatMessageText(message) {
|
|
382
|
+
if (typeof message.content === "string") {
|
|
383
|
+
return message.content;
|
|
384
|
+
}
|
|
385
|
+
if (!Array.isArray(message.content)) {
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
return message.content.map((part) => typeof part.text === "string" ? part.text : part.image_url ? "[image]" : "").filter(Boolean).join("\n");
|
|
389
|
+
}
|
|
390
|
+
function countRoles(messages) {
|
|
391
|
+
const counts = {};
|
|
392
|
+
for (const message of messages) {
|
|
393
|
+
const role = message.role ?? "user";
|
|
394
|
+
counts[role] = (counts[role] ?? 0) + 1;
|
|
395
|
+
}
|
|
396
|
+
return counts;
|
|
397
|
+
}
|
|
398
|
+
function summarizeRecentMessages(messages) {
|
|
399
|
+
return messages.slice(-8).map((message) => ({
|
|
400
|
+
role: message.role ?? "user",
|
|
401
|
+
textPreview: truncateForLog(extractChatMessageText(message), 180),
|
|
402
|
+
toolCallId: message.tool_call_id
|
|
280
403
|
}));
|
|
281
404
|
}
|
|
405
|
+
function summarizeToolNames(tools) {
|
|
406
|
+
if (!tools) {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
return tools.map((tool) => {
|
|
410
|
+
if (!tool || typeof tool !== "object") {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
const record = tool;
|
|
414
|
+
const fn = record.function && typeof record.function === "object" ? record.function : null;
|
|
415
|
+
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : "";
|
|
416
|
+
}).filter(Boolean);
|
|
417
|
+
}
|
|
418
|
+
function summarizeChatCompletionsRequest(data) {
|
|
419
|
+
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
420
|
+
const toolNames = summarizeToolNames(data.tools);
|
|
421
|
+
return {
|
|
422
|
+
endpoint: "/v1/chat/completions",
|
|
423
|
+
model: data.model ?? "default",
|
|
424
|
+
stream: data.stream ?? false,
|
|
425
|
+
messageCount: data.messages.length,
|
|
426
|
+
roleCounts: countRoles(data.messages),
|
|
427
|
+
recentMessages: summarizeRecentMessages(data.messages),
|
|
428
|
+
lastUserTextPreview: lastUserMessage ? truncateForLog(extractChatMessageText(lastUserMessage)) : "",
|
|
429
|
+
toolCount: data.tools?.length ?? 0,
|
|
430
|
+
toolNames: toolNames.slice(0, 50),
|
|
431
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
432
|
+
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
433
|
+
parallelToolCalls: data.parallel_tool_calls,
|
|
434
|
+
hasReasoning: Boolean(data.reasoning || data.reasoning_effort),
|
|
435
|
+
maxTokens: data.max_completion_tokens ?? data.max_tokens
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function summarizeCodexChatBody(body) {
|
|
439
|
+
const toolNames = summarizeToolNames(Array.isArray(body.tools) ? body.tools : void 0);
|
|
440
|
+
return {
|
|
441
|
+
keys: Object.keys(body).sort(),
|
|
442
|
+
model: body.model ?? "default",
|
|
443
|
+
stream: body.stream,
|
|
444
|
+
store: body.store,
|
|
445
|
+
inputItems: Array.isArray(body.input) ? body.input.length : void 0,
|
|
446
|
+
tools: Array.isArray(body.tools) ? body.tools.length : void 0,
|
|
447
|
+
toolNames: toolNames.slice(0, 50),
|
|
448
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
449
|
+
toolChoice: typeof body.tool_choice === "undefined" ? "default" : typeof body.tool_choice,
|
|
450
|
+
parallelToolCalls: body.parallel_tool_calls,
|
|
451
|
+
hasReasoning: Boolean(body.reasoning)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function profileLogLabel(profile) {
|
|
455
|
+
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
456
|
+
}
|
|
457
|
+
function requestSourceFromUserAgent(userAgent) {
|
|
458
|
+
return typeof userAgent === "string" && userAgent.toLowerCase().includes("openclaw") ? "OpenClaw" : "API";
|
|
459
|
+
}
|
|
282
460
|
function createChatCompletionsCodexBody(data) {
|
|
283
|
-
const body =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
body.
|
|
461
|
+
const body = {
|
|
462
|
+
store: false,
|
|
463
|
+
stream: true,
|
|
464
|
+
input: normalizeChatMessages(data.messages)
|
|
465
|
+
};
|
|
466
|
+
if (data.model) {
|
|
467
|
+
body.model = data.model;
|
|
468
|
+
}
|
|
469
|
+
if (typeof data.parallel_tool_calls === "boolean") {
|
|
470
|
+
body.parallel_tool_calls = data.parallel_tool_calls;
|
|
471
|
+
}
|
|
472
|
+
if (data.tools) {
|
|
473
|
+
body.tools = normalizeChatTools(data.tools);
|
|
474
|
+
}
|
|
475
|
+
if (typeof data.tool_choice !== "undefined") {
|
|
476
|
+
body.tool_choice = normalizeChatToolChoice(data.tool_choice);
|
|
477
|
+
}
|
|
478
|
+
const reasoning = normalizeChatReasoning(data);
|
|
479
|
+
if (reasoning) {
|
|
480
|
+
body.reasoning = reasoning;
|
|
290
481
|
}
|
|
291
|
-
body.input = normalizeChatMessages(data.messages);
|
|
292
482
|
return body;
|
|
293
483
|
}
|
|
294
484
|
function summarizeImageRequestForLog(body) {
|
|
@@ -373,6 +563,7 @@ function buildResponseApiBody(result, includeRaw) {
|
|
|
373
563
|
return responseBody;
|
|
374
564
|
}
|
|
375
565
|
function buildChatCompletionsBody(result) {
|
|
566
|
+
const hasToolCalls = result.toolCalls.length > 0;
|
|
376
567
|
const body = {
|
|
377
568
|
id: `chatcmpl_${randomUUID().replace(/-/g, "")}`,
|
|
378
569
|
object: "chat.completion",
|
|
@@ -381,10 +572,11 @@ function buildChatCompletionsBody(result) {
|
|
|
381
572
|
choices: [
|
|
382
573
|
{
|
|
383
574
|
index: 0,
|
|
384
|
-
finish_reason: "stop",
|
|
575
|
+
finish_reason: hasToolCalls ? "tool_calls" : "stop",
|
|
385
576
|
message: {
|
|
386
577
|
role: "assistant",
|
|
387
|
-
content: result.text
|
|
578
|
+
content: hasToolCalls ? result.text || null : result.text,
|
|
579
|
+
...hasToolCalls ? { tool_calls: result.toolCalls } : {}
|
|
388
580
|
}
|
|
389
581
|
}
|
|
390
582
|
]
|
|
@@ -394,6 +586,79 @@ function buildChatCompletionsBody(result) {
|
|
|
394
586
|
}
|
|
395
587
|
return body;
|
|
396
588
|
}
|
|
589
|
+
function writeChatCompletionsSseEvent(reply, data) {
|
|
590
|
+
reply.raw.write(`data: ${JSON.stringify(data)}
|
|
591
|
+
|
|
592
|
+
`);
|
|
593
|
+
}
|
|
594
|
+
function buildChatCompletionChunk(params) {
|
|
595
|
+
return {
|
|
596
|
+
id: params.id,
|
|
597
|
+
object: "chat.completion.chunk",
|
|
598
|
+
created: params.created,
|
|
599
|
+
model: params.model,
|
|
600
|
+
choices: [
|
|
601
|
+
{
|
|
602
|
+
index: 0,
|
|
603
|
+
delta: params.delta,
|
|
604
|
+
finish_reason: params.finishReason ?? null
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function sendChatCompletionsStream(reply, result) {
|
|
610
|
+
const id = `chatcmpl_${randomUUID().replace(/-/g, "")}`;
|
|
611
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
612
|
+
reply.raw.writeHead(200, {
|
|
613
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
614
|
+
"Cache-Control": "no-cache, no-transform",
|
|
615
|
+
Connection: "keep-alive",
|
|
616
|
+
"X-Accel-Buffering": "no"
|
|
617
|
+
});
|
|
618
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
619
|
+
id,
|
|
620
|
+
created,
|
|
621
|
+
model: result.model,
|
|
622
|
+
delta: { role: "assistant" }
|
|
623
|
+
}));
|
|
624
|
+
if (result.text) {
|
|
625
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
626
|
+
id,
|
|
627
|
+
created,
|
|
628
|
+
model: result.model,
|
|
629
|
+
delta: { content: result.text }
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
result.toolCalls.forEach((toolCall, index) => {
|
|
633
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
634
|
+
id,
|
|
635
|
+
created,
|
|
636
|
+
model: result.model,
|
|
637
|
+
delta: {
|
|
638
|
+
tool_calls: [
|
|
639
|
+
{
|
|
640
|
+
index,
|
|
641
|
+
id: toolCall.id,
|
|
642
|
+
type: toolCall.type,
|
|
643
|
+
function: {
|
|
644
|
+
name: toolCall.function.name,
|
|
645
|
+
arguments: toolCall.function.arguments
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
}));
|
|
651
|
+
});
|
|
652
|
+
writeChatCompletionsSseEvent(reply, buildChatCompletionChunk({
|
|
653
|
+
id,
|
|
654
|
+
created,
|
|
655
|
+
model: result.model,
|
|
656
|
+
delta: {},
|
|
657
|
+
finishReason: result.toolCalls.length > 0 ? "tool_calls" : "stop"
|
|
658
|
+
}));
|
|
659
|
+
reply.raw.write("data: [DONE]\n\n");
|
|
660
|
+
reply.raw.end();
|
|
661
|
+
}
|
|
397
662
|
function validateImageRequest(data) {
|
|
398
663
|
if (data.response_format === "url") {
|
|
399
664
|
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";
|
|
@@ -502,6 +767,24 @@ function createApp(params) {
|
|
|
502
767
|
bodyLimit: params?.bodyLimit
|
|
503
768
|
});
|
|
504
769
|
const ctx = createGatewayContext();
|
|
770
|
+
const gatewayRequestLogs = [];
|
|
771
|
+
function pushGatewayRequestLog(log) {
|
|
772
|
+
gatewayRequestLogs.unshift({
|
|
773
|
+
id: log.id ?? randomUUID(),
|
|
774
|
+
time: log.time ?? Date.now(),
|
|
775
|
+
method: log.method,
|
|
776
|
+
endpoint: log.endpoint,
|
|
777
|
+
account: log.account,
|
|
778
|
+
model: log.model,
|
|
779
|
+
statusCode: log.statusCode,
|
|
780
|
+
durationMs: log.durationMs,
|
|
781
|
+
source: log.source,
|
|
782
|
+
details: log.details
|
|
783
|
+
});
|
|
784
|
+
if (gatewayRequestLogs.length > MAX_GATEWAY_REQUEST_LOGS) {
|
|
785
|
+
gatewayRequestLogs.length = MAX_GATEWAY_REQUEST_LOGS;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
505
788
|
void app.register(cors, {
|
|
506
789
|
origin: params?.corsOrigin ?? true,
|
|
507
790
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
@@ -524,6 +807,9 @@ function createApp(params) {
|
|
|
524
807
|
}
|
|
525
808
|
};
|
|
526
809
|
});
|
|
810
|
+
app.get("/_gateway/admin/request-logs", async () => ({
|
|
811
|
+
data: gatewayRequestLogs
|
|
812
|
+
}));
|
|
527
813
|
async function buildAdminConfig(request) {
|
|
528
814
|
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
529
815
|
ctx.authService.getStatus(),
|
|
@@ -548,6 +834,7 @@ function createApp(params) {
|
|
|
548
834
|
adminUrl: `${origin}/`,
|
|
549
835
|
baseUrl: `${origin}/v1`,
|
|
550
836
|
restartSupported: Boolean(params?.onRestart),
|
|
837
|
+
codexRestartSupported: Boolean(params?.onRestartCodex),
|
|
551
838
|
supportedEndpoints: [
|
|
552
839
|
{
|
|
553
840
|
method: "GET",
|
|
@@ -839,6 +1126,22 @@ function createApp(params) {
|
|
|
839
1126
|
restarting: true
|
|
840
1127
|
};
|
|
841
1128
|
});
|
|
1129
|
+
app.post("/_gateway/admin/desktop/restart-codex", async (_request, reply) => {
|
|
1130
|
+
if (!params?.onRestartCodex) {
|
|
1131
|
+
reply.code(501);
|
|
1132
|
+
return {
|
|
1133
|
+
error: {
|
|
1134
|
+
type: "not_supported",
|
|
1135
|
+
message: "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u91CD\u542F Codex\u3002"
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
await params.onRestartCodex();
|
|
1140
|
+
return {
|
|
1141
|
+
ok: true,
|
|
1142
|
+
restarted: true
|
|
1143
|
+
};
|
|
1144
|
+
});
|
|
842
1145
|
app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
|
|
843
1146
|
const parsed = proxyTestSchema.safeParse(request.body);
|
|
844
1147
|
if (!parsed.success) {
|
|
@@ -1030,8 +1333,27 @@ function createApp(params) {
|
|
|
1030
1333
|
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
1031
1334
|
});
|
|
1032
1335
|
app.post("/v1/chat/completions", async (request, reply) => {
|
|
1336
|
+
const startedAt = performance.now();
|
|
1033
1337
|
const parsed = chatCompletionsBodySchema.safeParse(request.body);
|
|
1034
1338
|
if (!parsed.success) {
|
|
1339
|
+
pushGatewayRequestLog({
|
|
1340
|
+
method: request.method,
|
|
1341
|
+
endpoint: request.url,
|
|
1342
|
+
account: "-",
|
|
1343
|
+
model: "-",
|
|
1344
|
+
statusCode: 400,
|
|
1345
|
+
durationMs: performance.now() - startedAt,
|
|
1346
|
+
source: "API",
|
|
1347
|
+
details: {
|
|
1348
|
+
requestId: request.id,
|
|
1349
|
+
remoteAddress: request.ip,
|
|
1350
|
+
userAgent: request.headers["user-agent"],
|
|
1351
|
+
error: {
|
|
1352
|
+
type: "validation_error",
|
|
1353
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1035
1357
|
reply.code(400);
|
|
1036
1358
|
return {
|
|
1037
1359
|
error: {
|
|
@@ -1040,16 +1362,24 @@ function createApp(params) {
|
|
|
1040
1362
|
}
|
|
1041
1363
|
};
|
|
1042
1364
|
}
|
|
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
1365
|
if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
|
|
1366
|
+
pushGatewayRequestLog({
|
|
1367
|
+
method: request.method,
|
|
1368
|
+
endpoint: request.url,
|
|
1369
|
+
account: "-",
|
|
1370
|
+
model: parsed.data.model ?? "default",
|
|
1371
|
+
statusCode: 501,
|
|
1372
|
+
durationMs: performance.now() - startedAt,
|
|
1373
|
+
source: "API",
|
|
1374
|
+
details: {
|
|
1375
|
+
requestId: request.id,
|
|
1376
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1377
|
+
error: {
|
|
1378
|
+
type: "not_supported",
|
|
1379
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301\u4E00\u6B21\u8FD4\u56DE\u591A\u4E2A choices\uFF08n > 1\uFF09"
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1053
1383
|
reply.code(501);
|
|
1054
1384
|
return {
|
|
1055
1385
|
error: {
|
|
@@ -1059,16 +1389,93 @@ function createApp(params) {
|
|
|
1059
1389
|
};
|
|
1060
1390
|
}
|
|
1061
1391
|
const codexBody = createChatCompletionsCodexBody(parsed.data);
|
|
1392
|
+
console.info("[gateway:chat:request]", {
|
|
1393
|
+
requestId: request.id,
|
|
1394
|
+
remoteAddress: request.ip,
|
|
1395
|
+
userAgent: request.headers["user-agent"],
|
|
1396
|
+
...summarizeChatCompletionsRequest(parsed.data),
|
|
1397
|
+
codex: summarizeCodexChatBody(codexBody)
|
|
1398
|
+
});
|
|
1062
1399
|
const fallbackInput = parsed.data.messages.map(
|
|
1063
1400
|
(message) => typeof message.content === "string" ? message.content : (message.content ?? []).map((part) => typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n")
|
|
1064
1401
|
).filter(Boolean).join("\n").trim();
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1402
|
+
let result;
|
|
1403
|
+
try {
|
|
1404
|
+
result = await ctx.chatService.chat({
|
|
1405
|
+
model: parsed.data.model,
|
|
1406
|
+
input: fallbackInput || void 0,
|
|
1407
|
+
experimental: {
|
|
1408
|
+
codexBody
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
const normalized = normalizeError(error);
|
|
1413
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
1414
|
+
pushGatewayRequestLog({
|
|
1415
|
+
method: request.method,
|
|
1416
|
+
endpoint: request.url,
|
|
1417
|
+
account: profileLogLabel(await ctx.authService.getActiveProfile()),
|
|
1418
|
+
model: parsed.data.model ?? "default",
|
|
1419
|
+
statusCode,
|
|
1420
|
+
durationMs: performance.now() - startedAt,
|
|
1421
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1422
|
+
details: {
|
|
1423
|
+
requestId: request.id,
|
|
1424
|
+
remoteAddress: request.ip,
|
|
1425
|
+
userAgent: request.headers["user-agent"],
|
|
1426
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1427
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
1428
|
+
error: {
|
|
1429
|
+
message: normalized.message,
|
|
1430
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
1431
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1432
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
pushGatewayRequestLog({
|
|
1439
|
+
method: request.method,
|
|
1440
|
+
endpoint: request.url,
|
|
1441
|
+
account: profileLogLabel(await ctx.authService.getActiveProfile()),
|
|
1442
|
+
model: result.model,
|
|
1443
|
+
statusCode: 200,
|
|
1444
|
+
durationMs: performance.now() - startedAt,
|
|
1445
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1446
|
+
details: {
|
|
1447
|
+
requestId: request.id,
|
|
1448
|
+
remoteAddress: request.ip,
|
|
1449
|
+
userAgent: request.headers["user-agent"],
|
|
1450
|
+
request: summarizeChatCompletionsRequest(parsed.data),
|
|
1451
|
+
codex: summarizeCodexChatBody(codexBody),
|
|
1452
|
+
response: {
|
|
1453
|
+
textPreview: truncateForLog(result.text),
|
|
1454
|
+
textLength: result.text.length,
|
|
1455
|
+
toolCallCount: result.toolCalls.length,
|
|
1456
|
+
toolCalls: result.toolCalls.map((toolCall) => ({
|
|
1457
|
+
id: toolCall.id,
|
|
1458
|
+
name: toolCall.function.name,
|
|
1459
|
+
argumentsPreview: truncateForLog(toolCall.function.arguments)
|
|
1460
|
+
})),
|
|
1461
|
+
artifactCount: result.artifacts.length,
|
|
1462
|
+
stream: parsed.data.stream ?? false
|
|
1463
|
+
}
|
|
1070
1464
|
}
|
|
1071
1465
|
});
|
|
1466
|
+
console.info("[gateway:chat:response]", {
|
|
1467
|
+
requestId: request.id,
|
|
1468
|
+
model: result.model,
|
|
1469
|
+
stream: parsed.data.stream ?? false,
|
|
1470
|
+
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
1471
|
+
textLength: result.text.length,
|
|
1472
|
+
toolCallCount: result.toolCalls.length,
|
|
1473
|
+
artifactCount: result.artifacts.length
|
|
1474
|
+
});
|
|
1475
|
+
if (parsed.data.stream) {
|
|
1476
|
+
sendChatCompletionsStream(reply, result);
|
|
1477
|
+
return reply;
|
|
1478
|
+
}
|
|
1072
1479
|
return buildChatCompletionsBody(result);
|
|
1073
1480
|
});
|
|
1074
1481
|
app.post("/v1/images/generations", async (request, reply) => {
|
package/dist/server/index.js
CHANGED
|
@@ -41,7 +41,8 @@ async function startServer(params) {
|
|
|
41
41
|
const app = createApp({
|
|
42
42
|
corsOrigin: resolveCorsOrigin(),
|
|
43
43
|
bodyLimit,
|
|
44
|
-
onRestart: params?.onRestart
|
|
44
|
+
onRestart: params?.onRestart,
|
|
45
|
+
onRestartCodex: params?.onRestartCodex
|
|
45
46
|
});
|
|
46
47
|
try {
|
|
47
48
|
await app.listen({
|
package/docs/API_USAGE.md
CHANGED
|
@@ -41,6 +41,24 @@ Image model: gpt-image-2
|
|
|
41
41
|
|
|
42
42
|
Use `GET /v1/models` to see the models available through the current local Codex cache.
|
|
43
43
|
|
|
44
|
+
## OpenClaw Settings
|
|
45
|
+
|
|
46
|
+
Use the OpenAI-compatible provider mode in OpenClaw:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
Provider: OpenAI compatible
|
|
50
|
+
Base URL: http://127.0.0.1:8787/v1
|
|
51
|
+
API Key: local
|
|
52
|
+
Model: gpt-5.4
|
|
53
|
+
Chat endpoint: /chat/completions
|
|
54
|
+
Streaming: enabled
|
|
55
|
+
Tools / function calling: enabled
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The gateway accepts OpenClaw-style `chat.completions` requests with `tools`, `tool_choice`, `parallel_tool_calls`, `reasoning_effort`, assistant `tool_calls`, and tool-role result messages. It translates those fields to the upstream Codex Responses shape and returns OpenAI-style chat responses.
|
|
59
|
+
|
|
60
|
+
OpenClaw requests are visible in the management console request log when the client sends an OpenClaw user agent. The log keeps safe summaries only; it does not store full access tokens.
|
|
61
|
+
|
|
44
62
|
## Models
|
|
45
63
|
|
|
46
64
|
```bash
|
|
@@ -74,6 +92,50 @@ curl http://127.0.0.1:8787/v1/chat/completions \
|
|
|
74
92
|
"messages": [
|
|
75
93
|
{ "role": "user", "content": "Reply with OK only." }
|
|
76
94
|
]
|
|
95
|
+
}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Streaming chat completions:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
102
|
+
-H "Content-Type: application/json" \
|
|
103
|
+
-d '{
|
|
104
|
+
"model": "gpt-5.4",
|
|
105
|
+
"stream": true,
|
|
106
|
+
"messages": [
|
|
107
|
+
{ "role": "user", "content": "Reply with OK only." }
|
|
108
|
+
]
|
|
109
|
+
}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Tool-call compatible request:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{
|
|
118
|
+
"model": "gpt-5.4",
|
|
119
|
+
"messages": [
|
|
120
|
+
{ "role": "user", "content": "What is the weather tool argument for Shanghai?" }
|
|
121
|
+
],
|
|
122
|
+
"tools": [
|
|
123
|
+
{
|
|
124
|
+
"type": "function",
|
|
125
|
+
"function": {
|
|
126
|
+
"name": "get_weather",
|
|
127
|
+
"description": "Get weather for a city.",
|
|
128
|
+
"parameters": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"city": { "type": "string" }
|
|
132
|
+
},
|
|
133
|
+
"required": ["city"]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"tool_choice": "auto"
|
|
77
139
|
}'
|
|
78
140
|
```
|
|
79
141
|
|
|
@@ -134,5 +196,7 @@ console.log(response.choices[0]?.message?.content);
|
|
|
134
196
|
|
|
135
197
|
- Login first through the management page or `azt login`.
|
|
136
198
|
- A model appearing in `/v1/models` means the local Codex cache lists it. Final availability still depends on the active account.
|
|
137
|
-
- `stream=true` is
|
|
199
|
+
- `stream=true` is supported for `/v1/chat/completions` through OpenAI-style SSE chunks. `/v1/responses` streaming is still not implemented.
|
|
200
|
+
- `n > 1` is not supported for `/v1/chat/completions`.
|
|
201
|
+
- Tool/function calling is supported for common OpenAI-compatible clients, including OpenClaw, but exact upstream behavior still depends on the active Codex model and account.
|
|
138
202
|
- The default listener is `0.0.0.0:8787`, so local-network clients can call the gateway by using the machine IP.
|