plugin-ai-api 1.0.9 → 1.0.10

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.
@@ -10,14 +10,14 @@
10
10
  module.exports = {
11
11
  "react": "18.2.0",
12
12
  "antd": "5.24.2",
13
- "@nocobase/client-v2": "2.1.23",
14
- "@nocobase/flow-engine": "2.1.23",
15
- "@nocobase/client": "2.1.23",
16
- "@nocobase/plugin-acl": "2.1.23",
17
- "@nocobase/server": "2.1.23",
13
+ "@nocobase/client-v2": "2.1.27",
14
+ "@nocobase/flow-engine": "2.1.27",
15
+ "@nocobase/client": "2.1.27",
16
+ "@nocobase/plugin-acl": "2.1.27",
17
+ "@nocobase/server": "2.1.27",
18
18
  "dayjs": "1.11.13",
19
- "@nocobase/actions": "2.1.23",
20
- "@nocobase/database": "2.1.23",
21
- "@nocobase/resourcer": "2.1.23",
22
- "@nocobase/plugin-ai": "2.1.23"
19
+ "@nocobase/actions": "2.1.27",
20
+ "@nocobase/database": "2.1.27",
21
+ "@nocobase/resourcer": "2.1.27",
22
+ "@nocobase/plugin-ai": "2.1.27"
23
23
  };
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(agent_completions_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
34
  var import_role_permission = require("../middleware/role-permission");
35
+ var import_streaming = require("../utils/streaming");
35
36
  var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
36
37
  async function handleAgentCompletions(ctx, plugin) {
37
38
  var _a;
@@ -96,7 +97,7 @@ async function handleAgentCompletions(ctx, plugin) {
96
97
  );
97
98
  return;
98
99
  }
99
- const wantStream = body.stream === true;
100
+ const wantStream = (0, import_streaming.isStreamingRequested)(body.stream);
100
101
  const lifecycle = (0, import_ai_employee_runtime.getAgentRuntimeLifecycle)(ctx);
101
102
  let runtimeContext;
102
103
  let lifecycleCompleted = false;
@@ -182,42 +183,87 @@ async function handleAgentCompletions(ctx, plugin) {
182
183
  );
183
184
  const originalWrite = ctx.res.write.bind(ctx.res);
184
185
  const originalEnd = ctx.res.end.bind(ctx.res);
186
+ const aiPlugin = ctx.app.pm.get("ai");
187
+ const abortAgent = () => {
188
+ var _a2, _b, _c;
189
+ if (!ctx.res.writableEnded) {
190
+ (_c = (_b = (_a2 = aiPlugin == null ? void 0 : aiPlugin.aiEmployeesManager) == null ? void 0 : _a2.conversationController) == null ? void 0 : _b.get(String(sessionId))) == null ? void 0 : _c.abort();
191
+ }
192
+ };
193
+ ctx.req.once("aborted", abortAgent);
194
+ ctx.res.once("close", abortAgent);
195
+ let streamSucceeded = false;
196
+ let sawToolCalls = false;
197
+ let pendingSse = "";
185
198
  ctx.res.end = (...args) => {
186
199
  if (args[0]) {
187
200
  ctx.res.write(args[0]);
188
201
  }
189
202
  };
190
203
  ctx.res.write = (data) => {
191
- const text = typeof data === "string" ? data : data.toString("utf8");
192
- for (const line of text.split("\n")) {
193
- const trimmed = line.trim();
194
- if (!trimmed.startsWith("data: ")) continue;
195
- const jsonStr = trimmed.substring(6);
196
- if (!jsonStr) continue;
197
- try {
198
- const event = JSON.parse(jsonStr);
199
- if (event.type === "content" && event.body) {
200
- originalWrite(
201
- (0, import_openai_format.formatSSE)(
202
- (0, import_openai_format.toOpenAIStreamChunk)({
203
- id: completionId,
204
- model: body.model,
205
- delta: { content: String(event.body) }
204
+ var _a2;
205
+ pendingSse += typeof data === "string" ? data : data.toString("utf8");
206
+ const frames = pendingSse.split("\n\n");
207
+ pendingSse = frames.pop() || "";
208
+ for (const frame of frames) {
209
+ for (const line of frame.split("\n")) {
210
+ const trimmed = line.trim();
211
+ if (!trimmed.startsWith("data: ")) continue;
212
+ const jsonStr = trimmed.substring(6);
213
+ if (!jsonStr) continue;
214
+ try {
215
+ const event = JSON.parse(jsonStr);
216
+ if (event.type === "content" && event.body) {
217
+ originalWrite(
218
+ (0, import_openai_format.formatSSE)(
219
+ (0, import_openai_format.toOpenAIStreamChunk)({
220
+ id: completionId,
221
+ model: body.model,
222
+ delta: { content: String(event.body) }
223
+ })
224
+ )
225
+ );
226
+ } else if (event.type === "tool_call_chunks" && Array.isArray(event.body)) {
227
+ const chunks = toOpenAIToolCallChunks(event.body);
228
+ if (chunks.length) {
229
+ sawToolCalls = true;
230
+ originalWrite(
231
+ (0, import_openai_format.formatSSE)(
232
+ (0, import_openai_format.toOpenAIStreamChunk)({
233
+ id: completionId,
234
+ model: body.model,
235
+ delta: { tool_calls: chunks }
236
+ })
237
+ )
238
+ );
239
+ }
240
+ } else if (!sawToolCalls && event.type === "tool_calls" && Array.isArray((_a2 = event.body) == null ? void 0 : _a2.toolCalls)) {
241
+ const chunks = toOpenAIToolCallChunks(event.body.toolCalls);
242
+ if (chunks.length) {
243
+ sawToolCalls = true;
244
+ originalWrite(
245
+ (0, import_openai_format.formatSSE)(
246
+ (0, import_openai_format.toOpenAIStreamChunk)({
247
+ id: completionId,
248
+ model: body.model,
249
+ delta: { tool_calls: chunks }
250
+ })
251
+ )
252
+ );
253
+ }
254
+ } else if (event.type === "error" && event.body) {
255
+ originalWrite(
256
+ (0, import_openai_format.formatSSE)({
257
+ error: {
258
+ message: String(event.body),
259
+ type: "server_error",
260
+ code: "agent_error"
261
+ }
206
262
  })
207
- )
208
- );
209
- } else if (event.type === "error" && event.body) {
210
- originalWrite(
211
- (0, import_openai_format.formatSSE)({
212
- error: {
213
- message: String(event.body),
214
- type: "server_error",
215
- code: "agent_error"
216
- }
217
- })
218
- );
263
+ );
264
+ }
265
+ } catch {
219
266
  }
220
- } catch {
221
267
  }
222
268
  }
223
269
  return true;
@@ -229,7 +275,10 @@ async function handleAgentCompletions(ctx, plugin) {
229
275
  model: modelId
230
276
  })
231
277
  );
232
- await aiEmployee.stream({ userMessages });
278
+ streamSucceeded = await aiEmployee.stream({ userMessages });
279
+ if (!streamSucceeded) {
280
+ throw new Error("AI Employee stream failed");
281
+ }
233
282
  try {
234
283
  await (lifecycle == null ? void 0 : lifecycle.runAfterHooks(runtimeContext, { succeeded: true }));
235
284
  } finally {
@@ -238,18 +287,25 @@ async function handleAgentCompletions(ctx, plugin) {
238
287
  } finally {
239
288
  ctx.res.write = originalWrite;
240
289
  ctx.res.end = originalEnd;
241
- originalWrite(
242
- (0, import_openai_format.formatSSE)(
243
- (0, import_openai_format.toOpenAIStreamChunk)({
244
- id: completionId,
245
- model: body.model,
246
- delta: {},
247
- finishReason: "stop"
248
- })
249
- )
250
- );
251
- originalWrite((0, import_openai_format.formatSSEDone)());
252
- originalEnd();
290
+ ctx.req.off("aborted", abortAgent);
291
+ ctx.res.off("close", abortAgent);
292
+ if (streamSucceeded && !ctx.res.destroyed) {
293
+ originalWrite(
294
+ (0, import_openai_format.formatSSE)(
295
+ (0, import_openai_format.toOpenAIStreamChunk)({
296
+ id: completionId,
297
+ model: body.model,
298
+ delta: {},
299
+ finishReason: "stop"
300
+ })
301
+ )
302
+ );
303
+ originalWrite((0, import_openai_format.formatSSEDone)());
304
+ ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
305
+ } else {
306
+ ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "agent_error" };
307
+ }
308
+ if (!ctx.res.writableEnded && !ctx.res.destroyed) originalEnd();
253
309
  }
254
310
  } else {
255
311
  const aiEmployee = new AIEmployee(
@@ -312,6 +368,16 @@ async function handleAgentCompletions(ctx, plugin) {
312
368
  }
313
369
  }
314
370
  }
371
+ function toOpenAIToolCallChunks(value) {
372
+ return value.map((call, fallbackIndex) => ({
373
+ index: typeof call.index === "number" ? call.index : fallbackIndex,
374
+ ...call.id ? { id: String(call.id), type: "function" } : {},
375
+ function: {
376
+ ...call.name ? { name: String(call.name) } : {},
377
+ ...call.args !== void 0 ? { arguments: typeof call.args === "string" ? call.args : JSON.stringify(call.args) } : {}
378
+ }
379
+ }));
380
+ }
315
381
  function extractLastAiMessageContent(result) {
316
382
  var _a;
317
383
  if (!(result == null ? void 0 : result.messages) || !Array.isArray(result.messages)) return "";
@@ -31,6 +31,7 @@ __export(chat_completions_exports, {
31
31
  module.exports = __toCommonJS(chat_completions_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
+ var import_streaming = require("../utils/streaming");
34
35
  var import_role_permission = require("../middleware/role-permission");
35
36
  async function handleChatCompletions(ctx, plugin) {
36
37
  var _a;
@@ -55,7 +56,7 @@ async function handleChatCompletions(ctx, plugin) {
55
56
  );
56
57
  return;
57
58
  }
58
- const stream = body.stream === true;
59
+ const stream = (0, import_streaming.isStreamingRequested)(body.stream);
59
60
  const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
60
61
  if (!resolved) {
61
62
  ctx.status = 404;
@@ -150,10 +151,22 @@ async function handleChatCompletions(ctx, plugin) {
150
151
  const langchainMessages = messages.map((msg) => {
151
152
  const role = msg.role === "assistant" ? "ai" : msg.role;
152
153
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
154
+ if (msg.role === "assistant" && msg.tool_calls) {
155
+ return {
156
+ role,
157
+ content,
158
+ tool_calls: msg.tool_calls,
159
+ additional_kwargs: { tool_calls: msg.tool_calls }
160
+ };
161
+ }
162
+ if (msg.role === "tool") {
163
+ return { role: "tool", content, tool_call_id: msg.tool_call_id, name: msg.name };
164
+ }
153
165
  return [role, content];
154
166
  });
155
167
  const completionId = (0, import_openai_format.generateCompletionId)();
156
- const chatModel = provider.createModel();
168
+ const baseModel = provider.createModel();
169
+ const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice);
157
170
  if (stream) {
158
171
  await handleStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
159
172
  } else {
@@ -163,7 +176,7 @@ async function handleChatCompletions(ctx, plugin) {
163
176
  ctx.log.error("AI API chat completions error:", err);
164
177
  if (!ctx.res.headersSent) {
165
178
  ctx.status = 500;
166
- ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
179
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
167
180
  }
168
181
  }
169
182
  }
@@ -182,11 +195,13 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
182
195
  total_tokens: result.usage_metadata.total_tokens || 0
183
196
  } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
184
197
  ctx.status = 200;
198
+ const toolCalls = normalizeToolCalls(result.tool_calls);
185
199
  ctx.body = (0, import_openai_format.toOpenAIResponse)({
186
200
  id: completionId,
187
201
  model: modelName,
188
202
  content,
189
- usage
203
+ usage,
204
+ toolCalls
190
205
  });
191
206
  }
192
207
  async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
@@ -198,7 +213,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
198
213
  // Disable nginx buffering
199
214
  });
200
215
  ctx.status = 200;
201
- ctx.res.write(
216
+ await (0, import_streaming.writeResponse)(
217
+ ctx,
202
218
  (0, import_openai_format.formatSSE)(
203
219
  (0, import_openai_format.toOpenAIStreamChunk)({
204
220
  id: completionId,
@@ -207,9 +223,13 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
207
223
  })
208
224
  )
209
225
  );
226
+ const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
227
+ let usage;
228
+ let finishReason = "stop";
210
229
  try {
211
- const stream = await chatModel.stream(messages);
230
+ const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
212
231
  for await (const chunk of stream) {
232
+ if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
213
233
  let content = "";
214
234
  if (typeof chunk.content === "string") {
215
235
  content = chunk.content;
@@ -218,7 +238,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
218
238
  content = (textPart == null ? void 0 : textPart.text) || "";
219
239
  }
220
240
  if (content) {
221
- ctx.res.write(
241
+ await (0, import_streaming.writeResponse)(
242
+ ctx,
222
243
  (0, import_openai_format.formatSSE)(
223
244
  (0, import_openai_format.toOpenAIStreamChunk)({
224
245
  id: completionId,
@@ -228,31 +249,94 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
228
249
  )
229
250
  );
230
251
  }
252
+ const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
253
+ if (toolCallChunks.length) {
254
+ finishReason = "tool_calls";
255
+ await (0, import_streaming.writeResponse)(
256
+ ctx,
257
+ (0, import_openai_format.formatSSE)((0, import_openai_format.toOpenAIStreamChunk)({ id: completionId, model: modelName, delta: { tool_calls: toolCallChunks } }))
258
+ );
259
+ }
260
+ if (chunk.usage_metadata) {
261
+ usage = {
262
+ prompt_tokens: chunk.usage_metadata.input_tokens || 0,
263
+ completion_tokens: chunk.usage_metadata.output_tokens || 0,
264
+ total_tokens: chunk.usage_metadata.total_tokens || 0
265
+ };
266
+ }
231
267
  }
232
- ctx.res.write(
268
+ await (0, import_streaming.writeResponse)(
269
+ ctx,
233
270
  (0, import_openai_format.formatSSE)(
234
271
  (0, import_openai_format.toOpenAIStreamChunk)({
235
272
  id: completionId,
236
273
  model: modelName,
237
274
  delta: {},
238
- finishReason: "stop"
275
+ finishReason
239
276
  })
240
277
  )
241
278
  );
242
- ctx.res.write((0, import_openai_format.formatSSEDone)());
279
+ await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
280
+ ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
243
281
  } catch (err) {
244
282
  ctx.log.error("AI API streaming error:", err);
245
- ctx.res.write(
246
- (0, import_openai_format.formatSSE)({
247
- error: {
248
- message: err.message || "Streaming error",
249
- type: "server_error"
250
- }
251
- })
252
- );
283
+ if (!ctx.res.destroyed && !ctx.res.writableEnded) {
284
+ await (0, import_streaming.writeResponse)(
285
+ ctx,
286
+ (0, import_openai_format.formatSSE)({
287
+ error: {
288
+ message: getErrorMessage(err, "Streaming error"),
289
+ type: "server_error"
290
+ }
291
+ })
292
+ );
293
+ }
294
+ ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
253
295
  } finally {
254
- ctx.res.end();
296
+ requestAbort.dispose();
297
+ if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
298
+ }
299
+ }
300
+ function getErrorMessage(error, fallback) {
301
+ return error instanceof Error && error.message ? error.message : fallback;
302
+ }
303
+ function bindRequestTools(chatModel, tools, toolChoice) {
304
+ if (!Array.isArray(tools) || tools.length === 0) return chatModel;
305
+ if (typeof chatModel.bindTools !== "function") {
306
+ throw new Error("The selected LLM provider does not support tool calling");
255
307
  }
308
+ return chatModel.bindTools(tools, toolChoice === void 0 ? void 0 : { tool_choice: toolChoice });
309
+ }
310
+ function normalizeToolCalls(value) {
311
+ if (!Array.isArray(value) || value.length === 0) return void 0;
312
+ return value.map((call) => {
313
+ var _a, _b;
314
+ return {
315
+ id: String(call.id || ""),
316
+ type: "function",
317
+ function: {
318
+ name: String(call.name || ((_a = call.function) == null ? void 0 : _a.name) || ""),
319
+ arguments: serializeToolArguments(call.args ?? ((_b = call.function) == null ? void 0 : _b.arguments))
320
+ }
321
+ };
322
+ });
323
+ }
324
+ function normalizeToolCallChunks(value) {
325
+ if (!Array.isArray(value)) return [];
326
+ return value.map((call, fallbackIndex) => {
327
+ var _a, _b;
328
+ return {
329
+ index: typeof call.index === "number" ? call.index : fallbackIndex,
330
+ ...call.id ? { id: String(call.id), type: "function" } : {},
331
+ function: {
332
+ ...call.name ? { name: String(call.name) } : {},
333
+ ...call.args !== void 0 || ((_a = call.function) == null ? void 0 : _a.arguments) !== void 0 ? { arguments: serializeToolArguments(call.args ?? ((_b = call.function) == null ? void 0 : _b.arguments)) } : {}
334
+ }
335
+ };
336
+ });
337
+ }
338
+ function serializeToolArguments(value) {
339
+ return typeof value === "string" ? value : JSON.stringify(value ?? {});
256
340
  }
257
341
  // Annotate the CommonJS export names for ESM import in node:
258
342
  0 && (module.exports = {
@@ -31,6 +31,7 @@ __export(completions_exports, {
31
31
  module.exports = __toCommonJS(completions_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
+ var import_streaming = require("../utils/streaming");
34
35
  async function handleCompletions(ctx, plugin) {
35
36
  var _a;
36
37
  const body = ctx.request.body;
@@ -54,7 +55,7 @@ async function handleCompletions(ctx, plugin) {
54
55
  );
55
56
  return;
56
57
  }
57
- const stream = body.stream === true;
58
+ const stream = (0, import_streaming.isStreamingRequested)(body.stream);
58
59
  const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
59
60
  if (!resolved) {
60
61
  ctx.status = 404;
@@ -145,7 +146,7 @@ async function handleCompletions(ctx, plugin) {
145
146
  ctx.log.error("AI API completions error:", err);
146
147
  if (!ctx.res.headersSent) {
147
148
  ctx.status = 500;
148
- ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
149
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
149
150
  }
150
151
  }
151
152
  }
@@ -189,9 +190,12 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
189
190
  "X-Accel-Buffering": "no"
190
191
  });
191
192
  ctx.status = 200;
193
+ const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
194
+ let usage;
192
195
  try {
193
- const stream = await chatModel.stream(messages);
196
+ const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
194
197
  for await (const chunk of stream) {
198
+ if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
195
199
  let text = "";
196
200
  if (typeof chunk.content === "string") {
197
201
  text = chunk.content;
@@ -200,7 +204,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
200
204
  text = (textPart == null ? void 0 : textPart.text) || "";
201
205
  }
202
206
  if (text) {
203
- ctx.res.write(
207
+ await (0, import_streaming.writeResponse)(
208
+ ctx,
204
209
  (0, import_openai_format.formatSSE)({
205
210
  id: completionId,
206
211
  object: "text_completion",
@@ -218,8 +223,16 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
218
223
  })
219
224
  );
220
225
  }
226
+ if (chunk.usage_metadata) {
227
+ usage = {
228
+ prompt_tokens: chunk.usage_metadata.input_tokens || 0,
229
+ completion_tokens: chunk.usage_metadata.output_tokens || 0,
230
+ total_tokens: chunk.usage_metadata.total_tokens || 0
231
+ };
232
+ }
221
233
  }
222
- ctx.res.write(
234
+ await (0, import_streaming.writeResponse)(
235
+ ctx,
223
236
  (0, import_openai_format.formatSSE)({
224
237
  id: completionId,
225
238
  object: "text_completion",
@@ -236,21 +249,30 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
236
249
  ]
237
250
  })
238
251
  );
239
- ctx.res.write((0, import_openai_format.formatSSEDone)());
252
+ await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
253
+ ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
240
254
  } catch (err) {
241
255
  ctx.log.error("AI API completions streaming error:", err);
242
- ctx.res.write(
243
- (0, import_openai_format.formatSSE)({
244
- error: {
245
- message: err.message || "Streaming error",
246
- type: "server_error"
247
- }
248
- })
249
- );
256
+ if (!ctx.res.destroyed && !ctx.res.writableEnded) {
257
+ await (0, import_streaming.writeResponse)(
258
+ ctx,
259
+ (0, import_openai_format.formatSSE)({
260
+ error: {
261
+ message: getErrorMessage(err, "Streaming error"),
262
+ type: "server_error"
263
+ }
264
+ })
265
+ );
266
+ }
267
+ ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
250
268
  } finally {
251
- ctx.res.end();
269
+ requestAbort.dispose();
270
+ if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
252
271
  }
253
272
  }
273
+ function getErrorMessage(error, fallback) {
274
+ return error instanceof Error && error.message ? error.message : fallback;
275
+ }
254
276
  // Annotate the CommonJS export names for ESM import in node:
255
277
  0 && (module.exports = {
256
278
  handleCompletions
@@ -54,7 +54,7 @@ const API_PREFIX = "/api/ai-llm/v1";
54
54
  function createAiLlmRouter(plugin) {
55
55
  const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
56
56
  return async (ctx, next) => {
57
- var _a, _b;
57
+ var _a, _b, _c, _d;
58
58
  const { path, method } = ctx;
59
59
  if (!path.startsWith(API_PREFIX)) {
60
60
  return next();
@@ -117,7 +117,13 @@ function createAiLlmRouter(plugin) {
117
117
  if (method === "POST" && subPath === "/chat/completions") {
118
118
  const mode = await resolveMode(ctx);
119
119
  await (mode === "agent" ? (0, import_agent_completions.handleAgentCompletions)(ctx, plugin) : (0, import_chat_completions.handleChatCompletions)(ctx, plugin));
120
- logRequest(ctx, requestId, model, "ok", Date.now() - t0);
120
+ logRequest(
121
+ ctx,
122
+ requestId,
123
+ model,
124
+ ((_c = ctx.state.aiApiStreamResult) == null ? void 0 : _c.succeeded) === false ? "error" : "ok",
125
+ Date.now() - t0
126
+ );
121
127
  return;
122
128
  }
123
129
  if (method === "POST" && subPath === "/embeddings") {
@@ -137,7 +143,13 @@ function createAiLlmRouter(plugin) {
137
143
  } else {
138
144
  await (0, import_completions.handleCompletions)(ctx, plugin);
139
145
  }
140
- logRequest(ctx, requestId, model, "ok", Date.now() - t0);
146
+ logRequest(
147
+ ctx,
148
+ requestId,
149
+ model,
150
+ ((_d = ctx.state.aiApiStreamResult) == null ? void 0 : _d.succeeded) === false ? "error" : "ok",
151
+ Date.now() - t0
152
+ );
141
153
  return;
142
154
  }
143
155
  if (method === "GET" && subPath === "/models") {
@@ -177,7 +189,11 @@ function createAiLlmRouter(plugin) {
177
189
  logRequest(ctx, requestId, model, "error", Date.now() - t0);
178
190
  if (!ctx.res.headersSent) {
179
191
  ctx.status = 500;
180
- ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
192
+ ctx.body = (0, import_openai_format.toOpenAIError)(
193
+ 500,
194
+ err instanceof Error && err.message ? err.message : "Internal server error",
195
+ "server_error"
196
+ );
181
197
  }
182
198
  } finally {
183
199
  if (usageId !== void 0) {
@@ -58,15 +58,16 @@ async function startUsageRecord(ctx, requestId, endpoint, model, streaming) {
58
58
  async function finishUsageRecord(ctx, id, startedAt, status) {
59
59
  var _a;
60
60
  const response = ctx.body || {};
61
- const usage = response.usage;
61
+ const streamResult = ctx.state.aiApiStreamResult;
62
+ const usage = response.usage || (streamResult == null ? void 0 : streamResult.usage);
62
63
  const values = {
63
- status,
64
+ status: streamResult ? streamResult.succeeded ? "succeeded" : "failed" : status,
64
65
  httpStatus: ctx.status,
65
- errorCode: (_a = response.error) == null ? void 0 : _a.code,
66
+ errorCode: ((_a = response.error) == null ? void 0 : _a.code) || (streamResult == null ? void 0 : streamResult.errorCode),
66
67
  inputTokens: usage == null ? void 0 : usage.prompt_tokens,
67
68
  outputTokens: usage == null ? void 0 : usage.completion_tokens,
68
69
  totalTokens: usage == null ? void 0 : usage.total_tokens,
69
- providerRequestId: response.id,
70
+ providerRequestId: response.id || (streamResult == null ? void 0 : streamResult.id),
70
71
  completedAt: /* @__PURE__ */ new Date(),
71
72
  durationMs: Date.now() - startedAt,
72
73
  responseMetadata: { usageSource: usage ? "response" : "unavailable" }
@@ -71,7 +71,15 @@ function toOpenAIError(statusCode, message, type = "invalid_request_error", code
71
71
  };
72
72
  }
73
73
  function toOpenAIResponse(options) {
74
- const { id, model, content, finishReason = "stop", usage } = options;
74
+ var _a;
75
+ const {
76
+ id,
77
+ model,
78
+ content,
79
+ finishReason = ((_a = options.toolCalls) == null ? void 0 : _a.length) ? "tool_calls" : "stop",
80
+ usage,
81
+ toolCalls
82
+ } = options;
75
83
  return {
76
84
  id,
77
85
  object: "chat.completion",
@@ -83,7 +91,8 @@ function toOpenAIResponse(options) {
83
91
  index: 0,
84
92
  message: {
85
93
  role: "assistant",
86
- content
94
+ content,
95
+ ...(toolCalls == null ? void 0 : toolCalls.length) ? { tool_calls: toolCalls } : {}
87
96
  },
88
97
  logprobs: null,
89
98
  finish_reason: finishReason