plugin-ai-api 1.0.25 → 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 (115) 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.56952e321dc399b7.js → 757.6568d3504ad29352.js} +1 -1
  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.db678ca1aa6c422c.js → 757.f2bc9cfba07004b0.js} +1 -1
  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/locale/en-US.json +26 -8
  17. package/dist/locale/vi-VN.json +26 -8
  18. package/dist/locale/zh-CN.json +26 -8
  19. package/dist/server/billing.js +25 -32
  20. package/dist/server/collections/ai-api-config.js +1 -7
  21. package/dist/server/collections/ai-api-group-members.js +62 -0
  22. package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
  23. package/dist/server/collections/ai-api-model-metadata.js +6 -0
  24. package/dist/server/collections/ai-api-usage-groups.js +74 -0
  25. package/dist/server/collections/ai-api-usage-records.js +1 -0
  26. package/dist/server/middleware/rate-limit.js +7 -6
  27. package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
  28. package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
  29. package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
  30. package/dist/server/plugin.js +90 -22
  31. package/dist/server/quota-groups.js +108 -0
  32. package/dist/server/resource/ai-api-config.js +0 -3
  33. package/dist/server/resource/ai-api-usage-groups.js +168 -0
  34. package/dist/server/routes/agent-completions.js +2 -1
  35. package/dist/server/routes/chat-completions.js +32 -32
  36. package/dist/server/routes/completions.js +16 -19
  37. package/dist/server/routes/embeddings.js +2 -1
  38. package/dist/server/routes/models.js +2 -1
  39. package/dist/server/routes/router.js +3 -2
  40. package/dist/server/services/file-processor.js +186 -22
  41. package/dist/server/usage.js +5 -1
  42. package/dist/server/utils/direct-llm-context.js +13 -11
  43. package/dist/server/utils/rate-limiter.js +1 -1
  44. package/dist/server/utils/request-cache.js +61 -0
  45. package/dist/server/utils/resolve-service.js +2 -1
  46. package/dist/server/utils/user-permissions.js +25 -39
  47. package/dist/server/validation.js +7 -0
  48. package/dist/swagger.js +6 -7
  49. package/package.json +1 -1
  50. package/src/client/__tests__/settings-registration.test.tsx +6 -29
  51. package/src/client/plugin.tsx +5 -16
  52. package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
  53. package/src/client-v2/locale.ts +3 -1
  54. package/src/client-v2/pages/GeneralPage.tsx +0 -5
  55. package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
  56. package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
  57. package/src/client-v2/plugin.tsx +4 -13
  58. package/src/constants.ts +0 -7
  59. package/src/locale/en-US.json +26 -8
  60. package/src/locale/vi-VN.json +26 -8
  61. package/src/locale/zh-CN.json +26 -8
  62. package/src/server/__tests__/billing-quota.test.ts +28 -9
  63. package/src/server/__tests__/direct-llm-context.test.ts +122 -4
  64. package/src/server/__tests__/file-processor.test.ts +225 -0
  65. package/src/server/__tests__/models.test.ts +1 -1
  66. package/src/server/__tests__/permission-sync.test.ts +34 -35
  67. package/src/server/__tests__/usage-groups.test.ts +160 -0
  68. package/src/server/__tests__/usage-monitor.test.ts +2 -0
  69. package/src/server/__tests__/usage-route.test.ts +262 -2
  70. package/src/server/__tests__/usage.test.ts +38 -0
  71. package/src/server/__tests__/user-permissions.test.ts +214 -133
  72. package/src/server/__tests__/validation.test.ts +11 -0
  73. package/src/server/billing.ts +30 -38
  74. package/src/server/collections/ai-api-config.ts +1 -7
  75. package/src/server/collections/ai-api-group-members.ts +41 -0
  76. package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
  77. package/src/server/collections/ai-api-model-metadata.ts +7 -0
  78. package/src/server/collections/ai-api-usage-groups.ts +53 -0
  79. package/src/server/collections/ai-api-usage-records.ts +1 -0
  80. package/src/server/middleware/rate-limit.ts +10 -12
  81. package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
  82. package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
  83. package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
  84. package/src/server/plugin.ts +101 -30
  85. package/src/server/quota-groups.ts +117 -0
  86. package/src/server/resource/ai-api-config.ts +0 -3
  87. package/src/server/resource/ai-api-usage-groups.ts +171 -0
  88. package/src/server/routes/agent-completions.ts +2 -1
  89. package/src/server/routes/chat-completions.ts +39 -36
  90. package/src/server/routes/completions.ts +18 -21
  91. package/src/server/routes/embeddings.ts +2 -1
  92. package/src/server/routes/models.ts +4 -3
  93. package/src/server/routes/router.ts +4 -3
  94. package/src/server/services/file-processor.ts +214 -24
  95. package/src/server/usage.ts +5 -1
  96. package/src/server/utils/direct-llm-context.ts +20 -11
  97. package/src/server/utils/rate-limiter.ts +1 -1
  98. package/src/server/utils/request-cache.ts +59 -0
  99. package/src/server/utils/resolve-service.ts +2 -1
  100. package/src/server/utils/user-permissions.ts +49 -69
  101. package/src/server/validation.ts +7 -0
  102. package/src/swagger.ts +7 -8
  103. package/dist/client/123.e6fe04c856ce6417.js +0 -10
  104. package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
  105. package/dist/client/562.17a0a299d2e5152c.js +0 -10
  106. package/dist/client/902.e74518750f1e4201.js +0 -10
  107. package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
  108. package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
  109. package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
  110. package/dist/client-v2/902.c7c00a565085438a.js +0 -10
  111. package/dist/server/resource/ai-api-user-permissions.js +0 -75
  112. package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
  113. package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
  114. package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
  115. package/src/server/resource/ai-api-user-permissions.ts +0 -76
@@ -34,6 +34,7 @@ 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
36
  var import_chat_completions = require("./chat-completions");
37
+ var import_request_cache = require("../utils/request-cache");
37
38
  var import_usage = require("../usage");
38
39
  var import_billing = require("../billing");
39
40
  var import_direct_llm_context = require("../utils/direct-llm-context");
@@ -91,7 +92,7 @@ async function handleCompletions(ctx, plugin) {
91
92
  );
92
93
  return;
93
94
  }
94
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
95
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
95
96
  if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
96
97
  return;
97
98
  }
@@ -110,19 +111,7 @@ async function handleCompletions(ctx, plugin) {
110
111
  if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
111
112
  if (body.stop !== void 0) modelOptions.stop = body.stop;
112
113
  const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
113
- const messages = [];
114
- if (config == null ? void 0 : config.defaultAiEmployee) {
115
- const employee = await ctx.db.getRepository("aiEmployees").findOne({
116
- filter: { username: config.defaultAiEmployee }
117
- });
118
- if (employee) {
119
- const systemPrompt = employee.about || employee.defaultPrompt || "";
120
- if (systemPrompt) {
121
- messages.push({ role: "system", content: systemPrompt });
122
- }
123
- }
124
- }
125
- messages.push({ role: "user", content: prompt });
114
+ const messages = [{ role: "user", content: prompt }];
126
115
  const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
127
116
  serviceName: service.name,
128
117
  modelId,
@@ -211,7 +200,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
211
200
  text,
212
201
  index: 0,
213
202
  logprobs: null,
214
- finish_reason: "stop"
203
+ finish_reason: (0, import_chat_completions.extractFinishReason)(result) ?? "stop"
215
204
  }
216
205
  ],
217
206
  usage: usage ? {
@@ -232,7 +221,9 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
232
221
  ctx.status = 200;
233
222
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
234
223
  let usage;
224
+ let usageResponseMetadata;
235
225
  let providerRequestId;
226
+ let providerFinishReason;
236
227
  try {
237
228
  const stream = await chatModel.stream(messages, {
238
229
  ...providerRequestParameters,
@@ -271,8 +262,14 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
271
262
  );
272
263
  }
273
264
  if (chunk.usage_metadata) {
274
- 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
+ }
275
270
  }
271
+ const chunkFinishReason = (0, import_chat_completions.extractFinishReason)(chunk);
272
+ if (chunkFinishReason) providerFinishReason = chunkFinishReason;
276
273
  providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
277
274
  }
278
275
  await (0, import_streaming.writeResponse)(
@@ -288,7 +285,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
288
285
  text: "",
289
286
  index: 0,
290
287
  logprobs: null,
291
- finish_reason: "stop"
288
+ finish_reason: providerFinishReason ?? "stop"
292
289
  }
293
290
  ],
294
291
  usage: null
@@ -308,7 +305,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
308
305
  );
309
306
  }
310
307
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
311
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
308
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
312
309
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
313
310
  } catch (err) {
314
311
  const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
@@ -324,7 +321,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
324
321
  })
325
322
  );
326
323
  }
327
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
324
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
328
325
  ctx.state.aiApiStreamResult = {
329
326
  succeeded: false,
330
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") {
@@ -7,9 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ var __create = Object.create;
10
11
  var __defProp = Object.defineProperty;
11
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
13
  var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
13
15
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
16
  var __export = (target, all) => {
15
17
  for (var name in all)
@@ -23,6 +25,14 @@ var __copyProps = (to, from, except, desc) => {
23
25
  }
24
26
  return to;
25
27
  };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
26
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
37
  var file_processor_exports = {};
28
38
  __export(file_processor_exports, {
@@ -31,10 +41,14 @@ __export(file_processor_exports, {
31
41
  base64FileForwarder: () => base64FileForwarder,
32
42
  fetchFileAsBase64: () => fetchFileAsBase64,
33
43
  httpFileUrlFetcher: () => httpFileUrlFetcher,
44
+ isBlockedAddress: () => isBlockedAddress,
34
45
  pdfFileProcessor: () => pdfFileProcessor
35
46
  });
36
47
  module.exports = __toCommonJS(file_processor_exports);
48
+ var import_dns = __toESM(require("dns"));
49
+ var import_net = require("net");
37
50
  var import_path = require("path");
51
+ var import_request_cache = require("../utils/request-cache");
38
52
  class FileProcessorError extends Error {
39
53
  constructor(code, message) {
40
54
  super(message);
@@ -44,7 +58,9 @@ class FileProcessorError extends Error {
44
58
  }
45
59
  const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
46
60
  const DEFAULT_TIMEOUT_MS = 3e4;
61
+ const DEFAULT_MAX_REDIRECTS = 5;
47
62
  const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
63
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
48
64
  class FileProcessorService {
49
65
  processors = [];
50
66
  pdfRenderer = null;
@@ -82,11 +98,6 @@ class FileProcessorService {
82
98
  function isRecord(value) {
83
99
  return typeof value === "object" && value !== null && !Array.isArray(value);
84
100
  }
85
- function getUrlString(value) {
86
- if (typeof value === "string") return value;
87
- if (isRecord(value) && typeof value.url === "string") return value.url;
88
- return void 0;
89
- }
90
101
  function extractFilename(url, contentDisposition) {
91
102
  if (contentDisposition) {
92
103
  const match = contentDisposition.match(/filename="?([^"]+)"?/);
@@ -99,35 +110,187 @@ function extractFilename(url, contentDisposition) {
99
110
  }
100
111
  return void 0;
101
112
  }
113
+ function parseIpv4Octets(ip) {
114
+ const parts = ip.split(".");
115
+ if (parts.length !== 4) return void 0;
116
+ const octets = [];
117
+ for (const part of parts) {
118
+ if (!/^\d{1,3}$/.test(part)) return void 0;
119
+ const value = Number(part);
120
+ if (value > 255) return void 0;
121
+ octets.push(value);
122
+ }
123
+ return octets;
124
+ }
125
+ function isBlockedIpv4(ip) {
126
+ const octets = parseIpv4Octets(ip);
127
+ if (!octets) return true;
128
+ const [a, b] = octets;
129
+ if (a === 0) return true;
130
+ if (a === 10) return true;
131
+ if (a === 100 && b >= 64 && b <= 127) return true;
132
+ if (a === 127) return true;
133
+ if (a === 169 && b === 254) return true;
134
+ if (a === 172 && b >= 16 && b <= 31) return true;
135
+ if (a === 192 && b === 0) return true;
136
+ if (a === 192 && b === 168) return true;
137
+ if (a === 198 && (b === 18 || b === 19)) return true;
138
+ if (a >= 224) return true;
139
+ return false;
140
+ }
141
+ function parseHexGroup(group) {
142
+ if (group.length < 1 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return void 0;
143
+ return parseInt(group, 16);
144
+ }
145
+ function expandIpv6(input) {
146
+ let address = input;
147
+ const zoneIndex = address.indexOf("%");
148
+ if (zoneIndex !== -1) address = address.slice(0, zoneIndex);
149
+ if (!address) return void 0;
150
+ const lastColon = address.lastIndexOf(":");
151
+ if (lastColon !== -1 && address.includes(".", lastColon)) {
152
+ const octets = parseIpv4Octets(address.slice(lastColon + 1));
153
+ if (!octets) return void 0;
154
+ const high = (octets[0] << 8 | octets[1]).toString(16);
155
+ const low = (octets[2] << 8 | octets[3]).toString(16);
156
+ address = `${address.slice(0, lastColon + 1)}${high}:${low}`;
157
+ }
158
+ const groups = [];
159
+ const doubleColonIndex = address.indexOf("::");
160
+ if (doubleColonIndex !== -1) {
161
+ if (address.indexOf("::", doubleColonIndex + 1) !== -1) return void 0;
162
+ const head = address.slice(0, doubleColonIndex);
163
+ const tail = address.slice(doubleColonIndex + 2);
164
+ const headGroups = head === "" ? [] : head.split(":");
165
+ const tailGroups = tail === "" ? [] : tail.split(":");
166
+ const fillCount = 8 - headGroups.length - tailGroups.length;
167
+ if (fillCount < 1) return void 0;
168
+ const allGroups = [...headGroups];
169
+ for (let i = 0; i < fillCount; i += 1) allGroups.push("0");
170
+ allGroups.push(...tailGroups);
171
+ for (const group of allGroups) {
172
+ const parsed = parseHexGroup(group);
173
+ if (parsed === void 0) return void 0;
174
+ groups.push(parsed);
175
+ }
176
+ } else {
177
+ for (const group of address.split(":")) {
178
+ const parsed = parseHexGroup(group);
179
+ if (parsed === void 0) return void 0;
180
+ groups.push(parsed);
181
+ }
182
+ }
183
+ return groups.length === 8 ? groups : void 0;
184
+ }
185
+ function isBlockedIpv6(ip) {
186
+ const groups = expandIpv6(ip);
187
+ if (!groups) return true;
188
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
189
+ if (groups.every((group) => group === 0)) return true;
190
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0 && g7 === 1) {
191
+ return true;
192
+ }
193
+ const isIpv4Mapped = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 65535;
194
+ const isIpv4Compatible = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
195
+ const isNat64 = g0 === 100 && g1 === 65435 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
196
+ if (isIpv4Mapped || isIpv4Compatible || isNat64) {
197
+ const embedded = `${g6 >> 8 & 255}.${g6 & 255}.${g7 >> 8 & 255}.${g7 & 255}`;
198
+ return isBlockedIpv4(embedded);
199
+ }
200
+ if ((g0 & 65024) === 64512) return true;
201
+ if ((g0 & 65472) === 65152) return true;
202
+ if ((g0 & 65280) === 65280) return true;
203
+ return false;
204
+ }
205
+ function isBlockedAddress(ip) {
206
+ const version = (0, import_net.isIP)(ip);
207
+ if (version === 4) return isBlockedIpv4(ip);
208
+ if (version === 6) return isBlockedIpv6(ip);
209
+ return true;
210
+ }
211
+ async function assertHostAllowed(hostname) {
212
+ if ((0, import_net.isIP)(hostname) !== 0) {
213
+ if (isBlockedAddress(hostname)) {
214
+ throw new FileProcessorError("blocked_host", `Host '${hostname}' is a blocked address.`);
215
+ }
216
+ return;
217
+ }
218
+ let addresses;
219
+ try {
220
+ addresses = await import_dns.default.promises.lookup(hostname, { all: true });
221
+ } catch (error) {
222
+ const message = error instanceof Error && error.message ? error.message : String(error);
223
+ throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}': ${message}`);
224
+ }
225
+ if (!addresses.length) {
226
+ throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}'.`);
227
+ }
228
+ for (const { address } of addresses) {
229
+ if (isBlockedAddress(address)) {
230
+ throw new FileProcessorError("blocked_host", `Host '${hostname}' resolves to blocked address '${address}'.`);
231
+ }
232
+ }
233
+ }
234
+ async function fetchWithSsrfGuard(initialUrl, options) {
235
+ let currentUrl = initialUrl;
236
+ for (let hop = 0; ; hop += 1) {
237
+ let parsed;
238
+ try {
239
+ parsed = new URL(currentUrl);
240
+ } catch {
241
+ throw new FileProcessorError("invalid_url", `File URL '${currentUrl}' is not a valid URL.`);
242
+ }
243
+ if (!options.allowedProtocols.has(parsed.protocol)) {
244
+ throw new FileProcessorError("unsupported_protocol", `File URL protocol '${parsed.protocol}' is not allowed.`);
245
+ }
246
+ const hostname = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
247
+ await assertHostAllowed(hostname);
248
+ const response = await fetch(currentUrl, { signal: options.signal, redirect: "manual" });
249
+ if (!REDIRECT_STATUSES.has(response.status)) {
250
+ return { response, finalUrl: currentUrl };
251
+ }
252
+ if (hop >= options.maxRedirects) {
253
+ throw new FileProcessorError(
254
+ "too_many_redirects",
255
+ `File URL '${initialUrl}' exceeded the limit of ${options.maxRedirects} redirects.`
256
+ );
257
+ }
258
+ const location = response.headers.get("location");
259
+ if (!location) {
260
+ throw new FileProcessorError("fetch_failed", `Redirect from '${currentUrl}' is missing a Location header.`);
261
+ }
262
+ try {
263
+ currentUrl = new URL(location, currentUrl).toString();
264
+ } catch {
265
+ throw new FileProcessorError(
266
+ "invalid_url",
267
+ `Redirect Location '${location}' from '${currentUrl}' is not a valid URL.`
268
+ );
269
+ }
270
+ }
271
+ }
102
272
  async function fetchFileAsBase64(url, options = {}) {
103
273
  const maxSize = options.maxSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
104
274
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
275
+ const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
105
276
  const allowedProtocols = options.allowedProtocols ? new Set(options.allowedProtocols) : ALLOWED_PROTOCOLS;
106
- let protocol;
107
- try {
108
- protocol = new URL(url).protocol;
109
- } catch {
110
- throw new FileProcessorError("invalid_url", `File URL '${url}' is not a valid URL.`);
111
- }
112
- if (!allowedProtocols.has(protocol)) {
113
- throw new FileProcessorError("unsupported_protocol", `File URL protocol '${protocol}' is not allowed.`);
114
- }
115
277
  const controller = new AbortController();
116
278
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
117
279
  try {
118
- const response = await fetch(url, {
119
- signal: controller.signal,
120
- redirect: "follow"
280
+ const { response, finalUrl } = await fetchWithSsrfGuard(url, {
281
+ allowedProtocols,
282
+ maxRedirects,
283
+ signal: controller.signal
121
284
  });
122
285
  if (!response.ok) {
123
286
  throw new FileProcessorError(
124
287
  "fetch_failed",
125
- `Failed to fetch file from '${url}': ${response.status} ${response.statusText}`
288
+ `Failed to fetch file from '${finalUrl}': ${response.status} ${response.statusText}`
126
289
  );
127
290
  }
128
291
  const contentLength = response.headers.get("content-length");
129
292
  if (contentLength && Number(contentLength) > maxSize) {
130
- throw new FileProcessorError("file_too_large", `File at '${url}' exceeds maximum allowed size.`);
293
+ throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
131
294
  }
132
295
  const contentType = response.headers.get("content-type") || void 0;
133
296
  if (options.allowedContentTypes && contentType && !options.allowedContentTypes.some((type) => contentType.includes(type))) {
@@ -135,11 +298,11 @@ async function fetchFileAsBase64(url, options = {}) {
135
298
  }
136
299
  const buffer = Buffer.from(await response.arrayBuffer());
137
300
  if (buffer.length > maxSize) {
138
- throw new FileProcessorError("file_too_large", `File at '${url}' exceeds maximum allowed size.`);
301
+ throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
139
302
  }
140
303
  const mimeType = (contentType == null ? void 0 : contentType.split(";")[0].trim()) ?? "application/octet-stream";
141
304
  const contentDisposition = response.headers.get("content-disposition");
142
- const filename = extractFilename(url, contentDisposition) ?? "file";
305
+ const filename = extractFilename(finalUrl, contentDisposition) ?? "file";
143
306
  return {
144
307
  fileData: `data:${mimeType};base64,${buffer.toString("base64")}`,
145
308
  mimeType,
@@ -225,7 +388,7 @@ const pdfFileProcessor = {
225
388
  },
226
389
  async process(block, context) {
227
390
  var _a, _b, _c, _d;
228
- const config = await context.ctx.db.getRepository("aiApiConfig").findOne();
391
+ const config = await (0, import_request_cache.getAiApiConfig)(context.ctx);
229
392
  if (!(config == null ? void 0 : config.pdfRenderPagesAsImages)) {
230
393
  return block;
231
394
  }
@@ -258,5 +421,6 @@ const pdfFileProcessor = {
258
421
  base64FileForwarder,
259
422
  fetchFileAsBase64,
260
423
  httpFileUrlFetcher,
424
+ isBlockedAddress,
261
425
  pdfFileProcessor
262
426
  });
@@ -74,7 +74,10 @@ function normalizeUsage(value) {
74
74
  prompt_tokens: prompt,
75
75
  completion_tokens: completion,
76
76
  total_tokens: total,
77
- prompt_cache_tokens: extractPromptCacheTokens(source)
77
+ // Keep normalizeUsage idempotent: streaming routes normalize the chunk
78
+ // usage once and hand the result to setAiApiUsageResult, which normalizes
79
+ // again — an already-extracted prompt_cache_tokens must survive that pass.
80
+ prompt_cache_tokens: extractPromptCacheTokens(source) ?? normalizeTokenCount(source.prompt_cache_tokens)
78
81
  };
79
82
  }
80
83
  function setAiApiUsageResult(ctx, value, metadata = {}, responseMetadata) {
@@ -171,6 +174,7 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
171
174
  costStatus: billing.costStatus ?? null,
172
175
  modelPriceId: billing.modelPriceId ?? null,
173
176
  quotaPolicyId: billing.quotaPolicyId ?? null,
177
+ groupId: billing.groupId ?? null,
174
178
  inputPricePerMillionTokens: billing.inputPricePerMillionTokens ?? null,
175
179
  outputPricePerMillionTokens: billing.outputPricePerMillionTokens ?? null,
176
180
  fixedCostPerRequest: billing.fixedCostPerRequest ?? null,
@@ -31,6 +31,7 @@ __export(direct_llm_context_exports, {
31
31
  prepareDirectLlmContext: () => prepareDirectLlmContext
32
32
  });
33
33
  module.exports = __toCommonJS(direct_llm_context_exports);
34
+ var import_request_cache = require("./request-cache");
34
35
  class DirectLlmContextError extends Error {
35
36
  constructor(code, message) {
36
37
  super(message);
@@ -237,16 +238,16 @@ async function loadModelMetadata(ctx, serviceName, modelId) {
237
238
  `Context metadata is not configured for '${serviceName}/${modelId}'. Configure context window and max completion tokens.`
238
239
  );
239
240
  }
240
- return { contextWindow, maxCompletionTokens };
241
+ const systemPromptValue = getValue(row, "systemPrompt");
242
+ const systemPrompt = typeof systemPromptValue === "string" ? systemPromptValue.trim() : "";
243
+ return { contextWindow, maxCompletionTokens, ...systemPrompt ? { systemPrompt } : {} };
241
244
  }
242
245
  async function resolveOverflowBehavior(ctx) {
243
246
  var _a;
244
247
  const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
245
248
  if (userId === null || userId === void 0) return "reject";
246
- const policy = await ctx.db.getRepository("aiApiUserQuotaPolicies").findOne({
247
- filter: { userId, enabled: true }
248
- });
249
- return getValue(policy, "contextOverflowBehavior") === "truncate" ? "truncate" : "reject";
249
+ const group = await (0, import_request_cache.resolveRequestUserGroup)(ctx, userId);
250
+ return group.contextOverflowBehavior === "truncate" ? "truncate" : "reject";
250
251
  }
251
252
  function resolveReservedOutputTokens(options, metadata) {
252
253
  const requested = positiveInteger(options.maxCompletionTokens ?? options.maxTokens);
@@ -272,10 +273,11 @@ async function prepareDirectLlmContext(ctx, options) {
272
273
  );
273
274
  }
274
275
  const fixedOverheadTokens = estimateValueTokens(options.tools) + (options.tools === void 0 ? 0 : 4);
275
- const originalEstimate = estimateMessagesTokens(options.messages) + fixedOverheadTokens;
276
+ const baseMessages = metadata.systemPrompt ? [{ role: "system", content: metadata.systemPrompt }, ...options.messages] : options.messages;
277
+ const originalEstimate = estimateMessagesTokens(baseMessages) + fixedOverheadTokens;
276
278
  if (originalEstimate <= inputTokenBudget) {
277
279
  return {
278
- messages: options.messages,
280
+ messages: baseMessages,
279
281
  estimatedInputTokens: originalEstimate,
280
282
  inputTokenBudget,
281
283
  reservedOutputTokens,
@@ -288,13 +290,13 @@ async function prepareDirectLlmContext(ctx, options) {
288
290
  `Estimated input tokens (${originalEstimate}) exceed the allowed input budget (${inputTokenBudget}).`
289
291
  );
290
292
  }
291
- const { turns } = splitTurns(options.messages);
293
+ const { turns } = splitTurns(baseMessages);
292
294
  let remainingTurns = turns;
293
- let messages = messagesWithTurns(options.messages, remainingTurns);
295
+ let messages = messagesWithTurns(baseMessages, remainingTurns);
294
296
  let estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
295
297
  while (remainingTurns.length > 1 && estimatedInputTokens > inputTokenBudget) {
296
298
  remainingTurns = remainingTurns.slice(1);
297
- messages = messagesWithTurns(options.messages, remainingTurns);
299
+ messages = messagesWithTurns(baseMessages, remainingTurns);
298
300
  estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
299
301
  }
300
302
  if (estimatedInputTokens > inputTokenBudget) {
@@ -308,7 +310,7 @@ async function prepareDirectLlmContext(ctx, options) {
308
310
  estimatedInputTokens,
309
311
  inputTokenBudget,
310
312
  reservedOutputTokens,
311
- truncated: messages.length !== options.messages.length
313
+ truncated: messages.length !== baseMessages.length
312
314
  };
313
315
  }
314
316
  // Annotate the CommonJS export names for ESM import in node:
@@ -39,7 +39,7 @@ class RateLimiter {
39
39
  * Check and record a request for a user.
40
40
  *
41
41
  * @param userId The user ID (string or numeric)
42
- * @param limit Max allowed requests per window (from aiApiConfig.rateLimitPerMinute)
42
+ * @param limit Max allowed requests per window (from the user's usage group)
43
43
  * @returns { allowed: true } or { allowed: false, retryAfterMs: number }
44
44
  */
45
45
  check(userId, limit) {
@@ -0,0 +1,61 @@
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 request_cache_exports = {};
28
+ __export(request_cache_exports, {
29
+ getAiApiConfig: () => getAiApiConfig,
30
+ resolveRequestUserGroup: () => resolveRequestUserGroup
31
+ });
32
+ module.exports = __toCommonJS(request_cache_exports);
33
+ var import_quota_groups = require("../quota-groups");
34
+ function getCache(ctx) {
35
+ if (!ctx.state.aiApiRequestCache) {
36
+ ctx.state.aiApiRequestCache = { configLoaded: false, config: null };
37
+ }
38
+ return ctx.state.aiApiRequestCache;
39
+ }
40
+ async function getAiApiConfig(ctx) {
41
+ const cache = getCache(ctx);
42
+ if (!cache.configLoaded) {
43
+ cache.config = await ctx.db.getRepository("aiApiConfig").findOne() ?? null;
44
+ cache.configLoaded = true;
45
+ }
46
+ return cache.config;
47
+ }
48
+ async function resolveRequestUserGroup(ctx, userId) {
49
+ const cache = getCache(ctx);
50
+ const groupKey = userId === void 0 || userId === null ? "" : String(userId);
51
+ if (!cache.group || cache.groupKey !== groupKey) {
52
+ cache.group = await (0, import_quota_groups.resolveUserGroup)(ctx, userId);
53
+ cache.groupKey = groupKey;
54
+ }
55
+ return cache.group;
56
+ }
57
+ // Annotate the CommonJS export names for ESM import in node:
58
+ 0 && (module.exports = {
59
+ getAiApiConfig,
60
+ resolveRequestUserGroup
61
+ });
@@ -30,6 +30,7 @@ __export(resolve_service_exports, {
30
30
  resolveModelString: () => resolveModelString
31
31
  });
32
32
  module.exports = __toCommonJS(resolve_service_exports);
33
+ var import_request_cache = require("./request-cache");
33
34
  async function resolveLlmService(ctx, serviceKey) {
34
35
  const repo = ctx.db.getRepository("llmServices");
35
36
  let service = await repo.findOne({ filter: { name: serviceKey } });
@@ -60,7 +61,7 @@ async function resolveModelString(ctx, modelString) {
60
61
  }
61
62
  }
62
63
  }
63
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
64
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
64
65
  if (config == null ? void 0 : config.defaultLlmService) {
65
66
  const service = await repo.findOne({ filter: { name: config.defaultLlmService } });
66
67
  if (service) {