plugin-ai-api 1.0.15 → 1.0.20
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/302.25edd5d75460acbf.js +10 -0
- package/dist/client/757.71e30f2a1306562d.js +10 -0
- package/dist/client/902.4238b04ac667c30a.js +10 -0
- package/dist/client/97.37cda285d7da3a26.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.9b27a263901d54d8.js +10 -0
- package/dist/client-v2/757.c377e2f2b054d89d.js +10 -0
- package/dist/client-v2/902.d40d7bda106124c8.js +10 -0
- package/dist/client-v2/97.fc922c37ced86831.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +9 -9
- package/dist/locale/en-US.json +78 -2
- package/dist/locale/vi-VN.json +86 -0
- package/dist/locale/zh-CN.json +86 -10
- package/dist/server/billing.js +331 -0
- package/dist/server/collections/ai-api-config.js +12 -0
- package/dist/server/collections/ai-api-model-prices.js +55 -0
- package/dist/server/collections/ai-api-usage-records.js +9 -0
- package/dist/server/collections/ai-api-user-quota-buckets.js +54 -0
- package/dist/server/collections/ai-api-user-quota-policies.js +62 -0
- package/dist/server/plugin.js +23 -2
- package/dist/server/resource/ai-api-config.js +8 -0
- package/dist/server/resource/ai-api-usage-monitor.js +86 -0
- package/dist/server/routes/chat-completions.js +12 -2
- package/dist/server/routes/completions.js +12 -2
- package/dist/server/routes/router.js +14 -1
- package/dist/server/usage.js +17 -2
- package/dist/server/validation.js +102 -0
- package/package.json +1 -1
- package/src/client/plugin.tsx +73 -48
- package/src/client-v2/locale.ts +1 -0
- package/src/client-v2/pages/GeneralPage.tsx +170 -0
- package/src/client-v2/pages/ModelPricingPage.tsx +285 -0
- package/src/client-v2/pages/UsagePage.tsx +248 -0
- package/src/client-v2/pages/UserQuotasPage.tsx +258 -0
- package/src/client-v2/pages/api.ts +16 -0
- package/src/client-v2/plugin.tsx +21 -3
- package/src/locale/en-US.json +78 -2
- package/src/locale/vi-VN.json +86 -0
- package/src/locale/zh-CN.json +86 -10
- package/src/server/__tests__/billing-quota.test.ts +134 -0
- package/src/server/__tests__/billing.test.ts +33 -0
- package/src/server/__tests__/usage-monitor.test.ts +63 -0
- package/src/server/__tests__/usage-route.test.ts +4 -0
- package/src/server/billing.ts +387 -0
- package/src/server/collections/ai-api-config.ts +63 -51
- package/src/server/collections/ai-api-model-prices.ts +25 -0
- package/src/server/collections/ai-api-usage-records.ts +9 -0
- package/src/server/collections/ai-api-user-quota-buckets.ts +24 -0
- package/src/server/collections/ai-api-user-quota-policies.ts +32 -0
- package/src/server/plugin.ts +24 -2
- package/src/server/resource/ai-api-config.ts +82 -74
- package/src/server/resource/ai-api-usage-monitor.ts +74 -0
- package/src/server/routes/chat-completions.ts +13 -2
- package/src/server/routes/completions.ts +13 -2
- package/src/server/routes/router.ts +16 -1
- package/src/server/usage.ts +17 -1
- package/src/server/validation.ts +62 -0
- package/dist/client/950.83390c5f1d5a97fb.js +0 -10
- package/dist/client-v2/950.42b30b5cc9e32b8f.js +0 -10
package/dist/server/plugin.js
CHANGED
|
@@ -43,8 +43,10 @@ module.exports = __toCommonJS(plugin_exports);
|
|
|
43
43
|
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
|
+
var import_ai_api_usage_monitor = __toESM(require("./resource/ai-api-usage-monitor"));
|
|
46
47
|
var import_rate_limiter = require("./utils/rate-limiter");
|
|
47
48
|
var import_role_permission = require("./middleware/role-permission");
|
|
49
|
+
var import_validation = require("./validation");
|
|
48
50
|
var import_dayjs = __toESM(require("dayjs"));
|
|
49
51
|
var import_utc = __toESM(require("dayjs/plugin/utc"));
|
|
50
52
|
var import_timezone = __toESM(require("dayjs/plugin/timezone"));
|
|
@@ -60,10 +62,17 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
60
62
|
async afterAdd() {
|
|
61
63
|
}
|
|
62
64
|
async beforeLoad() {
|
|
65
|
+
this.app.db.on("aiApiModelPrices.beforeSave", async (model) => {
|
|
66
|
+
await (0, import_validation.validateModelPrice)(this.db, model);
|
|
67
|
+
});
|
|
68
|
+
this.app.db.on("aiApiUserQuotaPolicies.beforeSave", (model) => {
|
|
69
|
+
(0, import_validation.validateQuotaPolicy)(model);
|
|
70
|
+
});
|
|
63
71
|
}
|
|
64
72
|
async load() {
|
|
65
73
|
this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
|
|
66
74
|
this.app.resourceManager.define(import_ai_api_config.default);
|
|
75
|
+
this.app.resourceManager.define(import_ai_api_usage_monitor.default);
|
|
67
76
|
this.app.db.on("aiApiRolePermissions.afterSave", (model) => {
|
|
68
77
|
(0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
|
|
69
78
|
});
|
|
@@ -72,7 +81,17 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
72
81
|
});
|
|
73
82
|
this.app.acl.registerSnippet({
|
|
74
83
|
name: `pm.${this.name}.configuration`,
|
|
75
|
-
actions: [
|
|
84
|
+
actions: [
|
|
85
|
+
"aiApiConfig:*",
|
|
86
|
+
"aiApiRolePermissions:*",
|
|
87
|
+
"aiApiModelPrices:*",
|
|
88
|
+
"aiApiUserQuotaPolicies:*",
|
|
89
|
+
"aiApiUserQuotaBuckets:list",
|
|
90
|
+
"aiApiUserQuotaBuckets:get",
|
|
91
|
+
"aiApiUsageRecords:list",
|
|
92
|
+
"aiApiUsageRecords:get",
|
|
93
|
+
"aiApiUsageMonitor:summary"
|
|
94
|
+
]
|
|
76
95
|
});
|
|
77
96
|
this.gcInterval = setInterval(() => this.rateLimiter.gc(), 5 * 60 * 1e3);
|
|
78
97
|
this.gcInterval.unref();
|
|
@@ -84,7 +103,9 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
84
103
|
values: {
|
|
85
104
|
defaultAiEmployee: "",
|
|
86
105
|
enabledLlmServices: [],
|
|
87
|
-
rateLimitPerMinute: 60
|
|
106
|
+
rateLimitPerMinute: 60,
|
|
107
|
+
quotaEnabled: false,
|
|
108
|
+
defaultReservationOutputTokens: 4096
|
|
88
109
|
}
|
|
89
110
|
});
|
|
90
111
|
}
|
|
@@ -42,6 +42,8 @@ const aiApiConfigResource = {
|
|
|
42
42
|
defaultLlmService: "",
|
|
43
43
|
enabledLlmServices: [],
|
|
44
44
|
rateLimitPerMinute: 60,
|
|
45
|
+
quotaEnabled: false,
|
|
46
|
+
defaultReservationOutputTokens: 4096,
|
|
45
47
|
options: {}
|
|
46
48
|
}
|
|
47
49
|
});
|
|
@@ -61,6 +63,8 @@ const aiApiConfigResource = {
|
|
|
61
63
|
defaultLlmService: values.defaultLlmService ?? "",
|
|
62
64
|
enabledLlmServices: values.enabledLlmServices ?? [],
|
|
63
65
|
rateLimitPerMinute: values.rateLimitPerMinute ?? 60,
|
|
66
|
+
quotaEnabled: values.quotaEnabled ?? false,
|
|
67
|
+
defaultReservationOutputTokens: values.defaultReservationOutputTokens ?? 4096,
|
|
64
68
|
options: values.options ?? {}
|
|
65
69
|
}
|
|
66
70
|
});
|
|
@@ -71,6 +75,10 @@ const aiApiConfigResource = {
|
|
|
71
75
|
if (values.defaultLlmService !== void 0) updateData.defaultLlmService = values.defaultLlmService;
|
|
72
76
|
if (values.enabledLlmServices !== void 0) updateData.enabledLlmServices = values.enabledLlmServices;
|
|
73
77
|
if (values.rateLimitPerMinute !== void 0) updateData.rateLimitPerMinute = values.rateLimitPerMinute;
|
|
78
|
+
if (values.quotaEnabled !== void 0) updateData.quotaEnabled = values.quotaEnabled;
|
|
79
|
+
if (values.defaultReservationOutputTokens !== void 0) {
|
|
80
|
+
updateData.defaultReservationOutputTokens = values.defaultReservationOutputTokens;
|
|
81
|
+
}
|
|
74
82
|
if (values.options !== void 0) updateData.options = values.options;
|
|
75
83
|
await config.update(updateData);
|
|
76
84
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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_monitor_exports = {};
|
|
28
|
+
__export(ai_api_usage_monitor_exports, {
|
|
29
|
+
default: () => ai_api_usage_monitor_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_usage_monitor_exports);
|
|
32
|
+
var import_sequelize = require("sequelize");
|
|
33
|
+
function buildWhere(ctx) {
|
|
34
|
+
const params = ctx.action.params;
|
|
35
|
+
const where = {};
|
|
36
|
+
const start = typeof params.start === "string" ? new Date(params.start) : void 0;
|
|
37
|
+
const end = typeof params.end === "string" ? new Date(params.end) : void 0;
|
|
38
|
+
if (start && !Number.isNaN(start.getTime()) || end && !Number.isNaN(end.getTime())) {
|
|
39
|
+
const startedAt = {};
|
|
40
|
+
if (start && !Number.isNaN(start.getTime())) startedAt[import_sequelize.Op.gte] = start;
|
|
41
|
+
if (end && !Number.isNaN(end.getTime())) startedAt[import_sequelize.Op.lte] = end;
|
|
42
|
+
where.startedAt = startedAt;
|
|
43
|
+
}
|
|
44
|
+
if (params.userId !== void 0 && params.userId !== "") where.userId = params.userId;
|
|
45
|
+
if (params.resolvedService) where.resolvedService = params.resolvedService;
|
|
46
|
+
if (params.resolvedModel) where.resolvedModel = params.resolvedModel;
|
|
47
|
+
if (params.status) where.status = params.status;
|
|
48
|
+
return where;
|
|
49
|
+
}
|
|
50
|
+
const aiApiUsageMonitorResource = {
|
|
51
|
+
name: "aiApiUsageMonitor",
|
|
52
|
+
actions: {
|
|
53
|
+
async summary(ctx, next) {
|
|
54
|
+
const model = ctx.db.getCollection("aiApiUsageRecords").model;
|
|
55
|
+
const where = buildWhere(ctx);
|
|
56
|
+
const totals = await model.findOne({
|
|
57
|
+
attributes: [
|
|
58
|
+
[(0, import_sequelize.fn)("COUNT", (0, import_sequelize.col)("id")), "requestCount"],
|
|
59
|
+
[(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("inputTokens")), 0), "inputTokens"],
|
|
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"]
|
|
62
|
+
],
|
|
63
|
+
where,
|
|
64
|
+
raw: true
|
|
65
|
+
});
|
|
66
|
+
const costs = await model.findAll({
|
|
67
|
+
attributes: ["currency", [(0, import_sequelize.fn)("COALESCE", (0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("estimatedCost")), 0), "totalCost"]],
|
|
68
|
+
where: { ...where, estimatedCost: { [import_sequelize.Op.ne]: null } },
|
|
69
|
+
group: ["currency"],
|
|
70
|
+
raw: true
|
|
71
|
+
});
|
|
72
|
+
ctx.body = {
|
|
73
|
+
requestCount: Number((totals == null ? void 0 : totals.requestCount) ?? 0),
|
|
74
|
+
inputTokens: Number((totals == null ? void 0 : totals.inputTokens) ?? 0),
|
|
75
|
+
outputTokens: Number((totals == null ? void 0 : totals.outputTokens) ?? 0),
|
|
76
|
+
totalTokens: Number((totals == null ? void 0 : totals.totalTokens) ?? 0),
|
|
77
|
+
costsByCurrency: costs.map((item) => ({
|
|
78
|
+
currency: item.currency || "USD",
|
|
79
|
+
totalCost: String(item.totalCost ?? 0)
|
|
80
|
+
}))
|
|
81
|
+
};
|
|
82
|
+
await next();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var ai_api_usage_monitor_default = aiApiUsageMonitorResource;
|
|
@@ -36,6 +36,7 @@ var import_resolve_service = require("../utils/resolve-service");
|
|
|
36
36
|
var import_streaming = require("../utils/streaming");
|
|
37
37
|
var import_role_permission = require("../middleware/role-permission");
|
|
38
38
|
var import_usage = require("../usage");
|
|
39
|
+
var import_billing = require("../billing");
|
|
39
40
|
async function handleChatCompletions(ctx, plugin) {
|
|
40
41
|
var _a;
|
|
41
42
|
const body = ctx.request.body;
|
|
@@ -111,6 +112,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
111
112
|
ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
115
|
+
await (0, import_billing.prepareLlmBilling)(ctx, resolved);
|
|
114
116
|
const providerRequestParameters = getProviderRequestParameters(body);
|
|
115
117
|
const modelOptions = {
|
|
116
118
|
model: modelId,
|
|
@@ -173,6 +175,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
173
175
|
const baseModel = provider.createModel();
|
|
174
176
|
applyProviderRequestParameters(baseModel, providerRequestParameters);
|
|
175
177
|
const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice, providerRequestParameters);
|
|
178
|
+
(0, import_billing.markLlmProviderAttempted)(ctx);
|
|
176
179
|
if (stream) {
|
|
177
180
|
await handleStreamingCompletion(
|
|
178
181
|
ctx,
|
|
@@ -195,8 +198,15 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
195
198
|
} catch (err) {
|
|
196
199
|
ctx.log.error("AI API chat completions error:", err);
|
|
197
200
|
if (!ctx.res.headersSent) {
|
|
198
|
-
|
|
199
|
-
ctx.
|
|
201
|
+
const isQuotaError = err instanceof import_billing.AiApiQuotaError;
|
|
202
|
+
ctx.status = isQuotaError ? 429 : 500;
|
|
203
|
+
if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
|
|
204
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
205
|
+
ctx.status,
|
|
206
|
+
getErrorMessage(err, "Internal server error"),
|
|
207
|
+
isQuotaError ? "quota_error" : "server_error",
|
|
208
|
+
isQuotaError ? err.code : void 0
|
|
209
|
+
);
|
|
200
210
|
}
|
|
201
211
|
}
|
|
202
212
|
}
|
|
@@ -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_streaming = require("../utils/streaming");
|
|
35
35
|
var import_usage = require("../usage");
|
|
36
|
+
var import_billing = require("../billing");
|
|
36
37
|
async function handleCompletions(ctx, plugin) {
|
|
37
38
|
var _a;
|
|
38
39
|
const body = ctx.request.body;
|
|
@@ -108,6 +109,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
108
109
|
ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
112
|
+
await (0, import_billing.prepareLlmBilling)(ctx, resolved);
|
|
111
113
|
const modelOptions = {
|
|
112
114
|
model: modelId,
|
|
113
115
|
llmService: service.name
|
|
@@ -138,6 +140,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
138
140
|
langchainMessages.push(["human", prompt]);
|
|
139
141
|
const completionId = (0, import_openai_format.generateCompletionId)().replace("chatcmpl-", "cmpl-");
|
|
140
142
|
const chatModel = provider.createModel();
|
|
143
|
+
(0, import_billing.markLlmProviderAttempted)(ctx);
|
|
141
144
|
if (stream) {
|
|
142
145
|
await handleStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
|
|
143
146
|
} else {
|
|
@@ -146,8 +149,15 @@ async function handleCompletions(ctx, plugin) {
|
|
|
146
149
|
} catch (err) {
|
|
147
150
|
ctx.log.error("AI API completions error:", err);
|
|
148
151
|
if (!ctx.res.headersSent) {
|
|
149
|
-
|
|
150
|
-
ctx.
|
|
152
|
+
const isQuotaError = err instanceof import_billing.AiApiQuotaError;
|
|
153
|
+
ctx.status = isQuotaError ? 429 : 500;
|
|
154
|
+
if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
|
|
155
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
156
|
+
ctx.status,
|
|
157
|
+
getErrorMessage(err, "Internal server error"),
|
|
158
|
+
isQuotaError ? "quota_error" : "server_error",
|
|
159
|
+
isQuotaError ? err.code : void 0
|
|
160
|
+
);
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
}
|
|
@@ -51,6 +51,7 @@ var import_rate_limit = require("../middleware/rate-limit");
|
|
|
51
51
|
var import_role_permission = require("../middleware/role-permission");
|
|
52
52
|
var import_usage = require("../usage");
|
|
53
53
|
var import_streaming = require("../utils/streaming");
|
|
54
|
+
var import_billing = require("../billing");
|
|
54
55
|
const API_PREFIX = "/api/ai-llm/v1";
|
|
55
56
|
function createAiLlmRouter(plugin) {
|
|
56
57
|
const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
|
|
@@ -65,7 +66,10 @@ function createAiLlmRouter(plugin) {
|
|
|
65
66
|
ctx.set("Access-Control-Allow-Origin", "*");
|
|
66
67
|
ctx.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
67
68
|
ctx.set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-AI-Mode, X-Timezone, X-Locale");
|
|
68
|
-
ctx.set(
|
|
69
|
+
ctx.set(
|
|
70
|
+
"Access-Control-Expose-Headers",
|
|
71
|
+
"X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reason, Retry-After"
|
|
72
|
+
);
|
|
69
73
|
ctx.set("Access-Control-Max-Age", "86400");
|
|
70
74
|
if (method === "OPTIONS") {
|
|
71
75
|
ctx.status = 204;
|
|
@@ -211,6 +215,15 @@ function createAiLlmRouter(plugin) {
|
|
|
211
215
|
} catch (usageError) {
|
|
212
216
|
ctx.log.error("AI API usage record could not be finalized:", usageError);
|
|
213
217
|
}
|
|
218
|
+
} else if (ctx.state.aiApiLlmBilling) {
|
|
219
|
+
try {
|
|
220
|
+
const usageResult = ctx.state.aiApiUsageResult;
|
|
221
|
+
const providerUsage = (usageResult == null ? void 0 : usageResult.source) === "provider" ? usageResult.usage : void 0;
|
|
222
|
+
const succeeded = ctx.state.aiApiStreamResult ? ctx.state.aiApiStreamResult.succeeded : ctx.status >= 200 && ctx.status < 400;
|
|
223
|
+
await (0, import_billing.finalizeLlmBilling)(ctx, providerUsage, succeeded);
|
|
224
|
+
} catch (billingError) {
|
|
225
|
+
ctx.log.error("AI API quota reservation could not be finalized:", billingError);
|
|
226
|
+
}
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
229
|
};
|
package/dist/server/usage.js
CHANGED
|
@@ -34,6 +34,7 @@ __export(usage_exports, {
|
|
|
34
34
|
startUsageRecord: () => startUsageRecord
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(usage_exports);
|
|
37
|
+
var import_billing = require("./billing");
|
|
37
38
|
function getAiApiState(ctx) {
|
|
38
39
|
return ctx.state;
|
|
39
40
|
}
|
|
@@ -113,12 +114,15 @@ async function startUsageRecord(ctx, requestId, endpoint, model, streaming, mode
|
|
|
113
114
|
return record.id;
|
|
114
115
|
}
|
|
115
116
|
async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
116
|
-
var _a;
|
|
117
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
117
118
|
const response = ctx.body || {};
|
|
118
119
|
const state = getAiApiState(ctx);
|
|
119
120
|
const streamResult = state.aiApiStreamResult;
|
|
120
121
|
const usageResult = state.aiApiUsageResult ?? { source: "unavailable" };
|
|
121
|
-
const
|
|
122
|
+
const providerUsage = usageResult.source === "provider" ? usageResult.usage : void 0;
|
|
123
|
+
const succeeded = streamResult ? streamResult.succeeded : status === "succeeded";
|
|
124
|
+
const billing = await (0, import_billing.finalizeLlmBilling)(ctx, providerUsage, succeeded);
|
|
125
|
+
const usage = billing.usage ?? providerUsage;
|
|
122
126
|
const gatewayResponseId = usageResult.gatewayResponseId || response.id || (streamResult == null ? void 0 : streamResult.id);
|
|
123
127
|
const values = {
|
|
124
128
|
status: streamResult ? streamResult.succeeded ? "succeeded" : "failed" : status,
|
|
@@ -127,6 +131,17 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
|
127
131
|
inputTokens: (usage == null ? void 0 : usage.prompt_tokens) ?? null,
|
|
128
132
|
outputTokens: (usage == null ? void 0 : usage.completion_tokens) ?? null,
|
|
129
133
|
totalTokens: (usage == null ? void 0 : usage.total_tokens) ?? null,
|
|
134
|
+
resolvedService: ((_c = (_b = state.aiApiLlmBilling) == null ? void 0 : _b.resolution) == null ? void 0 : _c.service) ?? null,
|
|
135
|
+
resolvedProvider: ((_e = (_d = state.aiApiLlmBilling) == null ? void 0 : _d.resolution) == null ? void 0 : _e.provider) ?? null,
|
|
136
|
+
resolvedModel: ((_g = (_f = state.aiApiLlmBilling) == null ? void 0 : _f.resolution) == null ? void 0 : _g.model) ?? null,
|
|
137
|
+
estimatedCost: billing.estimatedCost ?? null,
|
|
138
|
+
currency: billing.currency ?? null,
|
|
139
|
+
costStatus: billing.costStatus ?? null,
|
|
140
|
+
modelPriceId: billing.modelPriceId ?? null,
|
|
141
|
+
quotaPolicyId: billing.quotaPolicyId ?? null,
|
|
142
|
+
inputPricePerMillionTokens: billing.inputPricePerMillionTokens ?? null,
|
|
143
|
+
outputPricePerMillionTokens: billing.outputPricePerMillionTokens ?? null,
|
|
144
|
+
fixedCostPerRequest: billing.fixedCostPerRequest ?? null,
|
|
130
145
|
providerRequestId: usageResult.providerRequestId ?? null,
|
|
131
146
|
completedAt: /* @__PURE__ */ new Date(),
|
|
132
147
|
durationMs: Date.now() - startedAt,
|
|
@@ -0,0 +1,102 @@
|
|
|
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 __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var validation_exports = {};
|
|
38
|
+
__export(validation_exports, {
|
|
39
|
+
validateModelPrice: () => validateModelPrice,
|
|
40
|
+
validateQuotaPolicy: () => validateQuotaPolicy
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(validation_exports);
|
|
43
|
+
var import_dayjs = __toESM(require("dayjs"));
|
|
44
|
+
function requireNonNegativeDecimal(value, field) {
|
|
45
|
+
const normalized = String(value ?? "").trim();
|
|
46
|
+
if (!/^\d+(?:\.\d+)?$/.test(normalized)) {
|
|
47
|
+
throw new Error(`${field} must be a non-negative decimal.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function requireNonNegativeIntegerOrNull(value, field) {
|
|
51
|
+
if (value === null || value === void 0 || value === "") return;
|
|
52
|
+
const parsed = Number(value);
|
|
53
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error(`${field} must be a non-negative integer.`);
|
|
54
|
+
}
|
|
55
|
+
async function validateModelPrice(db, model) {
|
|
56
|
+
requireNonNegativeDecimal(model.get("inputPricePerMillionTokens"), "inputPricePerMillionTokens");
|
|
57
|
+
requireNonNegativeDecimal(model.get("outputPricePerMillionTokens"), "outputPricePerMillionTokens");
|
|
58
|
+
requireNonNegativeDecimal(model.get("fixedCostPerRequest") ?? 0, "fixedCostPerRequest");
|
|
59
|
+
const effectiveFrom = new Date(String(model.get("effectiveFrom")));
|
|
60
|
+
const effectiveToValue = model.get("effectiveTo");
|
|
61
|
+
const effectiveTo = effectiveToValue ? new Date(String(effectiveToValue)) : void 0;
|
|
62
|
+
if (Number.isNaN(effectiveFrom.getTime())) throw new Error("effectiveFrom must be a valid date.");
|
|
63
|
+
if (effectiveTo && (Number.isNaN(effectiveTo.getTime()) || effectiveTo <= effectiveFrom)) {
|
|
64
|
+
throw new Error("effectiveTo must be later than effectiveFrom.");
|
|
65
|
+
}
|
|
66
|
+
if (model.get("enabled") === false) return;
|
|
67
|
+
const overlapFilter = {
|
|
68
|
+
llmService: model.get("llmService"),
|
|
69
|
+
model: model.get("model"),
|
|
70
|
+
enabled: true,
|
|
71
|
+
effectiveFrom: { $lt: effectiveTo ?? /* @__PURE__ */ new Date("9999-12-31T23:59:59.999Z") },
|
|
72
|
+
$or: [{ effectiveTo: null }, { effectiveTo: { $gt: effectiveFrom } }]
|
|
73
|
+
};
|
|
74
|
+
if (model.get("id")) overlapFilter.id = { $ne: model.get("id") };
|
|
75
|
+
const overlap = await db.getRepository("aiApiModelPrices").findOne({
|
|
76
|
+
filter: overlapFilter
|
|
77
|
+
});
|
|
78
|
+
if (overlap) throw new Error("An enabled price already overlaps this effective period.");
|
|
79
|
+
}
|
|
80
|
+
function validateQuotaPolicy(model) {
|
|
81
|
+
if (!["daily", "monthly"].includes(String(model.get("periodType")))) {
|
|
82
|
+
throw new Error("periodType must be daily or monthly.");
|
|
83
|
+
}
|
|
84
|
+
if (!["allow", "use_reserved"].includes(String(model.get("missingUsageBehavior")))) {
|
|
85
|
+
throw new Error("missingUsageBehavior must be allow or use_reserved.");
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
(0, import_dayjs.default)().tz(String(model.get("timezone") || "UTC"));
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error("timezone must be a valid IANA timezone.");
|
|
91
|
+
}
|
|
92
|
+
requireNonNegativeIntegerOrNull(model.get("requestLimit"), "requestLimit");
|
|
93
|
+
requireNonNegativeIntegerOrNull(model.get("totalTokenLimit"), "totalTokenLimit");
|
|
94
|
+
if (model.get("costLimit") !== null && model.get("costLimit") !== void 0) {
|
|
95
|
+
requireNonNegativeDecimal(model.get("costLimit"), "costLimit");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
99
|
+
0 && (module.exports = {
|
|
100
|
+
validateModelPrice,
|
|
101
|
+
validateQuotaPolicy
|
|
102
|
+
});
|
package/package.json
CHANGED
package/src/client/plugin.tsx
CHANGED
|
@@ -1,48 +1,73 @@
|
|
|
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
|
-
import { Plugin, lazy } from '@nocobase/client';
|
|
11
|
-
import PluginACLClient from '@nocobase/plugin-acl/client';
|
|
12
|
-
import React from 'react';
|
|
13
|
-
|
|
14
|
-
const AiApiConfigPage = React.lazy(() => import('
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
import { Plugin, lazy } from '@nocobase/client';
|
|
11
|
+
import PluginACLClient from '@nocobase/plugin-acl/client';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
const AiApiConfigPage = React.lazy(() => import('../client-v2/pages/GeneralPage'));
|
|
15
|
+
const AiApiModelPricingPage = React.lazy(() => import('../client-v2/pages/ModelPricingPage'));
|
|
16
|
+
const AiApiUserQuotasPage = React.lazy(() => import('../client-v2/pages/UserQuotasPage'));
|
|
17
|
+
const AiApiUsagePage = React.lazy(() => import('../client-v2/pages/UsagePage'));
|
|
18
|
+
const { AiApiRolePermissions } = lazy(() => import('./components/AiApiRolePermissions'), 'AiApiRolePermissions');
|
|
19
|
+
|
|
20
|
+
export class PluginAiApiClient extends Plugin {
|
|
21
|
+
async load() {
|
|
22
|
+
this.app.pluginSettingsManager.add('ai-api', {
|
|
23
|
+
icon: 'ApiOutlined',
|
|
24
|
+
title: this.t('AI API Gateway'),
|
|
25
|
+
aclSnippet: 'pm.ai-api.configuration',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.app.pluginSettingsManager.add('ai-api.config', {
|
|
29
|
+
title: this.t('Configuration'),
|
|
30
|
+
Component: AiApiConfigPage,
|
|
31
|
+
aclSnippet: 'pm.ai-api.configuration',
|
|
32
|
+
sort: 1,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.app.pluginSettingsManager.add('ai-api.model-pricing', {
|
|
36
|
+
title: this.t('Model pricing'),
|
|
37
|
+
Component: AiApiModelPricingPage,
|
|
38
|
+
aclSnippet: 'pm.ai-api.configuration',
|
|
39
|
+
sort: 2,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.app.pluginSettingsManager.add('ai-api.user-quotas', {
|
|
43
|
+
title: this.t('User quotas'),
|
|
44
|
+
Component: AiApiUserQuotasPage,
|
|
45
|
+
aclSnippet: 'pm.ai-api.configuration',
|
|
46
|
+
sort: 3,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.app.pluginSettingsManager.add('ai-api.usage', {
|
|
50
|
+
title: this.t('Usage'),
|
|
51
|
+
Component: AiApiUsagePage,
|
|
52
|
+
aclSnippet: 'pm.ai-api.configuration',
|
|
53
|
+
sort: 4,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Add "AI API" tab in Settings → Users & Permissions → [Role]
|
|
57
|
+
const aclPlugin = this.app.pm.get(PluginACLClient);
|
|
58
|
+
if (aclPlugin?.settingsUI) {
|
|
59
|
+
aclPlugin.settingsUI.addPermissionsTab(({ t, TabLayout, activeRole }) => ({
|
|
60
|
+
key: 'aiApi',
|
|
61
|
+
label: 'AI API',
|
|
62
|
+
sort: 25,
|
|
63
|
+
children: (
|
|
64
|
+
<TabLayout>
|
|
65
|
+
<AiApiRolePermissions role={activeRole} />
|
|
66
|
+
</TabLayout>
|
|
67
|
+
),
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default PluginAiApiClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { tExpr, useT } from '../client/locale';
|