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.
- package/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
- package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
- package/dist/client/562.44b16aad4718b4c7.js +10 -0
- package/dist/client/685.ae483e17b6b49c98.js +10 -0
- package/dist/client/757.6568d3504ad29352.js +10 -0
- package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.3971233415999b2c.js +10 -0
- package/dist/client-v2/562.45d5c504433be38b.js +10 -0
- package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
- package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
- package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
- package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +2 -5
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +27 -8
- package/dist/locale/vi-VN.json +27 -8
- package/dist/locale/zh-CN.json +27 -8
- package/dist/server/billing.js +31 -33
- package/dist/server/collections/ai-api-config.js +7 -7
- package/dist/server/collections/ai-api-group-members.js +62 -0
- package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
- package/dist/server/collections/ai-api-model-metadata.js +6 -0
- package/dist/server/collections/ai-api-usage-groups.js +74 -0
- package/dist/server/collections/ai-api-usage-records.js +2 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
- package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
- package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
- package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
- package/dist/server/plugin.js +100 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +5 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/resource/ai-api-usage-monitor.js +3 -1
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +121 -42
- package/dist/server/routes/completions.js +48 -29
- package/dist/server/routes/embeddings.js +2 -1
- package/dist/server/routes/models.js +2 -1
- package/dist/server/routes/router.js +3 -2
- package/dist/server/services/file-processor.js +426 -0
- package/dist/server/usage.js +37 -3
- package/dist/server/utils/direct-llm-context.js +163 -26
- package/dist/server/utils/openai-format.js +21 -2
- package/dist/server/utils/rate-limiter.js +1 -1
- package/dist/server/utils/request-cache.js +61 -0
- package/dist/server/utils/resolve-service.js +2 -1
- package/dist/server/utils/user-permissions.js +25 -39
- package/dist/server/validation.js +7 -0
- package/dist/swagger.js +48 -10
- package/package.json +1 -1
- package/src/client/__tests__/settings-registration.test.tsx +6 -29
- package/src/client/plugin.tsx +5 -16
- package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
- package/src/client-v2/locale.ts +3 -1
- package/src/client-v2/pages/GeneralPage.tsx +0 -5
- package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
- package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
- package/src/client-v2/pages/UsagePage.tsx +9 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +27 -8
- package/src/locale/vi-VN.json +27 -8
- package/src/locale/zh-CN.json +27 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +209 -10
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/openai-format.test.ts +12 -2
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/request-body.test.ts +45 -2
- package/src/server/__tests__/usage-groups.test.ts +160 -0
- package/src/server/__tests__/usage-monitor.test.ts +2 -0
- package/src/server/__tests__/usage-route.test.ts +382 -5
- package/src/server/__tests__/usage.test.ts +57 -0
- package/src/server/__tests__/user-permissions.test.ts +214 -133
- package/src/server/__tests__/validation.test.ts +11 -0
- package/src/server/billing.ts +36 -39
- package/src/server/collections/ai-api-config.ts +9 -7
- package/src/server/collections/ai-api-group-members.ts +41 -0
- package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
- package/src/server/collections/ai-api-model-metadata.ts +7 -0
- package/src/server/collections/ai-api-role-permissions.ts +41 -41
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +2 -0
- package/src/server/index.ts +10 -10
- package/src/server/middleware/rate-limit.ts +68 -70
- package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
- package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
- package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
- package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
- package/src/server/plugin.ts +121 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +5 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/resource/ai-api-usage-monitor.ts +3 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +173 -47
- package/src/server/routes/completions.ts +50 -27
- package/src/server/routes/embeddings.ts +2 -1
- package/src/server/routes/models.ts +4 -3
- package/src/server/routes/router.ts +4 -3
- package/src/server/services/__tests__/file-processor.test.ts +184 -0
- package/src/server/services/file-processor.ts +513 -0
- package/src/server/usage.ts +51 -1
- package/src/server/utils/direct-llm-context.ts +218 -31
- package/src/server/utils/openai-format.ts +25 -2
- package/src/server/utils/rate-limiter.ts +83 -83
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +83 -82
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +52 -11
- package/dist/client/123.e6fe04c856ce6417.js +0 -10
- package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
- package/dist/client/562.17a0a299d2e5152c.js +0 -10
- package/dist/client/757.a01403fb7a1bea01.js +0 -10
- package/dist/client/902.e74518750f1e4201.js +0 -10
- package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
- package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
- package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
- package/dist/client-v2/902.c7c00a565085438a.js +0 -10
- package/dist/server/resource/ai-api-user-permissions.js +0 -75
- package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
- package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
- package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
- package/src/server/resource/ai-api-user-permissions.ts +0 -76
package/dist/server/plugin.js
CHANGED
|
@@ -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
|
|
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
|
|
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("
|
|
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(
|
|
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("
|
|
97
|
-
this.
|
|
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("
|
|
100
|
-
this.
|
|
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
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
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
|
|
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
|
-
|
|
132
|
-
(0, import_user_permissions.
|
|
133
|
-
this.sendSyncMessage({ type: "
|
|
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) === "
|
|
140
|
-
(0, import_user_permissions.
|
|
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
|
|
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;
|