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
@@ -44,12 +44,13 @@ var import_server = require("@nocobase/server");
44
44
  var import_router = require("./routes/router");
45
45
  var import_ai_api_config = __toESM(require("./resource/ai-api-config"));
46
46
  var import_ai_api_usage_monitor = __toESM(require("./resource/ai-api-usage-monitor"));
47
- var import_ai_api_user_permissions = __toESM(require("./resource/ai-api-user-permissions"));
47
+ var import_ai_api_usage_groups = __toESM(require("./resource/ai-api-usage-groups"));
48
48
  var import_rate_limiter = require("./utils/rate-limiter");
49
49
  var import_role_permission = require("./middleware/role-permission");
50
50
  var import_user_permissions = require("./utils/user-permissions");
51
51
  var import_validation = require("./validation");
52
52
  var import_constants = require("../constants");
53
+ var import_file_processor = require("./services/file-processor");
53
54
  var import_dayjs = __toESM(require("dayjs"));
54
55
  var import_utc = __toESM(require("dayjs/plugin/utc"));
55
56
  var import_timezone = __toESM(require("dayjs/plugin/timezone"));
@@ -58,9 +59,14 @@ import_dayjs.default.extend(import_timezone.default);
58
59
  class PluginAiApiServer extends import_server.Plugin {
59
60
  /**
60
61
  * Singleton rate limiter — lives for the entire plugin lifetime, shared across all requests.
61
- * Uses a 1-minute sliding window to enforce rateLimitPerMinute from aiApiConfig.
62
+ * Uses a 1-minute sliding window to enforce rateLimitPerMinute from the user's usage group.
62
63
  */
63
64
  rateLimiter = new import_rate_limiter.RateLimiter(6e4);
65
+ /**
66
+ * Extensible file processor service. Other plugins can register custom processors
67
+ * to transform file/file_url content blocks before they reach the LLM.
68
+ */
69
+ fileProcessorService = new import_file_processor.FileProcessorService();
64
70
  gcInterval = null;
65
71
  async afterAdd() {
66
72
  }
@@ -71,11 +77,60 @@ class PluginAiApiServer extends import_server.Plugin {
71
77
  this.app.db.on("aiApiModelMetadata.beforeSave", (model) => {
72
78
  (0, import_validation.validateModelMetadata)(model);
73
79
  });
74
- this.app.db.on("aiApiUserQuotaPolicies.beforeSave", (model) => {
80
+ this.app.db.on("aiApiUsageGroups.beforeSave", async (model, options) => {
75
81
  (0, import_validation.validateQuotaPolicy)(model);
82
+ if (model.get("isDefault")) {
83
+ const filter = { isDefault: true };
84
+ if (model.get("id")) filter.id = { $ne: model.get("id") };
85
+ const other = await this.db.getRepository("aiApiUsageGroups").findOne({
86
+ filter,
87
+ transaction: options == null ? void 0 : options.transaction
88
+ });
89
+ if (other) {
90
+ throw new Error("Only one default usage group is allowed.");
91
+ }
92
+ }
93
+ });
94
+ this.app.db.on("aiApiGroupMembers.beforeSave", async (model, options) => {
95
+ const userId = model.get("userId");
96
+ if (!userId) return;
97
+ const existing = await this.db.getRepository("aiApiGroupMembers").findOne({
98
+ filter: { userId },
99
+ transaction: options == null ? void 0 : options.transaction
100
+ });
101
+ if (existing && existing.get("id") !== model.get("id")) {
102
+ throw new Error("User already belongs to another usage group.");
103
+ }
104
+ });
105
+ this.app.db.on("aiApiUsageGroups.beforeDestroy", async (model, options) => {
106
+ if (model.get("isDefault")) {
107
+ throw new Error("The default usage group cannot be deleted.");
108
+ }
109
+ const members = await this.db.getRepository("aiApiGroupMembers").find({
110
+ filter: { groupId: model.get("id") },
111
+ transaction: options == null ? void 0 : options.transaction
112
+ });
113
+ if (members.length === 0) return;
114
+ const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
115
+ filter: { isDefault: true },
116
+ transaction: options == null ? void 0 : options.transaction
117
+ });
118
+ if (!defaultGroup) {
119
+ throw new Error("Default usage group is missing; cannot reassign members.");
120
+ }
121
+ for (const member of members) {
122
+ await this.db.getRepository("aiApiGroupMembers").update({
123
+ filterByTk: member.get("id"),
124
+ values: { groupId: defaultGroup.get("id") },
125
+ transaction: options == null ? void 0 : options.transaction
126
+ });
127
+ }
76
128
  });
77
129
  }
78
130
  async load() {
131
+ this.fileProcessorService.register(import_file_processor.base64FileForwarder);
132
+ this.fileProcessorService.register(import_file_processor.httpFileUrlFetcher);
133
+ this.fileProcessorService.register(import_file_processor.pdfFileProcessor);
79
134
  this.app.use(
80
135
  async (ctx, next) => {
81
136
  if (ctx.path.startsWith(import_router.AI_LLM_PREFIX)) ctx.disableBodyParser = true;
@@ -86,18 +141,18 @@ class PluginAiApiServer extends import_server.Plugin {
86
141
  this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
87
142
  this.app.resourceManager.define(import_ai_api_config.default);
88
143
  this.app.resourceManager.define(import_ai_api_usage_monitor.default);
89
- this.app.resourceManager.define(import_ai_api_user_permissions.default);
144
+ this.app.resourceManager.define(import_ai_api_usage_groups.default);
90
145
  this.app.db.on("aiApiRolePermissions.afterSave", (model) => {
91
146
  (0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
92
147
  });
93
148
  this.app.db.on("aiApiRolePermissions.afterDestroy", (model) => {
94
149
  (0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
95
150
  });
96
- this.app.db.on("aiApiUserPermissions.afterSave", (model, options) => {
97
- this.revokeUserPermissions(model.get("userId"), options == null ? void 0 : options.transaction);
151
+ this.app.db.on("aiApiUsageGroups.afterSave", (model, options) => {
152
+ this.invalidateGroupAccess(model.get("id"), options == null ? void 0 : options.transaction);
98
153
  });
99
- this.app.db.on("aiApiUserPermissions.afterDestroy", (model, options) => {
100
- this.revokeUserPermissions(model.get("userId"), options == null ? void 0 : options.transaction);
154
+ this.app.db.on("aiApiUsageGroups.afterDestroy", (model, options) => {
155
+ this.invalidateGroupAccess(model.get("id"), options == null ? void 0 : options.transaction);
101
156
  });
102
157
  this.app.acl.registerSnippet({
103
158
  name: import_constants.AI_API_ACL_SNIPPET,
@@ -106,38 +161,35 @@ class PluginAiApiServer extends import_server.Plugin {
106
161
  "aiApiRolePermissions:*",
107
162
  "aiApiModelPrices:*",
108
163
  "aiApiModelMetadata:*",
109
- "aiApiUserQuotaPolicies:*",
110
- "aiApiUserQuotaBuckets:list",
111
- "aiApiUserQuotaBuckets:get",
164
+ "aiApiUsageGroups:*",
165
+ "aiApiGroupMembers:*",
166
+ "aiApiGroupQuotaBuckets:list",
167
+ "aiApiGroupQuotaBuckets:get",
112
168
  "aiApiUsageRecords:list",
113
169
  "aiApiUsageRecords:get",
114
170
  "aiApiUsageMonitor:summary"
115
171
  ]
116
172
  });
117
- this.app.acl.registerSnippet({
118
- name: import_constants.AI_API_USER_PERMISSIONS_SNIPPET,
119
- actions: ["aiApiUserPermissions:*"]
120
- });
121
173
  this.gcInterval = setInterval(() => this.rateLimiter.gc(), 5 * 60 * 1e3);
122
174
  this.gcInterval.unref();
123
175
  }
124
176
  /**
125
- * Drop a user's cached LLM scope on every node.
177
+ * Drop a group's cached model-access scope on every node.
126
178
  *
127
179
  * The local call is not redundant: syncMessageManager hardcodes skipSelf, so the publishing
128
180
  * node never receives its own message. Passing the transaction defers the broadcast until
129
181
  * the write commits, so other nodes cannot re-read the old row and re-cache it.
130
182
  */
131
- revokeUserPermissions(userId, transaction) {
132
- (0, import_user_permissions.invalidateUserPermissionCache)(userId);
133
- this.sendSyncMessage({ type: "invalidateUserPermissions", userId }, { transaction });
183
+ invalidateGroupAccess(groupId, transaction) {
184
+ (0, import_user_permissions.invalidateGroupAccessCache)(groupId);
185
+ this.sendSyncMessage({ type: "invalidateGroupAccess", groupId }, { transaction });
134
186
  }
135
187
  /**
136
188
  * Received only on the *other* nodes (skipSelf), so this must not re-broadcast.
137
189
  */
138
190
  async handleSyncMessage(message) {
139
- if ((message == null ? void 0 : message.type) === "invalidateUserPermissions") {
140
- (0, import_user_permissions.invalidateUserPermissionCache)(message.userId);
191
+ if ((message == null ? void 0 : message.type) === "invalidateGroupAccess") {
192
+ (0, import_user_permissions.invalidateGroupAccessCache)(message.groupId);
141
193
  }
142
194
  }
143
195
  async install() {
@@ -147,12 +199,38 @@ class PluginAiApiServer extends import_server.Plugin {
147
199
  values: {
148
200
  defaultAiEmployee: "",
149
201
  enabledLlmServices: [],
150
- rateLimitPerMinute: 60,
151
202
  quotaEnabled: false,
203
+ pdfRenderPagesAsImages: false,
152
204
  defaultReservationOutputTokens: 4096
153
205
  }
154
206
  });
155
207
  }
208
+ const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
209
+ filter: { isDefault: true }
210
+ });
211
+ if (!defaultGroup) {
212
+ await this.db.getRepository("aiApiUsageGroups").create({
213
+ values: {
214
+ name: "Default",
215
+ isDefault: true,
216
+ quotaMode: "per_user",
217
+ rateLimitPerMinute: 60,
218
+ enabled: false,
219
+ periodType: "monthly",
220
+ timezone: "UTC",
221
+ requestLimit: null,
222
+ totalTokenLimit: null,
223
+ costLimit: null,
224
+ currency: "USD",
225
+ rejectUnpricedModel: true,
226
+ missingUsageBehavior: "use_reserved",
227
+ contextOverflowBehavior: "reject",
228
+ allowedLlmServices: [],
229
+ allowAllModels: true,
230
+ allowedModels: []
231
+ }
232
+ });
233
+ }
156
234
  }
157
235
  async afterEnable() {
158
236
  }
@@ -0,0 +1,108 @@
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 quota_groups_exports = {};
28
+ __export(quota_groups_exports, {
29
+ getDefaultGroup: () => getDefaultGroup,
30
+ modelToGroup: () => modelToGroup,
31
+ resolveUserGroup: () => resolveUserGroup
32
+ });
33
+ module.exports = __toCommonJS(quota_groups_exports);
34
+ async function getDefaultGroup(ctx) {
35
+ const existing = await ctx.db.getRepository("aiApiUsageGroups").findOne({
36
+ filter: { isDefault: true }
37
+ });
38
+ if (existing) {
39
+ return modelToGroup(existing);
40
+ }
41
+ const created = await ctx.db.getRepository("aiApiUsageGroups").create({
42
+ values: {
43
+ name: "Default",
44
+ isDefault: true,
45
+ quotaMode: "per_user",
46
+ rateLimitPerMinute: 60,
47
+ enabled: false,
48
+ periodType: "monthly",
49
+ timezone: "UTC",
50
+ requestLimit: null,
51
+ totalTokenLimit: null,
52
+ costLimit: null,
53
+ currency: "USD",
54
+ rejectUnpricedModel: true,
55
+ missingUsageBehavior: "use_reserved",
56
+ contextOverflowBehavior: "reject",
57
+ allowedLlmServices: [],
58
+ allowAllModels: true,
59
+ allowedModels: []
60
+ }
61
+ });
62
+ return modelToGroup(created);
63
+ }
64
+ async function resolveUserGroup(ctx, userId) {
65
+ if (userId === void 0 || userId === null) {
66
+ return getDefaultGroup(ctx);
67
+ }
68
+ const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
69
+ filter: { userId },
70
+ appends: ["group"]
71
+ });
72
+ if (member && member.get("group")) {
73
+ return modelToGroup(member.get("group"));
74
+ }
75
+ return getDefaultGroup(ctx);
76
+ }
77
+ function modelToGroup(model) {
78
+ const record = typeof model.get === "function" ? model.get() : model;
79
+ return {
80
+ id: record.id,
81
+ name: record.name ?? "",
82
+ isDefault: !!record.isDefault,
83
+ quotaMode: record.quotaMode ?? "per_user",
84
+ rateLimitPerMinute: Number(record.rateLimitPerMinute ?? 60),
85
+ enabled: !!record.enabled,
86
+ periodType: record.periodType ?? "monthly",
87
+ timezone: record.timezone ?? "UTC",
88
+ requestLimit: record.requestLimit ?? null,
89
+ totalTokenLimit: record.totalTokenLimit ?? null,
90
+ costLimit: record.costLimit ?? null,
91
+ currency: record.currency ?? "USD",
92
+ rejectUnpricedModel: !!record.rejectUnpricedModel,
93
+ missingUsageBehavior: record.missingUsageBehavior ?? "use_reserved",
94
+ contextOverflowBehavior: record.contextOverflowBehavior ?? "reject",
95
+ allowedLlmServices: stringArray(record.allowedLlmServices),
96
+ allowAllModels: record.allowAllModels !== false,
97
+ allowedModels: stringArray(record.allowedModels)
98
+ };
99
+ }
100
+ function stringArray(value) {
101
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
102
+ }
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ getDefaultGroup,
106
+ modelToGroup,
107
+ resolveUserGroup
108
+ });
@@ -53,8 +53,8 @@ const aiApiConfigResource = {
53
53
  defaultAiEmployee: "",
54
54
  defaultLlmService: "",
55
55
  enabledLlmServices: [],
56
- rateLimitPerMinute: 60,
57
56
  maxRequestBodyMb: 10,
57
+ pdfRenderPagesAsImages: false,
58
58
  quotaEnabled: false,
59
59
  defaultReservationOutputTokens: 4096,
60
60
  options: {}
@@ -75,8 +75,8 @@ const aiApiConfigResource = {
75
75
  defaultAiEmployee: values.defaultAiEmployee ?? "",
76
76
  defaultLlmService: values.defaultLlmService ?? "",
77
77
  enabledLlmServices: values.enabledLlmServices ?? [],
78
- rateLimitPerMinute: values.rateLimitPerMinute ?? 60,
79
78
  maxRequestBodyMb: coerceMaxRequestBodyMb(values.maxRequestBodyMb ?? DEFAULT_MAX_REQUEST_BODY_MB),
79
+ pdfRenderPagesAsImages: values.pdfRenderPagesAsImages ?? false,
80
80
  quotaEnabled: values.quotaEnabled ?? false,
81
81
  defaultReservationOutputTokens: values.defaultReservationOutputTokens ?? 4096,
82
82
  options: values.options ?? {}
@@ -88,10 +88,12 @@ const aiApiConfigResource = {
88
88
  if (values.defaultAiEmployee !== void 0) updateData.defaultAiEmployee = values.defaultAiEmployee;
89
89
  if (values.defaultLlmService !== void 0) updateData.defaultLlmService = values.defaultLlmService;
90
90
  if (values.enabledLlmServices !== void 0) updateData.enabledLlmServices = values.enabledLlmServices;
91
- if (values.rateLimitPerMinute !== void 0) updateData.rateLimitPerMinute = values.rateLimitPerMinute;
92
91
  if (values.maxRequestBodyMb !== void 0) {
93
92
  updateData.maxRequestBodyMb = coerceMaxRequestBodyMb(values.maxRequestBodyMb);
94
93
  }
94
+ if (values.pdfRenderPagesAsImages !== void 0) {
95
+ updateData.pdfRenderPagesAsImages = Boolean(values.pdfRenderPagesAsImages);
96
+ }
95
97
  if (values.quotaEnabled !== void 0) updateData.quotaEnabled = values.quotaEnabled;
96
98
  if (values.defaultReservationOutputTokens !== void 0) {
97
99
  updateData.defaultReservationOutputTokens = values.defaultReservationOutputTokens;
@@ -0,0 +1,168 @@
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 ai_api_usage_groups_exports = {};
28
+ __export(ai_api_usage_groups_exports, {
29
+ default: () => ai_api_usage_groups_default
30
+ });
31
+ module.exports = __toCommonJS(ai_api_usage_groups_exports);
32
+ const USER_SUMMARY_FIELDS = ["id", "username", "nickname", "email"];
33
+ async function getByUser(ctx, next) {
34
+ const { userId } = ctx.action.params;
35
+ if (!userId) {
36
+ ctx.throw(400, "userId is required");
37
+ }
38
+ const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
39
+ filter: { userId },
40
+ appends: ["group"]
41
+ });
42
+ if (member && member.get("group")) {
43
+ ctx.body = member.get("group");
44
+ return next();
45
+ }
46
+ const defaultGroup = await ctx.db.getRepository("aiApiUsageGroups").findOne({
47
+ filter: { isDefault: true }
48
+ });
49
+ if (!defaultGroup) {
50
+ ctx.throw(404, "Default group not found");
51
+ }
52
+ ctx.body = defaultGroup;
53
+ return next();
54
+ }
55
+ async function listUnassignedUsers(ctx, next) {
56
+ const { page = 1, pageSize = 20, keyword } = ctx.action.params;
57
+ const memberRows = await ctx.db.getRepository("aiApiGroupMembers").find({
58
+ fields: ["userId"],
59
+ pageSize: 1e4
60
+ });
61
+ const assignedUserIds = memberRows.map((row) => {
62
+ var _a;
63
+ const value = ((_a = row.get) == null ? void 0 : _a.call(row, "userId")) ?? row.userId;
64
+ return String(value);
65
+ });
66
+ const where = {};
67
+ if (keyword) {
68
+ where.$or = [
69
+ { username: { $includes: keyword } },
70
+ { email: { $includes: keyword } },
71
+ { nickname: { $includes: keyword } }
72
+ ];
73
+ }
74
+ if (assignedUserIds.length > 0) {
75
+ where.id = { $notIn: assignedUserIds };
76
+ }
77
+ const repository = ctx.db.getRepository("users");
78
+ const [rows, count] = await repository.findAndCount({
79
+ filter: where,
80
+ fields: USER_SUMMARY_FIELDS,
81
+ page: Number(page),
82
+ pageSize: Number(pageSize)
83
+ });
84
+ ctx.body = { rows, count };
85
+ return next();
86
+ }
87
+ async function searchUsers(ctx, next) {
88
+ const { keyword, pageSize = 20 } = ctx.action.params;
89
+ const where = {};
90
+ if (keyword) {
91
+ where.$or = [
92
+ { username: { $includes: keyword } },
93
+ { email: { $includes: keyword } },
94
+ { nickname: { $includes: keyword } }
95
+ ];
96
+ }
97
+ const [rows, count] = await ctx.db.getRepository("users").findAndCount({
98
+ filter: where,
99
+ fields: USER_SUMMARY_FIELDS,
100
+ page: 1,
101
+ pageSize: Number(pageSize)
102
+ });
103
+ ctx.body = { rows, count };
104
+ return next();
105
+ }
106
+ async function addMember(ctx, next) {
107
+ const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
108
+ if (!groupId || !userId) {
109
+ ctx.throw(400, "groupId and userId are required");
110
+ }
111
+ const group = await ctx.db.getRepository("aiApiUsageGroups").findOne({
112
+ filterByTk: groupId
113
+ });
114
+ if (!group) {
115
+ ctx.throw(404, "Usage group not found");
116
+ }
117
+ if (group.get("isDefault")) {
118
+ ctx.throw(400, "Cannot add members to the default group: users without another group use it automatically");
119
+ }
120
+ const existing = await ctx.db.getRepository("aiApiGroupMembers").findOne({
121
+ filter: { userId }
122
+ });
123
+ if (existing) {
124
+ ctx.throw(409, "User already belongs to a group");
125
+ }
126
+ const member = await ctx.db.getRepository("aiApiGroupMembers").create({
127
+ values: { groupId, userId }
128
+ });
129
+ ctx.body = member;
130
+ return next();
131
+ }
132
+ async function removeMember(ctx, next) {
133
+ const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
134
+ if (!groupId || !userId) {
135
+ ctx.throw(400, "groupId and userId are required");
136
+ }
137
+ const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
138
+ filter: { groupId, userId }
139
+ });
140
+ if (!member) {
141
+ ctx.throw(404, "Member not found");
142
+ }
143
+ await ctx.db.getRepository("aiApiGroupMembers").destroy({
144
+ filterByTk: member.get("id")
145
+ });
146
+ ctx.body = member;
147
+ return next();
148
+ }
149
+ var ai_api_usage_groups_default = {
150
+ name: "aiApiUsageGroups",
151
+ actions: {
152
+ getByUser: {
153
+ handler: getByUser
154
+ },
155
+ listUnassignedUsers: {
156
+ handler: listUnassignedUsers
157
+ },
158
+ searchUsers: {
159
+ handler: searchUsers
160
+ },
161
+ addMember: {
162
+ handler: addMember
163
+ },
164
+ removeMember: {
165
+ handler: removeMember
166
+ }
167
+ }
168
+ };
@@ -58,7 +58,8 @@ const aiApiUsageMonitorResource = {
58
58
  [(0, import_sequelize.fn)("COUNT", (0, import_sequelize.col)("id")), "requestCount"],
59
59
  [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("inputTokens")), 0), "inputTokens"],
60
60
  [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("outputTokens")), 0), "outputTokens"],
61
- [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("totalTokens")), 0), "totalTokens"]
61
+ [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("totalTokens")), 0), "totalTokens"],
62
+ [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("promptCacheTokens")), 0), "promptCacheTokens"]
62
63
  ],
63
64
  where,
64
65
  raw: true
@@ -74,6 +75,7 @@ const aiApiUsageMonitorResource = {
74
75
  inputTokens: Number((totals == null ? void 0 : totals.inputTokens) ?? 0),
75
76
  outputTokens: Number((totals == null ? void 0 : totals.outputTokens) ?? 0),
76
77
  totalTokens: Number((totals == null ? void 0 : totals.totalTokens) ?? 0),
78
+ promptCacheTokens: Number((totals == null ? void 0 : totals.promptCacheTokens) ?? 0),
77
79
  costsByCurrency: costs.map((item) => ({
78
80
  currency: item.currency || "USD",
79
81
  totalCost: String(item.totalCost ?? 0)
@@ -33,6 +33,7 @@ var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
34
  var import_role_permission = require("../middleware/role-permission");
35
35
  var import_user_permissions = require("../utils/user-permissions");
36
+ var import_request_cache = require("../utils/request-cache");
36
37
  var import_streaming = require("../utils/streaming");
37
38
  var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
38
39
  var import_usage = require("../usage");
@@ -60,7 +61,7 @@ async function handleAgentCompletions(ctx, plugin) {
60
61
  );
61
62
  return;
62
63
  }
63
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
64
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
64
65
  const defaultAiEmployee = config ? config.get("defaultAiEmployee") || config.defaultAiEmployee : null;
65
66
  if (!defaultAiEmployee) {
66
67
  ctx.status = 400;