ai-zero-token 2.0.2 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +31 -6
  3. package/README.zh-CN.md +31 -6
  4. package/admin-ui/dist/assets/accounts-CTjk9c4F.js +4 -0
  5. package/admin-ui/dist/assets/{docs-CihX3Xsm.js → docs-oNIugCIL.js} +3 -3
  6. package/admin-ui/dist/assets/{image-bed-BGjlDLks.js → image-bed-CQtIhjg_.js} +1 -1
  7. package/admin-ui/dist/assets/index-By4r-wy3.css +1 -0
  8. package/admin-ui/dist/assets/index-rgcJgVAu.js +10 -0
  9. package/admin-ui/dist/assets/{launch-BWw7Odq7.js → launch-B-2Zdz9m.js} +1 -1
  10. package/admin-ui/dist/assets/logs-JFuSf56b.js +1 -0
  11. package/admin-ui/dist/assets/{network-detect-cUdjg4zk.js → network-detect-SfvK6uhx.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-CsjVVcvi.js → overview-X_WodIqE.js} +1 -1
  13. package/admin-ui/dist/assets/settings-0eXUAvcm.js +1 -0
  14. package/admin-ui/dist/assets/{tester-BIvH_8DY.js → tester-ocpF053C.js} +1 -1
  15. package/admin-ui/dist/index.html +2 -2
  16. package/build/mac-install-guide.txt +25 -0
  17. package/build/tray-icon-template.png +0 -0
  18. package/dist/core/providers/openai-codex/chat.js +77 -0
  19. package/dist/core/services/auth-service.js +88 -12
  20. package/dist/core/services/chat-service.js +1 -0
  21. package/dist/core/services/config-service.js +87 -23
  22. package/dist/core/services/version-service.js +1 -1
  23. package/dist/core/store/profile-store.js +73 -32
  24. package/dist/core/store/settings-store.js +14 -0
  25. package/dist/desktop/main.js +616 -15
  26. package/dist/server/app.js +512 -58
  27. package/dist/server/index.js +2 -1
  28. package/docs/API_USAGE.md +65 -1
  29. package/docs/DESKTOP_RELEASE.md +48 -3
  30. package/package.json +33 -2
  31. package/admin-ui/dist/assets/accounts-Ddq82u6R.js +0 -1
  32. package/admin-ui/dist/assets/index-CX8e0-BB.js +0 -10
  33. package/admin-ui/dist/assets/index-ywn2Jwpu.css +0 -1
  34. package/admin-ui/dist/assets/logs-DDdgDVwo.js +0 -1
  35. package/admin-ui/dist/assets/settings-Be99HpDD.js +0 -1
@@ -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",
@@ -115,6 +116,9 @@ const settingsUpdateSchema = z.object({
115
116
  autoSwitch: z.object({
116
117
  enabled: z.boolean()
117
118
  }).optional(),
119
+ runtime: z.object({
120
+ quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
121
+ }).optional(),
118
122
  server: z.object({
119
123
  port: z.number().int().min(1).max(65535)
120
124
  }).optional()
@@ -129,9 +133,15 @@ const proxyTestSchema = z.object({
129
133
  const profileActionSchema = z.object({
130
134
  profileId: z.string().min(1)
131
135
  });
136
+ const profileRemoveBatchSchema = z.object({
137
+ profileIds: z.array(z.string().min(1)).min(1)
138
+ });
132
139
  const profileImportSchema = z.object({
133
140
  profile: z.unknown()
134
141
  });
142
+ const runtimeRefreshSchema = z.object({
143
+ staleOnly: z.boolean().optional()
144
+ });
135
145
  const profileExportSchema = z.object({
136
146
  profileId: z.string().min(1).optional(),
137
147
  profileIds: z.array(z.string().min(1)).optional(),
@@ -189,13 +199,6 @@ const imageEditsBodySchema = z.object({
189
199
  response_format: z.enum(["b64_json", "url"]).optional(),
190
200
  user: z.string().optional()
191
201
  }).passthrough();
192
- const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
193
- "messages",
194
- "n",
195
- "stream",
196
- "max_tokens",
197
- "max_completion_tokens"
198
- ]);
199
202
  function extractTextInput(input) {
200
203
  if (typeof input === "undefined") {
201
204
  return "";
@@ -233,7 +236,20 @@ function normalizeChatRole(role) {
233
236
  }
234
237
  return role ?? "user";
235
238
  }
236
- function normalizeChatContentPart(part) {
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) {
237
253
  if (part.type === "image_url") {
238
254
  const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
239
255
  if (!url) {
@@ -249,37 +265,220 @@ function normalizeChatContentPart(part) {
249
265
  }
250
266
  const text = typeof part.text === "string" ? part.text : "";
251
267
  return {
252
- type: "input_text",
268
+ type: textType,
253
269
  text
254
270
  };
255
271
  }
256
- function normalizeChatContent(content) {
272
+ function normalizeChatContent(content, role) {
273
+ const textType = role === "assistant" ? "output_text" : "input_text";
257
274
  if (typeof content === "string") {
258
- return [{ type: "input_text", text: content }];
275
+ return [{ type: textType, text: content }];
259
276
  }
260
277
  if (!Array.isArray(content) || content.length === 0) {
261
- return [{ type: "input_text", text: "" }];
278
+ return [{ type: textType, text: "" }];
262
279
  }
263
- return content.map((part) => normalizeChatContentPart(part));
280
+ return content.map((part) => normalizeChatContentPart(part, textType));
264
281
  }
265
282
  function normalizeChatMessages(messages) {
266
- return messages.map((message) => ({
267
- role: normalizeChatRole(message.role),
268
- content: normalizeChatContent(message.content),
269
- ...message.name ? { name: message.name } : {},
270
- ...message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}
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
271
403
  }));
272
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
+ }
273
460
  function createChatCompletionsCodexBody(data) {
274
- const body = Object.fromEntries(
275
- Object.entries(data).filter(([key]) => !chatCompletionExcludedKeys.has(key))
276
- );
277
- if (typeof data.max_completion_tokens === "number") {
278
- body.max_output_tokens = data.max_completion_tokens;
279
- } else if (typeof data.max_tokens === "number") {
280
- body.max_output_tokens = data.max_tokens;
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;
281
481
  }
282
- body.input = normalizeChatMessages(data.messages);
283
482
  return body;
284
483
  }
285
484
  function summarizeImageRequestForLog(body) {
@@ -364,6 +563,7 @@ function buildResponseApiBody(result, includeRaw) {
364
563
  return responseBody;
365
564
  }
366
565
  function buildChatCompletionsBody(result) {
566
+ const hasToolCalls = result.toolCalls.length > 0;
367
567
  const body = {
368
568
  id: `chatcmpl_${randomUUID().replace(/-/g, "")}`,
369
569
  object: "chat.completion",
@@ -372,10 +572,11 @@ function buildChatCompletionsBody(result) {
372
572
  choices: [
373
573
  {
374
574
  index: 0,
375
- finish_reason: "stop",
575
+ finish_reason: hasToolCalls ? "tool_calls" : "stop",
376
576
  message: {
377
577
  role: "assistant",
378
- content: result.text
578
+ content: hasToolCalls ? result.text || null : result.text,
579
+ ...hasToolCalls ? { tool_calls: result.toolCalls } : {}
379
580
  }
380
581
  }
381
582
  ]
@@ -385,6 +586,79 @@ function buildChatCompletionsBody(result) {
385
586
  }
386
587
  return body;
387
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
+ }
388
662
  function validateImageRequest(data) {
389
663
  if (data.response_format === "url") {
390
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";
@@ -438,6 +712,7 @@ function serializeProfile(profile) {
438
712
  email: profile.email,
439
713
  quota: profile.quota,
440
714
  authStatus: profile.authStatus,
715
+ exportAudit: profile.exportAudit,
441
716
  expiresAt: profile.expires,
442
717
  accessTokenPreview: maskSecret(profile.access),
443
718
  refreshTokenPreview: maskSecret(profile.refresh)
@@ -451,6 +726,7 @@ function serializeManagedProfile(profile) {
451
726
  email: profile.email,
452
727
  quota: profile.quota,
453
728
  authStatus: profile.authStatus,
729
+ exportAudit: profile.exportAudit,
454
730
  expiresAt: profile.expiresAt,
455
731
  accessTokenPreview: profile.accessTokenPreview,
456
732
  refreshTokenPreview: profile.refreshTokenPreview,
@@ -491,6 +767,24 @@ function createApp(params) {
491
767
  bodyLimit: params?.bodyLimit
492
768
  });
493
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
+ }
494
788
  void app.register(cors, {
495
789
  origin: params?.corsOrigin ?? true,
496
790
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
@@ -513,6 +807,9 @@ function createApp(params) {
513
807
  }
514
808
  };
515
809
  });
810
+ app.get("/_gateway/admin/request-logs", async () => ({
811
+ data: gatewayRequestLogs
812
+ }));
516
813
  async function buildAdminConfig(request) {
517
814
  const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
518
815
  ctx.authService.getStatus(),
@@ -537,6 +834,7 @@ function createApp(params) {
537
834
  adminUrl: `${origin}/`,
538
835
  baseUrl: `${origin}/v1`,
539
836
  restartSupported: Boolean(params?.onRestart),
837
+ codexRestartSupported: Boolean(params?.onRestartCodex),
540
838
  supportedEndpoints: [
541
839
  {
542
840
  method: "GET",
@@ -612,10 +910,21 @@ function createApp(params) {
612
910
  catalog: result.catalog
613
911
  };
614
912
  });
615
- app.post("/_gateway/admin/runtime-refresh", async (request) => {
913
+ app.post("/_gateway/admin/runtime-refresh", async (request, reply) => {
914
+ const parsed = runtimeRefreshSchema.safeParse(request.body ?? {});
915
+ if (!parsed.success) {
916
+ reply.code(400);
917
+ return {
918
+ error: {
919
+ type: "validation_error",
920
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
921
+ }
922
+ };
923
+ }
616
924
  const [quotaSync] = await Promise.all([
617
925
  ctx.authService.syncAllProfileQuotas("openai-codex", {
618
- suppressErrors: true
926
+ suppressErrors: true,
927
+ staleAfterMs: parsed.data.staleOnly ? 30 * 60 * 1e3 : void 0
619
928
  }),
620
929
  ctx.versionService.getVersionStatus({
621
930
  force: true
@@ -688,6 +997,23 @@ function createApp(params) {
688
997
  await ctx.authService.removeProfile(parsed.data.profileId);
689
998
  return buildAdminConfig(request);
690
999
  });
1000
+ app.post("/_gateway/admin/profiles/remove-batch", async (request, reply) => {
1001
+ const parsed = profileRemoveBatchSchema.safeParse(request.body);
1002
+ if (!parsed.success) {
1003
+ reply.code(400);
1004
+ return {
1005
+ error: {
1006
+ type: "validation_error",
1007
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
1008
+ }
1009
+ };
1010
+ }
1011
+ const removedProfileCount = await ctx.authService.removeProfiles(parsed.data.profileIds);
1012
+ return {
1013
+ ...await buildAdminConfig(request),
1014
+ removedProfileCount
1015
+ };
1016
+ });
691
1017
  app.post("/_gateway/admin/profiles/import", async (request, reply) => {
692
1018
  const parsed = profileImportSchema.safeParse(request.body);
693
1019
  if (!parsed.success) {
@@ -708,6 +1034,23 @@ function createApp(params) {
708
1034
  importedProfileCount: importedProfiles.length
709
1035
  };
710
1036
  });
1037
+ app.post("/_gateway/admin/profiles/import/validate", async (request, reply) => {
1038
+ const parsed = profileImportSchema.safeParse(request.body);
1039
+ if (!parsed.success) {
1040
+ reply.code(400);
1041
+ return {
1042
+ error: {
1043
+ type: "validation_error",
1044
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
1045
+ }
1046
+ };
1047
+ }
1048
+ const profiles = ctx.authService.validateProfilesImport(parsed.data.profile);
1049
+ return {
1050
+ valid: true,
1051
+ profileCount: profiles.length
1052
+ };
1053
+ });
711
1054
  app.get("/_gateway/admin/profiles/import-template", async () => ({
712
1055
  profile: ctx.authService.getProfileImportTemplate()
713
1056
  }));
@@ -724,11 +1067,13 @@ function createApp(params) {
724
1067
  }
725
1068
  if (parsed.data.all || parsed.data.profileIds) {
726
1069
  return {
727
- profile: await ctx.authService.exportProfiles(parsed.data.profileIds)
1070
+ profile: await ctx.authService.exportProfiles(parsed.data.profileIds, "openai-codex", parsed.data.all ? "all" : "batch"),
1071
+ config: await buildAdminConfig(request)
728
1072
  };
729
1073
  }
730
1074
  return {
731
- profile: await ctx.authService.exportProfile(parsed.data.profileId)
1075
+ profile: await ctx.authService.exportProfile(parsed.data.profileId),
1076
+ config: await buildAdminConfig(request)
732
1077
  };
733
1078
  });
734
1079
  app.post("/_gateway/admin/codex/apply", async (request, reply) => {
@@ -758,18 +1103,7 @@ function createApp(params) {
758
1103
  }
759
1104
  };
760
1105
  }
761
- if (parsed.data.defaultModel) {
762
- await ctx.configService.setDefaultModel(parsed.data.defaultModel);
763
- }
764
- if (parsed.data.networkProxy) {
765
- await ctx.configService.setNetworkProxy(parsed.data.networkProxy);
766
- }
767
- if (parsed.data.autoSwitch) {
768
- await ctx.configService.setAutoSwitch(parsed.data.autoSwitch);
769
- }
770
- if (parsed.data.server) {
771
- await ctx.configService.setServerConfig({ port: parsed.data.server.port });
772
- }
1106
+ await ctx.configService.updateSettings(parsed.data);
773
1107
  return buildAdminConfig(request);
774
1108
  });
775
1109
  app.post("/_gateway/admin/restart", async (_request, reply) => {
@@ -792,6 +1126,22 @@ function createApp(params) {
792
1126
  restarting: true
793
1127
  };
794
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
+ });
795
1145
  app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
796
1146
  const parsed = proxyTestSchema.safeParse(request.body);
797
1147
  if (!parsed.success) {
@@ -983,8 +1333,27 @@ function createApp(params) {
983
1333
  return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
984
1334
  });
985
1335
  app.post("/v1/chat/completions", async (request, reply) => {
1336
+ const startedAt = performance.now();
986
1337
  const parsed = chatCompletionsBodySchema.safeParse(request.body);
987
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
+ });
988
1357
  reply.code(400);
989
1358
  return {
990
1359
  error: {
@@ -993,16 +1362,24 @@ function createApp(params) {
993
1362
  }
994
1363
  };
995
1364
  }
996
- if (parsed.data.stream) {
997
- reply.code(501);
998
- return {
999
- error: {
1000
- type: "not_supported",
1001
- message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 chat.completions \u7684 stream=true"
1002
- }
1003
- };
1004
- }
1005
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
+ });
1006
1383
  reply.code(501);
1007
1384
  return {
1008
1385
  error: {
@@ -1012,16 +1389,93 @@ function createApp(params) {
1012
1389
  };
1013
1390
  }
1014
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
+ });
1015
1399
  const fallbackInput = parsed.data.messages.map(
1016
1400
  (message) => typeof message.content === "string" ? message.content : (message.content ?? []).map((part) => typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n")
1017
1401
  ).filter(Boolean).join("\n").trim();
1018
- const result = await ctx.chatService.chat({
1019
- model: parsed.data.model,
1020
- input: fallbackInput || void 0,
1021
- experimental: {
1022
- codexBody
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
+ }
1023
1464
  }
1024
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
+ }
1025
1479
  return buildChatCompletionsBody(result);
1026
1480
  });
1027
1481
  app.post("/v1/images/generations", async (request, reply) => {