plugin-ai-api 1.0.13 → 1.0.14

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,15 @@
10
10
  module.exports = {
11
11
  "react": "18.2.0",
12
12
  "antd": "5.24.2",
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",
13
+ "@nocobase/client-v2": "2.1.30",
14
+ "@nocobase/flow-engine": "2.1.30",
15
+ "@nocobase/client": "2.1.30",
16
+ "@nocobase/plugin-acl": "2.1.30",
17
+ "@nocobase/server": "2.1.30",
18
18
  "dayjs": "1.11.13",
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"
19
+ "@nocobase/actions": "2.1.30",
20
+ "@nocobase/database": "2.1.30",
21
+ "sequelize": "6.35.2",
22
+ "@nocobase/resourcer": "2.1.30",
23
+ "@nocobase/plugin-ai": "2.1.30"
23
24
  };
@@ -35,7 +35,15 @@ var ai_api_usage_records_default = (0, import_database.defineCollection)({
35
35
  autoGenId: true,
36
36
  fields: [
37
37
  { name: "requestId", type: "string", unique: true, index: true },
38
- { name: "userId", type: "string", index: true },
38
+ { name: "userId", type: "bigInt", allowNull: false, index: true },
39
+ {
40
+ name: "user",
41
+ type: "belongsTo",
42
+ target: "users",
43
+ targetKey: "id",
44
+ foreignKey: "userId",
45
+ constraints: false
46
+ },
39
47
  { name: "roleName", type: "string", index: true },
40
48
  { name: "authType", type: "string", index: true },
41
49
  { name: "oauthClientId", type: "string", allowNull: true, index: true },
@@ -0,0 +1,208 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var change_usage_user_id_to_bigint_exports = {};
28
+ __export(change_usage_user_id_to_bigint_exports, {
29
+ default: () => ChangeUsageUserIdToBigInt,
30
+ isBigIntColumnType: () => isBigIntColumnType,
31
+ isLegacyStringColumnType: () => isLegacyStringColumnType,
32
+ normalizeLegacyUserId: () => normalizeLegacyUserId
33
+ });
34
+ module.exports = __toCommonJS(change_usage_user_id_to_bigint_exports);
35
+ var import_database = require("@nocobase/database");
36
+ var import_server = require("@nocobase/server");
37
+ var import_sequelize = require("sequelize");
38
+ const MAX_SIGNED_BIGINT = 9223372036854775807n;
39
+ const PREFLIGHT_BATCH_SIZE = 1e3;
40
+ function stringifyDatabaseValue(value) {
41
+ if (typeof value === "bigint") return value.toString();
42
+ if (value === null) return "null";
43
+ if (value === void 0) return "undefined";
44
+ return String(value);
45
+ }
46
+ function normalizeLegacyUserId(value) {
47
+ let normalized;
48
+ if (typeof value === "string") {
49
+ normalized = value.trim();
50
+ } else if (typeof value === "bigint") {
51
+ normalized = value.toString();
52
+ } else if (typeof value === "number" && Number.isSafeInteger(value)) {
53
+ normalized = String(value);
54
+ } else {
55
+ return void 0;
56
+ }
57
+ if (!/^\d+$/.test(normalized)) return void 0;
58
+ const parsed = BigInt(normalized);
59
+ if (parsed > MAX_SIGNED_BIGINT) return void 0;
60
+ return normalized;
61
+ }
62
+ function isBigIntColumnType(value) {
63
+ return typeof value === "string" && value.toUpperCase().includes("BIGINT");
64
+ }
65
+ function isLegacyStringColumnType(value) {
66
+ if (typeof value !== "string") return false;
67
+ const normalized = value.toUpperCase();
68
+ return normalized.includes("CHAR") || normalized.includes("TEXT");
69
+ }
70
+ class ChangeUsageUserIdToBigInt extends import_server.Migration {
71
+ on = "beforeLoad";
72
+ async up() {
73
+ const { collection, temporary } = this.getUsageCollection();
74
+ try {
75
+ if (!await collection.existsInDb()) return;
76
+ const userIdField = collection.getField("userId");
77
+ if (!userIdField) {
78
+ throw new Error("AI API usage migration could not resolve the userId field.");
79
+ }
80
+ const tableName = collection.getTableNameWithSchema();
81
+ const columnName = userIdField.columnName();
82
+ const columns = await this.queryInterface.describeTable(tableName);
83
+ const column = columns[columnName];
84
+ if (!column) {
85
+ throw new Error(`AI API usage migration could not find the physical column ${columnName}.`);
86
+ }
87
+ if (!isBigIntColumnType(column.type) && !isLegacyStringColumnType(column.type)) {
88
+ throw new Error(`AI API usage migration does not support converting ${column.type} to BIGINT.`);
89
+ }
90
+ const invalidRows = await this.findInvalidRows(collection.quotedTableName(), columnName);
91
+ if (invalidRows.count > 0) {
92
+ const samples = invalidRows.samples.map((row) => `${row.recordId}:${JSON.stringify(row.userId)}`).join(", ");
93
+ throw new Error(
94
+ `AI API usage migration found ${invalidRows.count} invalid userId value(s). Clean these records before retrying the upgrade. Samples: ${samples}`
95
+ );
96
+ }
97
+ if (isBigIntColumnType(column.type)) {
98
+ if (column.allowNull === false) return;
99
+ await this.makeColumnNotNull(tableName, collection.quotedTableName(), columnName);
100
+ return;
101
+ }
102
+ await this.changeColumnToBigInt(tableName, collection.quotedTableName(), columnName);
103
+ } finally {
104
+ if (temporary) this.db.removeCollection("aiApiUsageRecords");
105
+ }
106
+ }
107
+ async down() {
108
+ var _a;
109
+ const { collection, temporary } = this.getUsageCollection();
110
+ try {
111
+ if (!await collection.existsInDb()) return;
112
+ const userIdField = collection.getField("userId");
113
+ if (!userIdField) return;
114
+ const tableName = collection.getTableNameWithSchema();
115
+ const columnName = userIdField.columnName();
116
+ const columns = await this.queryInterface.describeTable(tableName);
117
+ if (!isBigIntColumnType((_a = columns[columnName]) == null ? void 0 : _a.type)) return;
118
+ if (this.db.inDialect("postgres")) {
119
+ const quotedColumn = this.db.quoteIdentifier(columnName);
120
+ await this.sequelize.transaction(async (transaction) => {
121
+ await this.sequelize.query(
122
+ `ALTER TABLE ${collection.quotedTableName()} ALTER COLUMN ${quotedColumn} DROP NOT NULL, ALTER COLUMN ${quotedColumn} TYPE VARCHAR(255) USING ${quotedColumn}::text`,
123
+ { transaction }
124
+ );
125
+ });
126
+ return;
127
+ }
128
+ await this.queryInterface.changeColumn(tableName, columnName, {
129
+ type: import_database.DataTypes.STRING,
130
+ allowNull: true
131
+ });
132
+ } finally {
133
+ if (temporary) this.db.removeCollection("aiApiUsageRecords");
134
+ }
135
+ }
136
+ getUsageCollection() {
137
+ const existing = this.db.getCollection("aiApiUsageRecords");
138
+ if (existing) return { collection: existing, temporary: false };
139
+ const collection = this.db.collection({
140
+ name: "aiApiUsageRecords",
141
+ fields: [{ name: "userId", type: "string" }]
142
+ });
143
+ return { collection, temporary: true };
144
+ }
145
+ async findInvalidRows(quotedTableName, columnName) {
146
+ const quotedId = this.db.quoteIdentifier("id");
147
+ const quotedUserId = this.db.quoteIdentifier(columnName);
148
+ const invalidRows = { count: 0, samples: [] };
149
+ let offset = 0;
150
+ let hasMoreRows = true;
151
+ while (hasMoreRows) {
152
+ const rows = await this.sequelize.query(
153
+ `SELECT ${quotedId} AS ${this.db.quoteIdentifier("recordId")}, ${quotedUserId} AS ${this.db.quoteIdentifier("userId")} FROM ${quotedTableName} ORDER BY ${quotedId} ASC LIMIT :limit OFFSET :offset`,
154
+ {
155
+ replacements: { limit: PREFLIGHT_BATCH_SIZE, offset },
156
+ type: import_sequelize.QueryTypes.SELECT
157
+ }
158
+ );
159
+ for (const row of rows) {
160
+ if (normalizeLegacyUserId(row.userId) === void 0) {
161
+ invalidRows.count += 1;
162
+ if (invalidRows.samples.length < 20) {
163
+ invalidRows.samples.push({
164
+ recordId: stringifyDatabaseValue(row.recordId),
165
+ userId: stringifyDatabaseValue(row.userId)
166
+ });
167
+ }
168
+ }
169
+ }
170
+ hasMoreRows = rows.length === PREFLIGHT_BATCH_SIZE;
171
+ offset += rows.length;
172
+ }
173
+ return invalidRows;
174
+ }
175
+ async changeColumnToBigInt(tableName, quotedTableName, columnName) {
176
+ if (this.db.inDialect("postgres")) {
177
+ const quotedColumn = this.db.quoteIdentifier(columnName);
178
+ await this.sequelize.transaction(async (transaction) => {
179
+ await this.sequelize.query(
180
+ `ALTER TABLE ${quotedTableName} ALTER COLUMN ${quotedColumn} TYPE BIGINT USING BTRIM(${quotedColumn})::BIGINT, ALTER COLUMN ${quotedColumn} SET NOT NULL`,
181
+ { transaction }
182
+ );
183
+ });
184
+ return;
185
+ }
186
+ await this.queryInterface.changeColumn(tableName, columnName, {
187
+ type: import_database.DataTypes.BIGINT,
188
+ allowNull: false
189
+ });
190
+ }
191
+ async makeColumnNotNull(tableName, quotedTableName, columnName) {
192
+ if (this.db.inDialect("postgres")) {
193
+ const quotedColumn = this.db.quoteIdentifier(columnName);
194
+ await this.sequelize.query(`ALTER TABLE ${quotedTableName} ALTER COLUMN ${quotedColumn} SET NOT NULL`);
195
+ return;
196
+ }
197
+ await this.queryInterface.changeColumn(tableName, columnName, {
198
+ type: import_database.DataTypes.BIGINT,
199
+ allowNull: false
200
+ });
201
+ }
202
+ }
203
+ // Annotate the CommonJS export names for ESM import in node:
204
+ 0 && (module.exports = {
205
+ isBigIntColumnType,
206
+ isLegacyStringColumnType,
207
+ normalizeLegacyUserId
208
+ });
@@ -34,6 +34,7 @@ var import_resolve_service = require("../utils/resolve-service");
34
34
  var import_role_permission = require("../middleware/role-permission");
35
35
  var import_streaming = require("../utils/streaming");
36
36
  var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
37
+ var import_usage = require("../usage");
37
38
  async function handleAgentCompletions(ctx, plugin) {
38
39
  var _a;
39
40
  const body = ctx.request.body;
@@ -301,8 +302,10 @@ async function handleAgentCompletions(ctx, plugin) {
301
302
  )
302
303
  );
303
304
  originalWrite((0, import_openai_format.formatSSEDone)());
305
+ (0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
304
306
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
305
307
  } else {
308
+ (0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
306
309
  ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "agent_error" };
307
310
  }
308
311
  if (!ctx.res.writableEnded && !ctx.res.destroyed) originalEnd();
@@ -342,6 +345,7 @@ async function handleAgentCompletions(ctx, plugin) {
342
345
  // Clients can detect agent mode by checking usage.total_tokens === 0.
343
346
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
344
347
  };
348
+ (0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
345
349
  }
346
350
  } catch (err) {
347
351
  const error = err instanceof Error ? err : new Error(String(err));
@@ -51,6 +51,7 @@ async function authenticateBearer(ctx) {
51
51
  }
52
52
  try {
53
53
  if (ctx.state.currentUser) {
54
+ ctx.state.aiApiAuthType = ctx.state.oauthPrincipal ? "oidc" : "bearer";
54
55
  if (!ctx.state.currentRole) {
55
56
  const requestedRole = ctx.get("X-Role");
56
57
  const rolesRepository2 = ctx.db.getRepository("users.roles", ctx.state.currentUser.id);
@@ -35,6 +35,7 @@ var import_openai_format = require("../utils/openai-format");
35
35
  var import_resolve_service = require("../utils/resolve-service");
36
36
  var import_streaming = require("../utils/streaming");
37
37
  var import_role_permission = require("../middleware/role-permission");
38
+ var import_usage = require("../usage");
38
39
  async function handleChatCompletions(ctx, plugin) {
39
40
  var _a;
40
41
  const body = ctx.request.body;
@@ -208,18 +209,17 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
208
209
  const textPart = result.content.find((c) => c.type === "text");
209
210
  content = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
210
211
  }
211
- const usage = result.usage_metadata ? {
212
- prompt_tokens: result.usage_metadata.input_tokens || 0,
213
- completion_tokens: result.usage_metadata.output_tokens || 0,
214
- total_tokens: result.usage_metadata.total_tokens || 0
215
- } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
212
+ const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
213
+ gatewayResponseId: completionId,
214
+ providerRequestId: (0, import_usage.extractProviderRequestId)(result)
215
+ });
216
216
  ctx.status = 200;
217
217
  const toolCalls = normalizeToolCalls(result.tool_calls);
218
218
  ctx.body = (0, import_openai_format.toOpenAIResponse)({
219
219
  id: completionId,
220
220
  model: modelName,
221
221
  content,
222
- usage,
222
+ usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
223
223
  toolCalls
224
224
  });
225
225
  }
@@ -244,6 +244,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
244
244
  );
245
245
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
246
246
  let usage;
247
+ let providerRequestId;
247
248
  let finishReason = "stop";
248
249
  try {
249
250
  const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
@@ -277,12 +278,9 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
277
278
  );
278
279
  }
279
280
  if (chunk.usage_metadata) {
280
- usage = {
281
- prompt_tokens: chunk.usage_metadata.input_tokens || 0,
282
- completion_tokens: chunk.usage_metadata.output_tokens || 0,
283
- total_tokens: chunk.usage_metadata.total_tokens || 0
284
- };
281
+ usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
285
282
  }
283
+ providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
286
284
  }
287
285
  await (0, import_streaming.writeResponse)(
288
286
  ctx,
@@ -296,7 +294,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
296
294
  )
297
295
  );
298
296
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
299
- ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
297
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
298
+ ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
300
299
  } catch (err) {
301
300
  ctx.log.error("AI API streaming error:", err);
302
301
  if (!ctx.res.destroyed && !ctx.res.writableEnded) {
@@ -310,7 +309,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
310
309
  })
311
310
  );
312
311
  }
313
- ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
312
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
313
+ ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "stream_error" };
314
314
  } finally {
315
315
  requestAbort.dispose();
316
316
  if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
@@ -32,6 +32,7 @@ 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
34
  var import_streaming = require("../utils/streaming");
35
+ var import_usage = require("../usage");
35
36
  async function handleCompletions(ctx, plugin) {
36
37
  var _a;
37
38
  const body = ctx.request.body;
@@ -159,11 +160,10 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
159
160
  const textPart = result.content.find((c) => c.type === "text");
160
161
  text = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
161
162
  }
162
- const usage = result.usage_metadata ? {
163
- prompt_tokens: result.usage_metadata.input_tokens || 0,
164
- completion_tokens: result.usage_metadata.output_tokens || 0,
165
- total_tokens: result.usage_metadata.total_tokens || 0
166
- } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
163
+ const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
164
+ gatewayResponseId: completionId,
165
+ providerRequestId: (0, import_usage.extractProviderRequestId)(result)
166
+ });
167
167
  ctx.status = 200;
168
168
  ctx.body = {
169
169
  id: completionId,
@@ -179,7 +179,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
179
179
  finish_reason: "stop"
180
180
  }
181
181
  ],
182
- usage
182
+ usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
183
183
  };
184
184
  }
185
185
  async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
@@ -192,6 +192,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
192
192
  ctx.status = 200;
193
193
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
194
194
  let usage;
195
+ let providerRequestId;
195
196
  try {
196
197
  const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
197
198
  for await (const chunk of stream) {
@@ -224,12 +225,9 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
224
225
  );
225
226
  }
226
227
  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
- };
228
+ usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
232
229
  }
230
+ providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
233
231
  }
234
232
  await (0, import_streaming.writeResponse)(
235
233
  ctx,
@@ -250,7 +248,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
250
248
  })
251
249
  );
252
250
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
253
- ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
251
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
252
+ ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
254
253
  } catch (err) {
255
254
  ctx.log.error("AI API completions streaming error:", err);
256
255
  if (!ctx.res.destroyed && !ctx.res.writableEnded) {
@@ -264,7 +263,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
264
263
  })
265
264
  );
266
265
  }
267
- ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
266
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
267
+ ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "stream_error" };
268
268
  } finally {
269
269
  requestAbort.dispose();
270
270
  if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
@@ -31,6 +31,7 @@ __export(embeddings_exports, {
31
31
  module.exports = __toCommonJS(embeddings_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
+ var import_usage = require("../usage");
34
35
  async function handleEmbeddings(ctx, plugin) {
35
36
  var _a;
36
37
  const body = ctx.request.body;
@@ -153,6 +154,7 @@ async function handleEmbeddings(ctx, plugin) {
153
154
  const vectors = await embeddingModel.embedDocuments(inputs);
154
155
  ctx.status = 200;
155
156
  ctx.set("Content-Type", "application/json");
157
+ (0, import_usage.setAiApiUsageUnavailable)(ctx);
156
158
  ctx.body = (0, import_openai_format.toOpenAIEmbeddingsResponse)({
157
159
  model: body.model,
158
160
  embeddings: vectors,
@@ -55,7 +55,7 @@ const API_PREFIX = "/api/ai-llm/v1";
55
55
  function createAiLlmRouter(plugin) {
56
56
  const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
57
57
  return async (ctx, next) => {
58
- var _a, _b, _c;
58
+ var _a, _b;
59
59
  const { path, method } = ctx;
60
60
  if (!path.startsWith(API_PREFIX)) {
61
61
  return next();
@@ -78,7 +78,7 @@ function createAiLlmRouter(plugin) {
78
78
  const rawBody = await getRawBody(ctx);
79
79
  ctx.request.body = JSON.parse(rawBody);
80
80
  } catch (bodyErr) {
81
- const status = (bodyErr == null ? void 0 : bodyErr.statusCode) === 413 ? 413 : 400;
81
+ const status = bodyErr && typeof bodyErr === "object" && "statusCode" in bodyErr && bodyErr.statusCode === 413 ? 413 : 400;
82
82
  const message = status === 413 ? "Request body too large (max 10 MB)" : "Invalid JSON in request body";
83
83
  ctx.status = status;
84
84
  ctx.body = (0, import_openai_format.toOpenAIError)(status, message, "invalid_request_error");
@@ -100,12 +100,13 @@ function createAiLlmRouter(plugin) {
100
100
  logRequest(ctx, requestId, "-", "rate_limited", 0);
101
101
  return;
102
102
  }
103
- const model = ((_a = ctx.request.body) == null ? void 0 : _a.model) ?? "-";
103
+ const requestBody = ctx.request.body || {};
104
+ const model = requestBody.model === void 0 || requestBody.model === null ? "-" : String(requestBody.model);
104
105
  const isUsageEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/embeddings");
106
+ const isStreamingEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions");
105
107
  const resolvedMode = isUsageEndpoint ? await resolveMode(ctx) : "llm";
106
- const requestBody = ctx.request.body || {};
107
- const streaming = isUsageEndpoint && (0, import_streaming.isStreamingRequested)(requestBody.stream);
108
- if (streaming && (subPath === "/chat/completions" || subPath === "/completions")) {
108
+ const streaming = isStreamingEndpoint && (0, import_streaming.isStreamingRequested)(requestBody.stream);
109
+ if (streaming) {
109
110
  const streamOptions = requestBody.stream_options;
110
111
  ctx.request.body = {
111
112
  ...requestBody,
@@ -118,7 +119,7 @@ function createAiLlmRouter(plugin) {
118
119
  const t0 = Date.now();
119
120
  let usageId;
120
121
  try {
121
- usageId = isUsageEndpoint ? await (0, import_usage.startUsageRecord)(ctx, requestId, subPath, String(model), streaming, resolvedMode) : void 0;
122
+ usageId = isUsageEndpoint ? await (0, import_usage.startUsageRecord)(ctx, requestId, subPath, model, streaming, resolvedMode) : void 0;
122
123
  } catch (usageError) {
123
124
  ctx.log.error("AI API usage record could not be created:", usageError);
124
125
  }
@@ -129,7 +130,7 @@ function createAiLlmRouter(plugin) {
129
130
  ctx,
130
131
  requestId,
131
132
  model,
132
- ((_b = ctx.state.aiApiStreamResult) == null ? void 0 : _b.succeeded) === false ? "error" : "ok",
133
+ ((_a = ctx.state.aiApiStreamResult) == null ? void 0 : _a.succeeded) === false ? "error" : "ok",
133
134
  Date.now() - t0
134
135
  );
135
136
  return;
@@ -142,8 +143,8 @@ function createAiLlmRouter(plugin) {
142
143
  if (method === "POST" && subPath === "/completions") {
143
144
  const completionsMode = resolvedMode;
144
145
  if (completionsMode === "agent") {
145
- const reqBody = ctx.request.body;
146
- if ((reqBody == null ? void 0 : reqBody.prompt) !== void 0) {
146
+ const reqBody = ctx.request.body || {};
147
+ if (reqBody.prompt !== void 0) {
147
148
  const prompt = typeof reqBody.prompt === "string" ? reqBody.prompt : Array.isArray(reqBody.prompt) ? reqBody.prompt.join("\n") : String(reqBody.prompt);
148
149
  ctx.request.body = { ...reqBody, messages: [{ role: "user", content: prompt }] };
149
150
  }
@@ -155,7 +156,7 @@ function createAiLlmRouter(plugin) {
155
156
  ctx,
156
157
  requestId,
157
158
  model,
158
- ((_c = ctx.state.aiApiStreamResult) == null ? void 0 : _c.succeeded) === false ? "error" : "ok",
159
+ ((_b = ctx.state.aiApiStreamResult) == null ? void 0 : _b.succeeded) === false ? "error" : "ok",
159
160
  Date.now() - t0
160
161
  );
161
162
  return;