plugin-ai-api 1.0.24 → 1.0.28

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 (130) hide show
  1. package/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
  2. package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
  3. package/dist/client/562.44b16aad4718b4c7.js +10 -0
  4. package/dist/client/685.ae483e17b6b49c98.js +10 -0
  5. package/dist/client/757.6568d3504ad29352.js +10 -0
  6. package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
  7. package/dist/client/index.js +1 -1
  8. package/dist/client-v2/302.3971233415999b2c.js +10 -0
  9. package/dist/client-v2/562.45d5c504433be38b.js +10 -0
  10. package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
  11. package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
  12. package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
  13. package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
  14. package/dist/client-v2/index.js +1 -1
  15. package/dist/constants.js +2 -5
  16. package/dist/externalVersion.js +8 -8
  17. package/dist/locale/en-US.json +27 -8
  18. package/dist/locale/vi-VN.json +27 -8
  19. package/dist/locale/zh-CN.json +27 -8
  20. package/dist/server/billing.js +31 -33
  21. package/dist/server/collections/ai-api-config.js +7 -7
  22. package/dist/server/collections/ai-api-group-members.js +62 -0
  23. package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
  24. package/dist/server/collections/ai-api-model-metadata.js +6 -0
  25. package/dist/server/collections/ai-api-usage-groups.js +74 -0
  26. package/dist/server/collections/ai-api-usage-records.js +2 -0
  27. package/dist/server/middleware/rate-limit.js +7 -6
  28. package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
  29. package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
  30. package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
  31. package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
  32. package/dist/server/plugin.js +100 -22
  33. package/dist/server/quota-groups.js +108 -0
  34. package/dist/server/resource/ai-api-config.js +5 -3
  35. package/dist/server/resource/ai-api-usage-groups.js +168 -0
  36. package/dist/server/resource/ai-api-usage-monitor.js +3 -1
  37. package/dist/server/routes/agent-completions.js +2 -1
  38. package/dist/server/routes/chat-completions.js +121 -42
  39. package/dist/server/routes/completions.js +48 -29
  40. package/dist/server/routes/embeddings.js +2 -1
  41. package/dist/server/routes/models.js +2 -1
  42. package/dist/server/routes/router.js +3 -2
  43. package/dist/server/services/file-processor.js +426 -0
  44. package/dist/server/usage.js +37 -3
  45. package/dist/server/utils/direct-llm-context.js +163 -26
  46. package/dist/server/utils/openai-format.js +21 -2
  47. package/dist/server/utils/rate-limiter.js +1 -1
  48. package/dist/server/utils/request-cache.js +61 -0
  49. package/dist/server/utils/resolve-service.js +2 -1
  50. package/dist/server/utils/user-permissions.js +25 -39
  51. package/dist/server/validation.js +7 -0
  52. package/dist/swagger.js +48 -10
  53. package/package.json +1 -1
  54. package/src/client/__tests__/settings-registration.test.tsx +6 -29
  55. package/src/client/plugin.tsx +5 -16
  56. package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
  57. package/src/client-v2/locale.ts +3 -1
  58. package/src/client-v2/pages/GeneralPage.tsx +0 -5
  59. package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
  60. package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
  61. package/src/client-v2/pages/UsagePage.tsx +9 -0
  62. package/src/client-v2/plugin.tsx +4 -13
  63. package/src/constants.ts +0 -7
  64. package/src/locale/en-US.json +27 -8
  65. package/src/locale/vi-VN.json +27 -8
  66. package/src/locale/zh-CN.json +27 -8
  67. package/src/server/__tests__/billing-quota.test.ts +28 -9
  68. package/src/server/__tests__/direct-llm-context.test.ts +209 -10
  69. package/src/server/__tests__/file-processor.test.ts +225 -0
  70. package/src/server/__tests__/models.test.ts +1 -1
  71. package/src/server/__tests__/openai-format.test.ts +12 -2
  72. package/src/server/__tests__/permission-sync.test.ts +34 -35
  73. package/src/server/__tests__/request-body.test.ts +45 -2
  74. package/src/server/__tests__/usage-groups.test.ts +160 -0
  75. package/src/server/__tests__/usage-monitor.test.ts +2 -0
  76. package/src/server/__tests__/usage-route.test.ts +382 -5
  77. package/src/server/__tests__/usage.test.ts +57 -0
  78. package/src/server/__tests__/user-permissions.test.ts +214 -133
  79. package/src/server/__tests__/validation.test.ts +11 -0
  80. package/src/server/billing.ts +36 -39
  81. package/src/server/collections/ai-api-config.ts +9 -7
  82. package/src/server/collections/ai-api-group-members.ts +41 -0
  83. package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
  84. package/src/server/collections/ai-api-model-metadata.ts +7 -0
  85. package/src/server/collections/ai-api-role-permissions.ts +41 -41
  86. package/src/server/collections/ai-api-usage-groups.ts +53 -0
  87. package/src/server/collections/ai-api-usage-records.ts +2 -0
  88. package/src/server/index.ts +10 -10
  89. package/src/server/middleware/rate-limit.ts +68 -70
  90. package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
  91. package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
  92. package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
  93. package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
  94. package/src/server/plugin.ts +121 -30
  95. package/src/server/quota-groups.ts +117 -0
  96. package/src/server/resource/ai-api-config.ts +5 -3
  97. package/src/server/resource/ai-api-usage-groups.ts +171 -0
  98. package/src/server/resource/ai-api-usage-monitor.ts +3 -0
  99. package/src/server/routes/agent-completions.ts +2 -1
  100. package/src/server/routes/chat-completions.ts +173 -47
  101. package/src/server/routes/completions.ts +50 -27
  102. package/src/server/routes/embeddings.ts +2 -1
  103. package/src/server/routes/models.ts +4 -3
  104. package/src/server/routes/router.ts +4 -3
  105. package/src/server/services/__tests__/file-processor.test.ts +184 -0
  106. package/src/server/services/file-processor.ts +513 -0
  107. package/src/server/usage.ts +51 -1
  108. package/src/server/utils/direct-llm-context.ts +218 -31
  109. package/src/server/utils/openai-format.ts +25 -2
  110. package/src/server/utils/rate-limiter.ts +83 -83
  111. package/src/server/utils/request-cache.ts +59 -0
  112. package/src/server/utils/resolve-service.ts +83 -82
  113. package/src/server/utils/user-permissions.ts +49 -69
  114. package/src/server/validation.ts +7 -0
  115. package/src/swagger.ts +52 -11
  116. package/dist/client/123.e6fe04c856ce6417.js +0 -10
  117. package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
  118. package/dist/client/562.17a0a299d2e5152c.js +0 -10
  119. package/dist/client/757.a01403fb7a1bea01.js +0 -10
  120. package/dist/client/902.e74518750f1e4201.js +0 -10
  121. package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
  122. package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
  123. package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
  124. package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
  125. package/dist/client-v2/902.c7c00a565085438a.js +0 -10
  126. package/dist/server/resource/ai-api-user-permissions.js +0 -75
  127. package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
  128. package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
  129. package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
  130. package/src/server/resource/ai-api-user-permissions.ts +0 -76
@@ -27,6 +27,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
27
27
  var chat_completions_exports = {};
28
28
  __export(chat_completions_exports, {
29
29
  applyProviderRequestParameters: () => applyProviderRequestParameters,
30
+ extractFinishReason: () => extractFinishReason,
30
31
  findContentBlockProblem: () => findContentBlockProblem,
31
32
  findMessageProblem: () => findMessageProblem,
32
33
  getProviderRequestParameters: () => getProviderRequestParameters,
@@ -37,12 +38,13 @@ module.exports = __toCommonJS(chat_completions_exports);
37
38
  var import_openai_format = require("../utils/openai-format");
38
39
  var import_resolve_service = require("../utils/resolve-service");
39
40
  var import_streaming = require("../utils/streaming");
40
- var import_role_permission = require("../middleware/role-permission");
41
41
  var import_user_permissions = require("../utils/user-permissions");
42
+ var import_request_cache = require("../utils/request-cache");
42
43
  var import_usage = require("../usage");
43
44
  var import_billing = require("../billing");
44
45
  var import_direct_llm_context = require("../utils/direct-llm-context");
45
46
  var import_app_observability = require("../utils/app-observability");
47
+ var import_file_processor = require("../services/file-processor");
46
48
  async function handleChatCompletions(ctx, plugin) {
47
49
  var _a;
48
50
  const body = ctx.request.body;
@@ -118,7 +120,7 @@ async function handleChatCompletions(ctx, plugin) {
118
120
  );
119
121
  return;
120
122
  }
121
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
123
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
122
124
  if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
123
125
  return;
124
126
  }
@@ -147,30 +149,13 @@ async function handleChatCompletions(ctx, plugin) {
147
149
  if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
148
150
  if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
149
151
  if (body.stop !== void 0) modelOptions.stop = body.stop;
150
- let systemPrompt = "";
151
- if (config == null ? void 0 : config.defaultAiEmployee) {
152
- if (!(0, import_role_permission.checkEmployeeAccess)(ctx, config.defaultAiEmployee)) {
153
- ctx.status = 403;
154
- ctx.body = (0, import_openai_format.toOpenAIError)(
155
- 403,
156
- `Role is not permitted to use AI Employee '${config.defaultAiEmployee}'. An admin must grant access in Settings \u2192 Users & Permissions \u2192 [Role] \u2192 AI API.`,
157
- "permission_denied",
158
- "employee_not_permitted"
159
- );
160
- return;
161
- }
162
- const employee = await ctx.db.getRepository("aiEmployees").findOne({
163
- filter: { username: config.defaultAiEmployee }
164
- });
165
- if (employee) {
166
- systemPrompt = employee.about || employee.defaultPrompt || "";
167
- }
168
- }
169
152
  let messages = [...body.messages];
170
- const hasSystemMessage = messages.some((m) => m.role === "system");
171
- if (systemPrompt && !hasSystemMessage) {
172
- messages.unshift({ role: "system", content: systemPrompt });
173
- }
153
+ messages = await Promise.all(
154
+ messages.map(async (msg) => ({
155
+ ...msg,
156
+ content: await processMessageContentFileBlocks(msg.content, ctx, plugin)
157
+ }))
158
+ );
174
159
  const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
175
160
  serviceName: service.name,
176
161
  modelId,
@@ -232,13 +217,14 @@ async function handleChatCompletions(ctx, plugin) {
232
217
  if (!((_a = ctx.res) == null ? void 0 : _a.headersSent)) {
233
218
  const isQuotaError = err instanceof import_billing.AiApiQuotaError;
234
219
  const isContextError = err instanceof import_direct_llm_context.DirectLlmContextError;
235
- ctx.status = isQuotaError ? 429 : isContextError ? 400 : 500;
220
+ const isFileError = err instanceof import_file_processor.FileProcessorError;
221
+ ctx.status = isQuotaError ? 429 : isContextError || isFileError ? 400 : 500;
236
222
  if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
237
223
  ctx.body = (0, import_openai_format.toOpenAIError)(
238
224
  ctx.status,
239
225
  getErrorMessage(err, "Internal server error"),
240
- isQuotaError ? "quota_error" : isContextError ? "invalid_request_error" : "server_error",
241
- isQuotaError || isContextError ? err.code : void 0
226
+ isQuotaError ? "quota_error" : isContextError || isFileError ? "invalid_request_error" : "server_error",
227
+ isQuotaError || isContextError || isFileError ? err.code : void 0
242
228
  );
243
229
  }
244
230
  }
@@ -252,18 +238,25 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
252
238
  const textPart = result.content.find((c) => c.type === "text");
253
239
  content = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
254
240
  }
255
- const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
256
- gatewayResponseId: completionId,
257
- providerRequestId: (0, import_usage.extractProviderRequestId)(result)
258
- });
241
+ const usage = (0, import_usage.setAiApiUsageResult)(
242
+ ctx,
243
+ result.usage_metadata,
244
+ {
245
+ gatewayResponseId: completionId,
246
+ providerRequestId: (0, import_usage.extractProviderRequestId)(result)
247
+ },
248
+ result.response_metadata
249
+ );
259
250
  ctx.status = 200;
260
251
  const toolCalls = normalizeToolCalls(result.tool_calls);
252
+ const providerFinishReason = extractFinishReason(result);
261
253
  ctx.body = (0, import_openai_format.toOpenAIResponse)({
262
254
  id: completionId,
263
255
  model: modelName,
264
256
  content,
265
257
  usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
266
- toolCalls
258
+ toolCalls,
259
+ ...providerFinishReason ? { finishReason: providerFinishReason } : {}
267
260
  });
268
261
  }
269
262
  async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
@@ -287,8 +280,10 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
287
280
  );
288
281
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
289
282
  let usage;
283
+ let usageResponseMetadata;
290
284
  let providerRequestId;
291
- let finishReason = "stop";
285
+ let providerFinishReason;
286
+ let sawToolCalls = false;
292
287
  try {
293
288
  const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
294
289
  for await (const chunk of stream) {
@@ -316,7 +311,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
316
311
  const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
317
312
  if (toolCallChunks.length) {
318
313
  (0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
319
- finishReason = "tool_calls";
314
+ sawToolCalls = true;
320
315
  await (0, import_streaming.writeResponse)(
321
316
  ctx,
322
317
  (0, import_openai_format.formatSSE)(
@@ -329,8 +324,14 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
329
324
  );
330
325
  }
331
326
  if (chunk.usage_metadata) {
332
- usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
327
+ const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
328
+ if (normalized) {
329
+ usage = normalized;
330
+ usageResponseMetadata = chunk.response_metadata;
331
+ }
333
332
  }
333
+ const chunkFinishReason = extractFinishReason(chunk);
334
+ if (chunkFinishReason) providerFinishReason = chunkFinishReason;
334
335
  providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
335
336
  }
336
337
  await (0, import_streaming.writeResponse)(
@@ -340,7 +341,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
340
341
  id: completionId,
341
342
  model: modelName,
342
343
  delta: {},
343
- finishReason
344
+ finishReason: providerFinishReason ?? (sawToolCalls ? "tool_calls" : "stop")
344
345
  })
345
346
  )
346
347
  );
@@ -357,7 +358,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
357
358
  );
358
359
  }
359
360
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
360
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
361
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
361
362
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
362
363
  } catch (err) {
363
364
  const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
@@ -373,7 +374,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
373
374
  })
374
375
  );
375
376
  }
376
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
377
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
377
378
  ctx.state.aiApiStreamResult = {
378
379
  succeeded: false,
379
380
  id: completionId,
@@ -387,8 +388,20 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
387
388
  function getErrorMessage(error, fallback) {
388
389
  return error instanceof Error && error.message ? error.message : fallback;
389
390
  }
390
- const SUPPORTED_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image_url"]);
391
+ function extractFinishReason(value) {
392
+ if (!isRecord(value)) return void 0;
393
+ const candidates = [
394
+ isRecord(value.response_metadata) ? value.response_metadata.finish_reason : void 0,
395
+ isRecord(value.additional_kwargs) ? value.additional_kwargs.finish_reason : void 0
396
+ ];
397
+ for (const candidate of candidates) {
398
+ if (typeof candidate === "string" && candidate.length > 0) return candidate;
399
+ }
400
+ return void 0;
401
+ }
402
+ const SUPPORTED_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image_url", "file", "file_url"]);
391
403
  const BASE64_DATA_URL_PATTERN = /^data:(\w+\/\w+);base64,([A-Za-z0-9+/]+=*)$/;
404
+ const FILE_BASE64_DATA_URL_PATTERN = /^data:([^;\s]+);base64,([A-Za-z0-9+/]+=*)$/;
392
405
  function isDecodableBase64(payload) {
393
406
  try {
394
407
  return Buffer.from(payload, "base64").toString("base64") === payload;
@@ -440,13 +453,50 @@ function describeContentBlockProblem(block) {
440
453
  const type = typeof block.type === "string" ? block.type : void 0;
441
454
  if (!type) return "each content block requires a 'type' field";
442
455
  if (!SUPPORTED_CONTENT_BLOCK_TYPES.has(type)) {
443
- return `content block type '${type}' is not supported \u2014 this gateway forwards 'text' and 'image_url' only. Send documents as text, or inline them as an 'image_url' data URL if the model reads images`;
456
+ return `content block type '${type}' is not supported \u2014 this gateway forwards 'text', 'image_url', 'file', and 'file_url' only. Send documents as text, or inline them as a 'file' / 'file_url' block`;
444
457
  }
445
458
  if (type === "text") {
446
459
  return typeof block.text === "string" ? void 0 : "a 'text' block requires a string 'text' field";
447
460
  }
461
+ if (type === "file") {
462
+ return describeFileProblem(block.file);
463
+ }
464
+ if (type === "file_url") {
465
+ return describeFileUrlProblem(block.file_url);
466
+ }
448
467
  return describeImageUrlProblem(block.image_url);
449
468
  }
469
+ function describeFileProblem(file) {
470
+ if (!isRecord(file)) return "a 'file' block requires an object 'file' field";
471
+ const fileData = typeof file.file_data === "string" ? file.file_data : void 0;
472
+ if (!fileData) return "a 'file' block requires a string 'file.file_data' field";
473
+ if (!fileData.startsWith("data:")) {
474
+ return "a 'file' block's 'file_data' must be a base64 data URL starting with 'data:'";
475
+ }
476
+ const match = FILE_BASE64_DATA_URL_PATTERN.exec(fileData);
477
+ if (!match || !match[1].includes("/")) {
478
+ return `malformed base64 data URL. Expected 'data:<mime-type>;base64,<base64>' with a valid type/subtype and standard base64 (no whitespace or URL-safe characters)`;
479
+ }
480
+ if (!isDecodableBase64(match[2])) {
481
+ return `base64 payload is not decodable. Check the padding and length \u2014 the data must be a multiple of 4 characters with at most two trailing '='`;
482
+ }
483
+ return void 0;
484
+ }
485
+ function describeFileUrlProblem(fileUrl) {
486
+ if (!isRecord(fileUrl)) return "a 'file_url' block requires an object 'file_url' field";
487
+ const url = typeof fileUrl.url === "string" ? fileUrl.url : void 0;
488
+ if (!url) return "a 'file_url' block requires a string 'file_url.url' field";
489
+ let protocol;
490
+ try {
491
+ protocol = new URL(url).protocol;
492
+ } catch {
493
+ return `'${url}' is not a valid URL. Use an http(s) URL`;
494
+ }
495
+ if (protocol !== "http:" && protocol !== "https:") {
496
+ return `URL protocol '${protocol}' is not supported. Use an http(s) URL`;
497
+ }
498
+ return void 0;
499
+ }
450
500
  function describeImageUrlProblem(imageUrl) {
451
501
  const url = typeof imageUrl === "string" ? imageUrl : isRecord(imageUrl) ? imageUrl.url : void 0;
452
502
  if (typeof url !== "string" || url === "") {
@@ -492,7 +542,35 @@ function normalizeMessageContent(content) {
492
542
  if (content === null || content === void 0) return "";
493
543
  return JSON.stringify(content);
494
544
  }
495
- const GATEWAY_MANAGED_PARAMETERS = /* @__PURE__ */ new Set(["model", "messages", "tools", "tool_choice", "stream", "n"]);
545
+ async function processMessageContentFileBlocks(content, ctx, plugin) {
546
+ if (!Array.isArray(content)) return content;
547
+ const processed = [];
548
+ for (const block of content) {
549
+ processed.push(...await processFileBlockChain(block, ctx, plugin, 0));
550
+ }
551
+ return processed;
552
+ }
553
+ const MAX_FILE_PROCESSOR_CHAIN_DEPTH = 3;
554
+ async function processFileBlockChain(block, ctx, plugin, depth) {
555
+ if (!isRecord(block) || block.type !== "file" && block.type !== "file_url") {
556
+ return [block];
557
+ }
558
+ if (depth > MAX_FILE_PROCESSOR_CHAIN_DEPTH) {
559
+ return [block];
560
+ }
561
+ const result = await plugin.fileProcessorService.process(block, { ctx });
562
+ const results = Array.isArray(result) ? result : [result];
563
+ const next = [];
564
+ for (const item of results) {
565
+ if (isRecord(item) && (item.type === "file" || item.type === "file_url")) {
566
+ next.push(...await processFileBlockChain(item, ctx, plugin, depth + 1));
567
+ } else {
568
+ next.push(item);
569
+ }
570
+ }
571
+ return next;
572
+ }
573
+ const GATEWAY_MANAGED_PARAMETERS = /* @__PURE__ */ new Set(["model", "messages", "prompt", "tools", "tool_choice", "stream", "n"]);
496
574
  function getProviderRequestParameters(body) {
497
575
  return Object.fromEntries(
498
576
  Object.entries(body).filter(([name, value]) => !GATEWAY_MANAGED_PARAMETERS.has(name) && value !== void 0)
@@ -562,6 +640,7 @@ function serializeToolArguments(value) {
562
640
  // Annotate the CommonJS export names for ESM import in node:
563
641
  0 && (module.exports = {
564
642
  applyProviderRequestParameters,
643
+ extractFinishReason,
565
644
  findContentBlockProblem,
566
645
  findMessageProblem,
567
646
  getProviderRequestParameters,
@@ -33,6 +33,8 @@ var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
34
  var import_user_permissions = require("../utils/user-permissions");
35
35
  var import_streaming = require("../utils/streaming");
36
+ var import_chat_completions = require("./chat-completions");
37
+ var import_request_cache = require("../utils/request-cache");
36
38
  var import_usage = require("../usage");
37
39
  var import_billing = require("../billing");
38
40
  var import_direct_llm_context = require("../utils/direct-llm-context");
@@ -90,7 +92,7 @@ async function handleCompletions(ctx, plugin) {
90
92
  );
91
93
  return;
92
94
  }
93
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
95
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
94
96
  if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
95
97
  return;
96
98
  }
@@ -109,19 +111,7 @@ async function handleCompletions(ctx, plugin) {
109
111
  if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
110
112
  if (body.stop !== void 0) modelOptions.stop = body.stop;
111
113
  const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
112
- const messages = [];
113
- if (config == null ? void 0 : config.defaultAiEmployee) {
114
- const employee = await ctx.db.getRepository("aiEmployees").findOne({
115
- filter: { username: config.defaultAiEmployee }
116
- });
117
- if (employee) {
118
- const systemPrompt = employee.about || employee.defaultPrompt || "";
119
- if (systemPrompt) {
120
- messages.push({ role: "system", content: systemPrompt });
121
- }
122
- }
123
- }
124
- messages.push({ role: "user", content: prompt });
114
+ const messages = [{ role: "user", content: prompt }];
125
115
  const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
126
116
  serviceName: service.name,
127
117
  modelId,
@@ -141,6 +131,8 @@ async function handleCompletions(ctx, plugin) {
141
131
  ]);
142
132
  const completionId = (0, import_openai_format.generateCompletionId)().replace("chatcmpl-", "cmpl-");
143
133
  const chatModel = provider.createModel();
134
+ const providerRequestParameters = (0, import_chat_completions.getProviderRequestParameters)(body);
135
+ (0, import_chat_completions.applyProviderRequestParameters)(chatModel, providerRequestParameters);
144
136
  (0, import_billing.markLlmProviderAttempted)(ctx);
145
137
  if (stream) {
146
138
  await handleStreamingTextCompletion(
@@ -149,10 +141,18 @@ async function handleCompletions(ctx, plugin) {
149
141
  langchainMessages,
150
142
  completionId,
151
143
  body.model,
152
- body.stream_options
144
+ body.stream_options,
145
+ providerRequestParameters
153
146
  );
154
147
  } else {
155
- await handleNonStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
148
+ await handleNonStreamingTextCompletion(
149
+ ctx,
150
+ chatModel,
151
+ langchainMessages,
152
+ completionId,
153
+ body.model,
154
+ providerRequestParameters
155
+ );
156
156
  }
157
157
  } catch (err) {
158
158
  ctx.log.error("AI API completions error:", err);
@@ -170,8 +170,8 @@ async function handleCompletions(ctx, plugin) {
170
170
  }
171
171
  }
172
172
  }
173
- async function handleNonStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
174
- const result = await chatModel.invoke(messages);
173
+ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
174
+ const result = await chatModel.invoke(messages, providerRequestParameters);
175
175
  let text = "";
176
176
  if (typeof result.content === "string") {
177
177
  text = result.content;
@@ -179,10 +179,15 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
179
179
  const textPart = result.content.find((c) => c.type === "text");
180
180
  text = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
181
181
  }
182
- const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
183
- gatewayResponseId: completionId,
184
- providerRequestId: (0, import_usage.extractProviderRequestId)(result)
185
- });
182
+ const usage = (0, import_usage.setAiApiUsageResult)(
183
+ ctx,
184
+ result.usage_metadata,
185
+ {
186
+ gatewayResponseId: completionId,
187
+ providerRequestId: (0, import_usage.extractProviderRequestId)(result)
188
+ },
189
+ result.response_metadata
190
+ );
186
191
  ctx.status = 200;
187
192
  ctx.body = {
188
193
  id: completionId,
@@ -195,13 +200,18 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
195
200
  text,
196
201
  index: 0,
197
202
  logprobs: null,
198
- finish_reason: "stop"
203
+ finish_reason: (0, import_chat_completions.extractFinishReason)(result) ?? "stop"
199
204
  }
200
205
  ],
201
- usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
206
+ usage: usage ? {
207
+ prompt_tokens: usage.prompt_tokens,
208
+ completion_tokens: usage.completion_tokens,
209
+ total_tokens: usage.total_tokens,
210
+ prompt_tokens_details: { cached_tokens: usage.prompt_cache_tokens ?? null }
211
+ } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: null } }
202
212
  };
203
213
  }
204
- async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions) {
214
+ async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions, providerRequestParameters) {
205
215
  ctx.set({
206
216
  "Content-Type": "text/event-stream",
207
217
  "Cache-Control": "no-cache",
@@ -211,9 +221,12 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
211
221
  ctx.status = 200;
212
222
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
213
223
  let usage;
224
+ let usageResponseMetadata;
214
225
  let providerRequestId;
226
+ let providerFinishReason;
215
227
  try {
216
228
  const stream = await chatModel.stream(messages, {
229
+ ...providerRequestParameters,
217
230
  stream_options: { ...streamOptions, include_usage: true },
218
231
  signal: requestAbort.signal
219
232
  });
@@ -249,8 +262,14 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
249
262
  );
250
263
  }
251
264
  if (chunk.usage_metadata) {
252
- usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
265
+ const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
266
+ if (normalized) {
267
+ usage = normalized;
268
+ usageResponseMetadata = chunk.response_metadata;
269
+ }
253
270
  }
271
+ const chunkFinishReason = (0, import_chat_completions.extractFinishReason)(chunk);
272
+ if (chunkFinishReason) providerFinishReason = chunkFinishReason;
254
273
  providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
255
274
  }
256
275
  await (0, import_streaming.writeResponse)(
@@ -266,7 +285,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
266
285
  text: "",
267
286
  index: 0,
268
287
  logprobs: null,
269
- finish_reason: "stop"
288
+ finish_reason: providerFinishReason ?? "stop"
270
289
  }
271
290
  ],
272
291
  usage: null
@@ -286,7 +305,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
286
305
  );
287
306
  }
288
307
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
289
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
308
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
290
309
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
291
310
  } catch (err) {
292
311
  const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
@@ -302,7 +321,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
302
321
  })
303
322
  );
304
323
  }
305
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
324
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
306
325
  ctx.state.aiApiStreamResult = {
307
326
  succeeded: false,
308
327
  id: completionId,
@@ -32,6 +32,7 @@ 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
34
  var import_user_permissions = require("../utils/user-permissions");
35
+ var import_request_cache = require("../utils/request-cache");
35
36
  var import_usage = require("../usage");
36
37
  async function handleEmbeddings(ctx, plugin) {
37
38
  const body = ctx.request.body;
@@ -104,7 +105,7 @@ async function handleEmbeddings(ctx, plugin) {
104
105
  }
105
106
  let globalEnabledServices = [];
106
107
  try {
107
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
108
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
108
109
  globalEnabledServices = (config == null ? void 0 : config.enabledLlmServices) ?? [];
109
110
  } catch {
110
111
  }
@@ -33,6 +33,7 @@ __export(models_exports, {
33
33
  module.exports = __toCommonJS(models_exports);
34
34
  var import_openai_format = require("../utils/openai-format");
35
35
  var import_user_permissions = require("../utils/user-permissions");
36
+ var import_request_cache = require("../utils/request-cache");
36
37
  async function handleListModels(ctx, plugin) {
37
38
  var _a;
38
39
  try {
@@ -198,7 +199,7 @@ function toPositiveInt(value) {
198
199
  return Number.isSafeInteger(n) && n > 0 ? n : null;
199
200
  }
200
201
  async function getPluginConfig(ctx) {
201
- return ctx.db.getRepository("aiApiConfig").findOne();
202
+ return (0, import_request_cache.getAiApiConfig)(ctx);
202
203
  }
203
204
  function resolveEnabledModels(service) {
204
205
  const raw = service.enabledModels;
@@ -55,6 +55,7 @@ var import_rate_limit = require("../middleware/rate-limit");
55
55
  var import_role_permission = require("../middleware/role-permission");
56
56
  var import_usage = require("../usage");
57
57
  var import_streaming = require("../utils/streaming");
58
+ var import_request_cache = require("../utils/request-cache");
58
59
  var import_billing = require("../billing");
59
60
  var import_app_observability = require("../utils/app-observability");
60
61
  const API_PREFIX = "/api/ai-llm/v1";
@@ -273,7 +274,7 @@ async function resolveMaxBodyBytes(ctx) {
273
274
  var _a, _b;
274
275
  let configuredMb;
275
276
  try {
276
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
277
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
277
278
  configuredMb = config == null ? void 0 : config.get("maxRequestBodyMb");
278
279
  } catch (err) {
279
280
  (_b = (_a = ctx.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, "AI API: could not read maxRequestBodyMb, using default:", err);
@@ -322,7 +323,7 @@ async function resolveMode(ctx) {
322
323
  return headerMode;
323
324
  }
324
325
  try {
325
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
326
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
326
327
  if (config) {
327
328
  const dbMode = config.get("mode") || config.mode;
328
329
  if (dbMode === "agent" || dbMode === "llm") {