plugin-ai-api 1.0.25 → 1.1.0

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
@@ -44,7 +44,7 @@ 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");
@@ -59,7 +59,7 @@ import_dayjs.default.extend(import_timezone.default);
59
59
  class PluginAiApiServer extends import_server.Plugin {
60
60
  /**
61
61
  * Singleton rate limiter — lives for the entire plugin lifetime, shared across all requests.
62
- * 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.
63
63
  */
64
64
  rateLimiter = new import_rate_limiter.RateLimiter(6e4);
65
65
  /**
@@ -77,8 +77,54 @@ class PluginAiApiServer extends import_server.Plugin {
77
77
  this.app.db.on("aiApiModelMetadata.beforeSave", (model) => {
78
78
  (0, import_validation.validateModelMetadata)(model);
79
79
  });
80
- this.app.db.on("aiApiUserQuotaPolicies.beforeSave", (model) => {
80
+ this.app.db.on("aiApiUsageGroups.beforeSave", async (model, options) => {
81
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
+ }
82
128
  });
83
129
  }
84
130
  async load() {
@@ -95,18 +141,18 @@ class PluginAiApiServer extends import_server.Plugin {
95
141
  this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
96
142
  this.app.resourceManager.define(import_ai_api_config.default);
97
143
  this.app.resourceManager.define(import_ai_api_usage_monitor.default);
98
- this.app.resourceManager.define(import_ai_api_user_permissions.default);
144
+ this.app.resourceManager.define(import_ai_api_usage_groups.default);
99
145
  this.app.db.on("aiApiRolePermissions.afterSave", (model) => {
100
146
  (0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
101
147
  });
102
148
  this.app.db.on("aiApiRolePermissions.afterDestroy", (model) => {
103
149
  (0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
104
150
  });
105
- this.app.db.on("aiApiUserPermissions.afterSave", (model, options) => {
106
- 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);
107
153
  });
108
- this.app.db.on("aiApiUserPermissions.afterDestroy", (model, options) => {
109
- 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);
110
156
  });
111
157
  this.app.acl.registerSnippet({
112
158
  name: import_constants.AI_API_ACL_SNIPPET,
@@ -115,38 +161,35 @@ class PluginAiApiServer extends import_server.Plugin {
115
161
  "aiApiRolePermissions:*",
116
162
  "aiApiModelPrices:*",
117
163
  "aiApiModelMetadata:*",
118
- "aiApiUserQuotaPolicies:*",
119
- "aiApiUserQuotaBuckets:list",
120
- "aiApiUserQuotaBuckets:get",
164
+ "aiApiUsageGroups:*",
165
+ "aiApiGroupMembers:*",
166
+ "aiApiGroupQuotaBuckets:list",
167
+ "aiApiGroupQuotaBuckets:get",
121
168
  "aiApiUsageRecords:list",
122
169
  "aiApiUsageRecords:get",
123
170
  "aiApiUsageMonitor:summary"
124
171
  ]
125
172
  });
126
- this.app.acl.registerSnippet({
127
- name: import_constants.AI_API_USER_PERMISSIONS_SNIPPET,
128
- actions: ["aiApiUserPermissions:*"]
129
- });
130
173
  this.gcInterval = setInterval(() => this.rateLimiter.gc(), 5 * 60 * 1e3);
131
174
  this.gcInterval.unref();
132
175
  }
133
176
  /**
134
- * Drop a user's cached LLM scope on every node.
177
+ * Drop a group's cached model-access scope on every node.
135
178
  *
136
179
  * The local call is not redundant: syncMessageManager hardcodes skipSelf, so the publishing
137
180
  * node never receives its own message. Passing the transaction defers the broadcast until
138
181
  * the write commits, so other nodes cannot re-read the old row and re-cache it.
139
182
  */
140
- revokeUserPermissions(userId, transaction) {
141
- (0, import_user_permissions.invalidateUserPermissionCache)(userId);
142
- this.sendSyncMessage({ type: "invalidateUserPermissions", userId }, { transaction });
183
+ invalidateGroupAccess(groupId, transaction) {
184
+ (0, import_user_permissions.invalidateGroupAccessCache)(groupId);
185
+ this.sendSyncMessage({ type: "invalidateGroupAccess", groupId }, { transaction });
143
186
  }
144
187
  /**
145
188
  * Received only on the *other* nodes (skipSelf), so this must not re-broadcast.
146
189
  */
147
190
  async handleSyncMessage(message) {
148
- if ((message == null ? void 0 : message.type) === "invalidateUserPermissions") {
149
- (0, import_user_permissions.invalidateUserPermissionCache)(message.userId);
191
+ if ((message == null ? void 0 : message.type) === "invalidateGroupAccess") {
192
+ (0, import_user_permissions.invalidateGroupAccessCache)(message.groupId);
150
193
  }
151
194
  }
152
195
  async install() {
@@ -156,13 +199,38 @@ class PluginAiApiServer extends import_server.Plugin {
156
199
  values: {
157
200
  defaultAiEmployee: "",
158
201
  enabledLlmServices: [],
159
- rateLimitPerMinute: 60,
160
202
  quotaEnabled: false,
161
203
  pdfRenderPagesAsImages: false,
162
204
  defaultReservationOutputTokens: 4096
163
205
  }
164
206
  });
165
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
+ }
166
234
  }
167
235
  async afterEnable() {
168
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,7 +53,6 @@ const aiApiConfigResource = {
53
53
  defaultAiEmployee: "",
54
54
  defaultLlmService: "",
55
55
  enabledLlmServices: [],
56
- rateLimitPerMinute: 60,
57
56
  maxRequestBodyMb: 10,
58
57
  pdfRenderPagesAsImages: false,
59
58
  quotaEnabled: false,
@@ -76,7 +75,6 @@ const aiApiConfigResource = {
76
75
  defaultAiEmployee: values.defaultAiEmployee ?? "",
77
76
  defaultLlmService: values.defaultLlmService ?? "",
78
77
  enabledLlmServices: values.enabledLlmServices ?? [],
79
- rateLimitPerMinute: values.rateLimitPerMinute ?? 60,
80
78
  maxRequestBodyMb: coerceMaxRequestBodyMb(values.maxRequestBodyMb ?? DEFAULT_MAX_REQUEST_BODY_MB),
81
79
  pdfRenderPagesAsImages: values.pdfRenderPagesAsImages ?? false,
82
80
  quotaEnabled: values.quotaEnabled ?? false,
@@ -90,7 +88,6 @@ const aiApiConfigResource = {
90
88
  if (values.defaultAiEmployee !== void 0) updateData.defaultAiEmployee = values.defaultAiEmployee;
91
89
  if (values.defaultLlmService !== void 0) updateData.defaultLlmService = values.defaultLlmService;
92
90
  if (values.enabledLlmServices !== void 0) updateData.enabledLlmServices = values.enabledLlmServices;
93
- if (values.rateLimitPerMinute !== void 0) updateData.rateLimitPerMinute = values.rateLimitPerMinute;
94
91
  if (values.maxRequestBodyMb !== void 0) {
95
92
  updateData.maxRequestBodyMb = coerceMaxRequestBodyMb(values.maxRequestBodyMb);
96
93
  }
@@ -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
+ };
@@ -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;
@@ -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,8 +38,8 @@ 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");
@@ -119,7 +120,7 @@ async function handleChatCompletions(ctx, plugin) {
119
120
  );
120
121
  return;
121
122
  }
122
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
123
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
123
124
  if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
124
125
  return;
125
126
  }
@@ -148,30 +149,7 @@ async function handleChatCompletions(ctx, plugin) {
148
149
  if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
149
150
  if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
150
151
  if (body.stop !== void 0) modelOptions.stop = body.stop;
151
- let systemPrompt = "";
152
- if (config == null ? void 0 : config.defaultAiEmployee) {
153
- if (!(0, import_role_permission.checkEmployeeAccess)(ctx, config.defaultAiEmployee)) {
154
- ctx.status = 403;
155
- ctx.body = (0, import_openai_format.toOpenAIError)(
156
- 403,
157
- `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.`,
158
- "permission_denied",
159
- "employee_not_permitted"
160
- );
161
- return;
162
- }
163
- const employee = await ctx.db.getRepository("aiEmployees").findOne({
164
- filter: { username: config.defaultAiEmployee }
165
- });
166
- if (employee) {
167
- systemPrompt = employee.about || employee.defaultPrompt || "";
168
- }
169
- }
170
152
  let messages = [...body.messages];
171
- const hasSystemMessage = messages.some((m) => m.role === "system");
172
- if (systemPrompt && !hasSystemMessage) {
173
- messages.unshift({ role: "system", content: systemPrompt });
174
- }
175
153
  messages = await Promise.all(
176
154
  messages.map(async (msg) => ({
177
155
  ...msg,
@@ -271,12 +249,14 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
271
249
  );
272
250
  ctx.status = 200;
273
251
  const toolCalls = normalizeToolCalls(result.tool_calls);
252
+ const providerFinishReason = extractFinishReason(result);
274
253
  ctx.body = (0, import_openai_format.toOpenAIResponse)({
275
254
  id: completionId,
276
255
  model: modelName,
277
256
  content,
278
257
  usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
279
- toolCalls
258
+ toolCalls,
259
+ ...providerFinishReason ? { finishReason: providerFinishReason } : {}
280
260
  });
281
261
  }
282
262
  async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
@@ -300,8 +280,10 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
300
280
  );
301
281
  const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
302
282
  let usage;
283
+ let usageResponseMetadata;
303
284
  let providerRequestId;
304
- let finishReason = "stop";
285
+ let providerFinishReason;
286
+ let sawToolCalls = false;
305
287
  try {
306
288
  const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
307
289
  for await (const chunk of stream) {
@@ -329,7 +311,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
329
311
  const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
330
312
  if (toolCallChunks.length) {
331
313
  (0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
332
- finishReason = "tool_calls";
314
+ sawToolCalls = true;
333
315
  await (0, import_streaming.writeResponse)(
334
316
  ctx,
335
317
  (0, import_openai_format.formatSSE)(
@@ -342,8 +324,14 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
342
324
  );
343
325
  }
344
326
  if (chunk.usage_metadata) {
345
- 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
+ }
346
332
  }
333
+ const chunkFinishReason = extractFinishReason(chunk);
334
+ if (chunkFinishReason) providerFinishReason = chunkFinishReason;
347
335
  providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
348
336
  }
349
337
  await (0, import_streaming.writeResponse)(
@@ -353,7 +341,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
353
341
  id: completionId,
354
342
  model: modelName,
355
343
  delta: {},
356
- finishReason
344
+ finishReason: providerFinishReason ?? (sawToolCalls ? "tool_calls" : "stop")
357
345
  })
358
346
  )
359
347
  );
@@ -370,7 +358,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
370
358
  );
371
359
  }
372
360
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
373
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
361
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
374
362
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
375
363
  } catch (err) {
376
364
  const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
@@ -386,7 +374,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
386
374
  })
387
375
  );
388
376
  }
389
- (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
377
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
390
378
  ctx.state.aiApiStreamResult = {
391
379
  succeeded: false,
392
380
  id: completionId,
@@ -400,6 +388,17 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
400
388
  function getErrorMessage(error, fallback) {
401
389
  return error instanceof Error && error.message ? error.message : fallback;
402
390
  }
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
+ }
403
402
  const SUPPORTED_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image_url", "file", "file_url"]);
404
403
  const BASE64_DATA_URL_PATTERN = /^data:(\w+\/\w+);base64,([A-Za-z0-9+/]+=*)$/;
405
404
  const FILE_BASE64_DATA_URL_PATTERN = /^data:([^;\s]+);base64,([A-Za-z0-9+/]+=*)$/;
@@ -641,6 +640,7 @@ function serializeToolArguments(value) {
641
640
  // Annotate the CommonJS export names for ESM import in node:
642
641
  0 && (module.exports = {
643
642
  applyProviderRequestParameters,
643
+ extractFinishReason,
644
644
  findContentBlockProblem,
645
645
  findMessageProblem,
646
646
  getProviderRequestParameters,