plugin-ai-api 1.0.14 → 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/locale/zh-CN.json
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
|
-
{
|
|
2
|
-
"AI API Gateway": "AI API 网关",
|
|
3
|
-
"Configuration": "配置",
|
|
4
|
-
"Default AI Employee": "默认 AI 员工",
|
|
5
|
-
"Enabled LLM Services": "已启用 LLM 服务",
|
|
6
|
-
"Rate Limit": "速率限制",
|
|
7
|
-
"Save Configuration": "保存配置",
|
|
8
|
-
"Configuration saved": "配置已保存",
|
|
9
|
-
"Failed to save configuration": "保存配置失败"
|
|
10
|
-
|
|
1
|
+
{
|
|
2
|
+
"AI API Gateway": "AI API 网关",
|
|
3
|
+
"Configuration": "配置",
|
|
4
|
+
"Default AI Employee": "默认 AI 员工",
|
|
5
|
+
"Enabled LLM Services": "已启用 LLM 服务",
|
|
6
|
+
"Rate Limit": "速率限制",
|
|
7
|
+
"Save Configuration": "保存配置",
|
|
8
|
+
"Configuration saved": "配置已保存",
|
|
9
|
+
"Failed to save configuration": "保存配置失败",
|
|
10
|
+
"API mode": "API 模式",
|
|
11
|
+
"Direct LLM": "直接 LLM",
|
|
12
|
+
"AI Employee agent": "AI 员工代理",
|
|
13
|
+
"Default LLM service": "默认 LLM 服务",
|
|
14
|
+
"Enable user quotas": "启用用户配额",
|
|
15
|
+
"Default reserved output tokens": "默认预留输出令牌数",
|
|
16
|
+
"Refresh": "刷新",
|
|
17
|
+
"Model pricing": "模型定价",
|
|
18
|
+
"User quotas": "用户配额",
|
|
19
|
+
"Usage": "用量",
|
|
20
|
+
"LLM service": "LLM 服务",
|
|
21
|
+
"Model": "模型",
|
|
22
|
+
"Input price / 1M": "输入价格 / 百万令牌",
|
|
23
|
+
"Output price / 1M": "输出价格 / 百万令牌",
|
|
24
|
+
"Fixed request cost": "每次请求固定费用",
|
|
25
|
+
"Currency": "货币",
|
|
26
|
+
"Status": "状态",
|
|
27
|
+
"Enabled": "已启用",
|
|
28
|
+
"Disabled": "已禁用",
|
|
29
|
+
"Actions": "操作",
|
|
30
|
+
"Edit": "编辑",
|
|
31
|
+
"Delete": "删除",
|
|
32
|
+
"Delete this price?": "删除此价格?",
|
|
33
|
+
"Add price": "添加价格",
|
|
34
|
+
"Edit price": "编辑价格",
|
|
35
|
+
"Effective from": "生效时间",
|
|
36
|
+
"Effective to": "失效时间",
|
|
37
|
+
"Notes": "备注",
|
|
38
|
+
"Saved successfully": "保存成功",
|
|
39
|
+
"Deleted successfully": "删除成功",
|
|
40
|
+
"User": "用户",
|
|
41
|
+
"Period": "周期",
|
|
42
|
+
"Request limit": "请求限制",
|
|
43
|
+
"Token limit": "令牌限制",
|
|
44
|
+
"Cost limit": "费用限制",
|
|
45
|
+
"Timezone": "时区",
|
|
46
|
+
"Unlimited": "无限制",
|
|
47
|
+
"Add quota": "添加配额",
|
|
48
|
+
"Edit quota": "编辑配额",
|
|
49
|
+
"Delete this quota?": "删除此配额?",
|
|
50
|
+
"Daily": "每日",
|
|
51
|
+
"Monthly": "每月",
|
|
52
|
+
"Reject unpriced models": "拒绝未定价模型",
|
|
53
|
+
"Missing usage behavior": "缺少用量时的行为",
|
|
54
|
+
"Use reserved estimate": "使用预留估算",
|
|
55
|
+
"Allow without token charge": "允许且不计令牌",
|
|
56
|
+
"Started at": "开始时间",
|
|
57
|
+
"Requested model": "请求模型",
|
|
58
|
+
"Resolved service": "解析后的服务",
|
|
59
|
+
"Resolved model": "解析后的模型",
|
|
60
|
+
"Input tokens": "输入令牌",
|
|
61
|
+
"Output tokens": "输出令牌",
|
|
62
|
+
"Total tokens": "总令牌",
|
|
63
|
+
"Cost": "费用",
|
|
64
|
+
"Cost status": "费用状态",
|
|
65
|
+
"Request ID": "请求 ID",
|
|
66
|
+
"Failed to load models": "加载模型失败",
|
|
67
|
+
"Select a model": "选择模型",
|
|
68
|
+
"Select an AI Employee": "选择 AI 员工",
|
|
69
|
+
"Usage guide": "使用指南",
|
|
70
|
+
"OpenAI-compatible endpoint": "OpenAI 兼容端点",
|
|
71
|
+
"Base URL": "基础 URL",
|
|
72
|
+
"Use a NocoBase API key as the Bearer token.": "使用 NocoBase API 密钥作为 Bearer Token。",
|
|
73
|
+
"List available models": "列出可用模型",
|
|
74
|
+
"Send a chat completion": "发送聊天补全请求",
|
|
75
|
+
"Usage filters": "用量筛选",
|
|
76
|
+
"Time range": "时间范围",
|
|
77
|
+
"User ID": "用户 ID",
|
|
78
|
+
"Succeeded": "成功",
|
|
79
|
+
"Failed": "失败",
|
|
80
|
+
"Started": "已开始",
|
|
81
|
+
"Apply filters": "应用筛选",
|
|
82
|
+
"Reset": "重置",
|
|
83
|
+
"Requests": "请求数",
|
|
84
|
+
"Total cost": "总费用",
|
|
85
|
+
"Usage records": "用量记录"
|
|
86
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
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 billing_exports = {};
|
|
38
|
+
__export(billing_exports, {
|
|
39
|
+
AiApiQuotaError: () => AiApiQuotaError,
|
|
40
|
+
calculateLlmCost: () => calculateLlmCost,
|
|
41
|
+
finalizeLlmBilling: () => finalizeLlmBilling,
|
|
42
|
+
markLlmProviderAttempted: () => markLlmProviderAttempted,
|
|
43
|
+
prepareLlmBilling: () => prepareLlmBilling
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(billing_exports);
|
|
46
|
+
var import_dayjs = __toESM(require("dayjs"));
|
|
47
|
+
var import_utc = __toESM(require("dayjs/plugin/utc"));
|
|
48
|
+
var import_timezone = __toESM(require("dayjs/plugin/timezone"));
|
|
49
|
+
import_dayjs.default.extend(import_utc.default);
|
|
50
|
+
import_dayjs.default.extend(import_timezone.default);
|
|
51
|
+
const PRICE_SCALE = 10;
|
|
52
|
+
const COST_SCALE = 8;
|
|
53
|
+
const PRICE_TO_COST_DIVISOR = 100000000n;
|
|
54
|
+
class AiApiQuotaError extends Error {
|
|
55
|
+
constructor(code, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.name = "AiApiQuotaError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function stateOf(ctx) {
|
|
62
|
+
return ctx.state;
|
|
63
|
+
}
|
|
64
|
+
function valueOf(model, name) {
|
|
65
|
+
if (!model) return void 0;
|
|
66
|
+
if (typeof model.get === "function") return model.get(name);
|
|
67
|
+
return model[name];
|
|
68
|
+
}
|
|
69
|
+
function decimalString(value, scale) {
|
|
70
|
+
const source = String(value ?? "0").trim();
|
|
71
|
+
const match = source.match(/^(-?)(\d+)(?:\.(\d+))?$/);
|
|
72
|
+
if (!match) throw new Error(`Invalid decimal value: ${source}`);
|
|
73
|
+
const fraction = (match[3] ?? "").padEnd(scale, "0").slice(0, scale);
|
|
74
|
+
return `${match[1]}${match[2]}.${fraction}`;
|
|
75
|
+
}
|
|
76
|
+
function decimalUnits(value, scale) {
|
|
77
|
+
return BigInt(decimalString(value, scale).replace(".", ""));
|
|
78
|
+
}
|
|
79
|
+
function formatUnits(value, scale) {
|
|
80
|
+
const sign = value < 0n ? "-" : "";
|
|
81
|
+
const digits = (value < 0n ? -value : value).toString().padStart(scale + 1, "0");
|
|
82
|
+
if (scale === 0) return `${sign}${digits}`;
|
|
83
|
+
return `${sign}${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
|
|
84
|
+
}
|
|
85
|
+
function divideRounded(value, divisor) {
|
|
86
|
+
return (value + divisor / 2n) / divisor;
|
|
87
|
+
}
|
|
88
|
+
function calculateCostUnits(inputTokens, outputTokens, price) {
|
|
89
|
+
const input = divideRounded(
|
|
90
|
+
BigInt(inputTokens) * decimalUnits(price.inputPricePerMillionTokens, PRICE_SCALE),
|
|
91
|
+
PRICE_TO_COST_DIVISOR
|
|
92
|
+
);
|
|
93
|
+
const output = divideRounded(
|
|
94
|
+
BigInt(outputTokens) * decimalUnits(price.outputPricePerMillionTokens, PRICE_SCALE),
|
|
95
|
+
PRICE_TO_COST_DIVISOR
|
|
96
|
+
);
|
|
97
|
+
const fixed = divideRounded(decimalUnits(price.fixedCostPerRequest, PRICE_SCALE), 100n);
|
|
98
|
+
return input + output + fixed;
|
|
99
|
+
}
|
|
100
|
+
function calculateLlmCost(inputTokens, outputTokens, price) {
|
|
101
|
+
return formatUnits(calculateCostUnits(inputTokens, outputTokens, price), COST_SCALE);
|
|
102
|
+
}
|
|
103
|
+
function normalizePositiveInteger(value, fallback) {
|
|
104
|
+
const parsed = Number(value);
|
|
105
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
106
|
+
}
|
|
107
|
+
function estimateInputTokens(ctx) {
|
|
108
|
+
const body = ctx.request.body ?? {};
|
|
109
|
+
const input = body.messages ?? body.prompt ?? "";
|
|
110
|
+
return Math.max(1, Math.ceil(JSON.stringify(input).length / 4));
|
|
111
|
+
}
|
|
112
|
+
function getPeriodBounds(periodType, timezone2) {
|
|
113
|
+
const unit = periodType === "daily" ? "day" : "month";
|
|
114
|
+
try {
|
|
115
|
+
const start = (0, import_dayjs.default)().tz(timezone2 || "UTC").startOf(unit);
|
|
116
|
+
return { start: start.utc().toDate(), end: start.add(1, unit).utc().toDate() };
|
|
117
|
+
} catch {
|
|
118
|
+
const start = (0, import_dayjs.default)().utc().startOf(unit);
|
|
119
|
+
return { start: start.toDate(), end: start.add(1, unit).toDate() };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function exceedsIntegerLimit(current, added, limit) {
|
|
123
|
+
if (limit === null || limit === void 0 || limit === "") return false;
|
|
124
|
+
return current + added > BigInt(String(limit));
|
|
125
|
+
}
|
|
126
|
+
function exceedsDecimalLimit(current, added, limit) {
|
|
127
|
+
if (limit === null || limit === void 0 || limit === "") return false;
|
|
128
|
+
return current + added > decimalUnits(limit, COST_SCALE);
|
|
129
|
+
}
|
|
130
|
+
async function findPrice(ctx, service, modelId) {
|
|
131
|
+
const now = /* @__PURE__ */ new Date();
|
|
132
|
+
const price = await ctx.db.getRepository("aiApiModelPrices").findOne({
|
|
133
|
+
filter: {
|
|
134
|
+
llmService: valueOf(service, "name"),
|
|
135
|
+
model: modelId,
|
|
136
|
+
enabled: true,
|
|
137
|
+
effectiveFrom: { $lte: now },
|
|
138
|
+
$or: [{ effectiveTo: null }, { effectiveTo: { $gt: now } }]
|
|
139
|
+
},
|
|
140
|
+
sort: "-effectiveFrom"
|
|
141
|
+
});
|
|
142
|
+
if (!price) return void 0;
|
|
143
|
+
return {
|
|
144
|
+
id: valueOf(price, "id"),
|
|
145
|
+
currency: valueOf(price, "currency"),
|
|
146
|
+
inputPricePerMillionTokens: decimalString(valueOf(price, "inputPricePerMillionTokens"), PRICE_SCALE),
|
|
147
|
+
outputPricePerMillionTokens: decimalString(valueOf(price, "outputPricePerMillionTokens"), PRICE_SCALE),
|
|
148
|
+
fixedCostPerRequest: decimalString(valueOf(price, "fixedCostPerRequest"), PRICE_SCALE)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async function prepareLlmBilling(ctx, resolved) {
|
|
152
|
+
var _a;
|
|
153
|
+
const userId = (_a = stateOf(ctx).currentUser) == null ? void 0 : _a.id;
|
|
154
|
+
const serviceName = valueOf(resolved.service, "name");
|
|
155
|
+
const provider = valueOf(resolved.service, "provider");
|
|
156
|
+
const price = await findPrice(ctx, resolved.service, resolved.modelId);
|
|
157
|
+
const billing = {
|
|
158
|
+
resolution: { service: serviceName, provider, model: resolved.modelId },
|
|
159
|
+
price
|
|
160
|
+
};
|
|
161
|
+
stateOf(ctx).aiApiLlmBilling = billing;
|
|
162
|
+
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
163
|
+
if (!valueOf(config, "quotaEnabled") || userId === void 0 || userId === null) return;
|
|
164
|
+
const policy = await ctx.db.getRepository("aiApiUserQuotaPolicies").findOne({
|
|
165
|
+
filter: { userId, enabled: true },
|
|
166
|
+
sort: "-updatedAt"
|
|
167
|
+
});
|
|
168
|
+
if (!policy) return;
|
|
169
|
+
const rejectUnpriced = valueOf(policy, "rejectUnpricedModel");
|
|
170
|
+
if (!price && rejectUnpriced) {
|
|
171
|
+
throw new AiApiQuotaError(
|
|
172
|
+
"model_price_not_configured",
|
|
173
|
+
`Pricing is not configured for '${serviceName}/${resolved.modelId}'.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const policyCurrency = valueOf(policy, "currency");
|
|
177
|
+
if (price && policyCurrency !== price.currency) {
|
|
178
|
+
throw new AiApiQuotaError(
|
|
179
|
+
"quota_currency_mismatch",
|
|
180
|
+
`Quota currency '${policyCurrency}' does not match model price currency '${price.currency}'.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const body = ctx.request.body ?? {};
|
|
184
|
+
const estimatedInputTokens = estimateInputTokens(ctx);
|
|
185
|
+
const defaultOutput = normalizePositiveInteger(valueOf(config, "defaultReservationOutputTokens"), 4096);
|
|
186
|
+
const estimatedOutputTokens = normalizePositiveInteger(body.max_completion_tokens ?? body.max_tokens, defaultOutput);
|
|
187
|
+
const reservedTokens = estimatedInputTokens + estimatedOutputTokens;
|
|
188
|
+
const reservedCost = price ? calculateLlmCost(estimatedInputTokens, estimatedOutputTokens, price) : "0.00000000";
|
|
189
|
+
const period = getPeriodBounds(valueOf(policy, "periodType"), valueOf(policy, "timezone"));
|
|
190
|
+
const Bucket = ctx.db.getModel("aiApiUserQuotaBuckets");
|
|
191
|
+
const reservation = await ctx.db.sequelize.transaction(async (transaction) => {
|
|
192
|
+
const [bucket] = await Bucket.findOrCreate({
|
|
193
|
+
where: { policyId: valueOf(policy, "id"), periodStart: period.start },
|
|
194
|
+
defaults: {
|
|
195
|
+
userId,
|
|
196
|
+
periodEnd: period.end,
|
|
197
|
+
requestCount: 0,
|
|
198
|
+
totalTokens: 0,
|
|
199
|
+
cost: "0.00000000",
|
|
200
|
+
reservedRequests: 0,
|
|
201
|
+
reservedTokens: 0,
|
|
202
|
+
reservedCost: "0.00000000"
|
|
203
|
+
},
|
|
204
|
+
transaction
|
|
205
|
+
});
|
|
206
|
+
await bucket.reload({ transaction, lock: transaction.LOCK.UPDATE });
|
|
207
|
+
const requestCount = BigInt(String(bucket.get("requestCount") ?? 0));
|
|
208
|
+
const reservedRequests = BigInt(String(bucket.get("reservedRequests") ?? 0));
|
|
209
|
+
if (exceedsIntegerLimit(requestCount + reservedRequests, 1n, valueOf(policy, "requestLimit"))) {
|
|
210
|
+
throw new AiApiQuotaError("request_quota_exceeded", "The request quota for this user has been exceeded.");
|
|
211
|
+
}
|
|
212
|
+
const totalTokens = BigInt(String(bucket.get("totalTokens") ?? 0));
|
|
213
|
+
const alreadyReservedTokens = BigInt(String(bucket.get("reservedTokens") ?? 0));
|
|
214
|
+
if (exceedsIntegerLimit(
|
|
215
|
+
totalTokens + alreadyReservedTokens,
|
|
216
|
+
BigInt(reservedTokens),
|
|
217
|
+
valueOf(policy, "totalTokenLimit")
|
|
218
|
+
)) {
|
|
219
|
+
throw new AiApiQuotaError("token_quota_exceeded", "The token quota for this user has been exceeded.");
|
|
220
|
+
}
|
|
221
|
+
const cost = decimalUnits(bucket.get("cost"), COST_SCALE);
|
|
222
|
+
const alreadyReservedCost = decimalUnits(bucket.get("reservedCost"), COST_SCALE);
|
|
223
|
+
if (exceedsDecimalLimit(
|
|
224
|
+
cost + alreadyReservedCost,
|
|
225
|
+
decimalUnits(reservedCost, COST_SCALE),
|
|
226
|
+
valueOf(policy, "costLimit")
|
|
227
|
+
)) {
|
|
228
|
+
throw new AiApiQuotaError("cost_quota_exceeded", "The cost quota for this user has been exceeded.");
|
|
229
|
+
}
|
|
230
|
+
await bucket.update(
|
|
231
|
+
{
|
|
232
|
+
reservedRequests: formatUnits(reservedRequests + 1n, 0),
|
|
233
|
+
reservedTokens: formatUnits(alreadyReservedTokens + BigInt(reservedTokens), 0),
|
|
234
|
+
reservedCost: formatUnits(alreadyReservedCost + decimalUnits(reservedCost, COST_SCALE), COST_SCALE)
|
|
235
|
+
},
|
|
236
|
+
{ transaction }
|
|
237
|
+
);
|
|
238
|
+
return {
|
|
239
|
+
bucketId: bucket.get("id"),
|
|
240
|
+
policyId: valueOf(policy, "id"),
|
|
241
|
+
estimatedInputTokens,
|
|
242
|
+
estimatedOutputTokens,
|
|
243
|
+
reservedTokens,
|
|
244
|
+
reservedCost,
|
|
245
|
+
missingUsageBehavior: valueOf(policy, "missingUsageBehavior") === "allow" ? "allow" : "use_reserved"
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
billing.reservation = reservation;
|
|
249
|
+
}
|
|
250
|
+
function markLlmProviderAttempted(ctx) {
|
|
251
|
+
const state = stateOf(ctx);
|
|
252
|
+
state.aiApiLlmBilling = { ...state.aiApiLlmBilling ?? {}, providerAttempted: true };
|
|
253
|
+
}
|
|
254
|
+
function usageNumbers(usage) {
|
|
255
|
+
if (!usage || usage.prompt_tokens === null || usage.completion_tokens === null) return void 0;
|
|
256
|
+
return {
|
|
257
|
+
input: usage.prompt_tokens,
|
|
258
|
+
output: usage.completion_tokens,
|
|
259
|
+
total: usage.total_tokens ?? usage.prompt_tokens + usage.completion_tokens
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function finalizeLlmBilling(ctx, providerUsage, succeeded) {
|
|
263
|
+
var _a, _b, _c, _d, _e, _f;
|
|
264
|
+
const billing = stateOf(ctx).aiApiLlmBilling;
|
|
265
|
+
if (!billing) return {};
|
|
266
|
+
let numbers = usageNumbers(providerUsage);
|
|
267
|
+
let costStatus;
|
|
268
|
+
if (numbers) {
|
|
269
|
+
costStatus = billing.price ? "calculated" : "unpriced";
|
|
270
|
+
} else if (succeeded && ((_a = billing.reservation) == null ? void 0 : _a.missingUsageBehavior) === "use_reserved") {
|
|
271
|
+
numbers = {
|
|
272
|
+
input: billing.reservation.estimatedInputTokens,
|
|
273
|
+
output: billing.reservation.estimatedOutputTokens,
|
|
274
|
+
total: billing.reservation.reservedTokens
|
|
275
|
+
};
|
|
276
|
+
costStatus = billing.price ? "estimated" : "unpriced";
|
|
277
|
+
} else {
|
|
278
|
+
costStatus = billing.price ? "usage_unavailable" : "unpriced";
|
|
279
|
+
}
|
|
280
|
+
const cost = numbers && billing.price ? calculateLlmCost(numbers.input, numbers.output, billing.price) : void 0;
|
|
281
|
+
const reservation = billing.reservation;
|
|
282
|
+
if (reservation) {
|
|
283
|
+
const Bucket = ctx.db.getModel("aiApiUserQuotaBuckets");
|
|
284
|
+
await ctx.db.sequelize.transaction(async (transaction) => {
|
|
285
|
+
const bucket = await Bucket.findByPk(reservation.bucketId, { transaction, lock: transaction.LOCK.UPDATE });
|
|
286
|
+
if (!bucket) return;
|
|
287
|
+
const reservedRequests = BigInt(String(bucket.get("reservedRequests") ?? 0));
|
|
288
|
+
const reservedTokens = BigInt(String(bucket.get("reservedTokens") ?? 0));
|
|
289
|
+
const reservedCost = decimalUnits(bucket.get("reservedCost"), COST_SCALE);
|
|
290
|
+
const requestCount = BigInt(String(bucket.get("requestCount") ?? 0));
|
|
291
|
+
const totalTokens = BigInt(String(bucket.get("totalTokens") ?? 0));
|
|
292
|
+
const currentCost = decimalUnits(bucket.get("cost"), COST_SCALE);
|
|
293
|
+
await bucket.update(
|
|
294
|
+
{
|
|
295
|
+
reservedRequests: formatUnits(reservedRequests > 0n ? reservedRequests - 1n : 0n, 0),
|
|
296
|
+
reservedTokens: formatUnits(
|
|
297
|
+
reservedTokens >= BigInt(reservation.reservedTokens) ? reservedTokens - BigInt(reservation.reservedTokens) : 0n,
|
|
298
|
+
0
|
|
299
|
+
),
|
|
300
|
+
reservedCost: formatUnits(
|
|
301
|
+
reservedCost >= decimalUnits(reservation.reservedCost, COST_SCALE) ? reservedCost - decimalUnits(reservation.reservedCost, COST_SCALE) : 0n,
|
|
302
|
+
COST_SCALE
|
|
303
|
+
),
|
|
304
|
+
requestCount: formatUnits(requestCount + (billing.providerAttempted ? 1n : 0n), 0),
|
|
305
|
+
totalTokens: formatUnits(totalTokens + BigInt((numbers == null ? void 0 : numbers.total) ?? 0), 0),
|
|
306
|
+
cost: formatUnits(currentCost + decimalUnits(cost ?? "0", COST_SCALE), COST_SCALE)
|
|
307
|
+
},
|
|
308
|
+
{ transaction }
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
usage: numbers ? { prompt_tokens: numbers.input, completion_tokens: numbers.output, total_tokens: numbers.total } : providerUsage,
|
|
314
|
+
estimatedCost: cost,
|
|
315
|
+
currency: (_b = billing.price) == null ? void 0 : _b.currency,
|
|
316
|
+
costStatus,
|
|
317
|
+
modelPriceId: (_c = billing.price) == null ? void 0 : _c.id,
|
|
318
|
+
quotaPolicyId: reservation == null ? void 0 : reservation.policyId,
|
|
319
|
+
inputPricePerMillionTokens: (_d = billing.price) == null ? void 0 : _d.inputPricePerMillionTokens,
|
|
320
|
+
outputPricePerMillionTokens: (_e = billing.price) == null ? void 0 : _e.outputPricePerMillionTokens,
|
|
321
|
+
fixedCostPerRequest: (_f = billing.price) == null ? void 0 : _f.fixedCostPerRequest
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
325
|
+
0 && (module.exports = {
|
|
326
|
+
AiApiQuotaError,
|
|
327
|
+
calculateLlmCost,
|
|
328
|
+
finalizeLlmBilling,
|
|
329
|
+
markLlmProviderAttempted,
|
|
330
|
+
prepareLlmBilling
|
|
331
|
+
});
|
|
@@ -62,6 +62,18 @@ var ai_api_config_default = (0, import_database.defineCollection)({
|
|
|
62
62
|
defaultValue: 60,
|
|
63
63
|
comment: "Max requests per user per minute"
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: "quotaEnabled",
|
|
67
|
+
type: "boolean",
|
|
68
|
+
defaultValue: false,
|
|
69
|
+
comment: "Enable per-user request, token, and cost quotas for direct LLM mode"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "defaultReservationOutputTokens",
|
|
73
|
+
type: "integer",
|
|
74
|
+
defaultValue: 4096,
|
|
75
|
+
comment: "Output tokens reserved when a request does not specify a maximum"
|
|
76
|
+
},
|
|
65
77
|
{
|
|
66
78
|
name: "options",
|
|
67
79
|
type: "jsonb",
|
|
@@ -0,0 +1,55 @@
|
|
|
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_model_prices_exports = {};
|
|
28
|
+
__export(ai_api_model_prices_exports, {
|
|
29
|
+
default: () => ai_api_model_prices_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_model_prices_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var ai_api_model_prices_default = (0, import_database.defineCollection)({
|
|
34
|
+
name: "aiApiModelPrices",
|
|
35
|
+
autoGenId: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: "llmService", type: "string", allowNull: false, index: true },
|
|
38
|
+
{ name: "provider", type: "string", allowNull: false, index: true },
|
|
39
|
+
{ name: "model", type: "string", allowNull: false, index: true },
|
|
40
|
+
{ name: "currency", type: "string", allowNull: false, defaultValue: "USD" },
|
|
41
|
+
{ name: "inputPricePerMillionTokens", type: "decimal", precision: 20, scale: 10, allowNull: false },
|
|
42
|
+
{ name: "outputPricePerMillionTokens", type: "decimal", precision: 20, scale: 10, allowNull: false },
|
|
43
|
+
{ name: "fixedCostPerRequest", type: "decimal", precision: 20, scale: 10, allowNull: false, defaultValue: 0 },
|
|
44
|
+
{ name: "effectiveFrom", type: "datetimeTz", allowNull: false, index: true },
|
|
45
|
+
{ name: "effectiveTo", type: "datetimeTz", allowNull: true, index: true },
|
|
46
|
+
{ name: "enabled", type: "boolean", defaultValue: true, index: true },
|
|
47
|
+
{ name: "notes", type: "text", allowNull: true }
|
|
48
|
+
],
|
|
49
|
+
indexes: [
|
|
50
|
+
{
|
|
51
|
+
fields: ["llmService", "model", "effectiveFrom"],
|
|
52
|
+
unique: true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
});
|
|
@@ -52,6 +52,9 @@ var ai_api_usage_records_default = (0, import_database.defineCollection)({
|
|
|
52
52
|
{ name: "endpoint", type: "string" },
|
|
53
53
|
{ name: "mode", type: "string", allowNull: true },
|
|
54
54
|
{ name: "model", type: "string", allowNull: true, index: true },
|
|
55
|
+
{ name: "resolvedService", type: "string", allowNull: true, index: true },
|
|
56
|
+
{ name: "resolvedProvider", type: "string", allowNull: true, index: true },
|
|
57
|
+
{ name: "resolvedModel", type: "string", allowNull: true, index: true },
|
|
55
58
|
{ name: "status", type: "string", index: true },
|
|
56
59
|
{ name: "httpStatus", type: "integer", allowNull: true },
|
|
57
60
|
{ name: "errorCode", type: "string", allowNull: true },
|
|
@@ -61,6 +64,12 @@ var ai_api_usage_records_default = (0, import_database.defineCollection)({
|
|
|
61
64
|
{ name: "totalTokens", type: "integer", allowNull: true },
|
|
62
65
|
{ name: "estimatedCost", type: "decimal", allowNull: true, precision: 20, scale: 8 },
|
|
63
66
|
{ name: "currency", type: "string", allowNull: true },
|
|
67
|
+
{ name: "costStatus", type: "string", allowNull: true, index: true },
|
|
68
|
+
{ name: "modelPriceId", type: "bigInt", allowNull: true, index: true },
|
|
69
|
+
{ name: "quotaPolicyId", type: "bigInt", allowNull: true, index: true },
|
|
70
|
+
{ name: "inputPricePerMillionTokens", type: "decimal", allowNull: true, precision: 20, scale: 10 },
|
|
71
|
+
{ name: "outputPricePerMillionTokens", type: "decimal", allowNull: true, precision: 20, scale: 10 },
|
|
72
|
+
{ name: "fixedCostPerRequest", type: "decimal", allowNull: true, precision: 20, scale: 10 },
|
|
64
73
|
{ name: "providerRequestId", type: "string", allowNull: true },
|
|
65
74
|
{ name: "requestMetadata", type: "jsonb", defaultValue: {} },
|
|
66
75
|
{ name: "responseMetadata", type: "jsonb", defaultValue: {} },
|
|
@@ -0,0 +1,54 @@
|
|
|
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_user_quota_buckets_exports = {};
|
|
28
|
+
__export(ai_api_user_quota_buckets_exports, {
|
|
29
|
+
default: () => ai_api_user_quota_buckets_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_user_quota_buckets_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var ai_api_user_quota_buckets_default = (0, import_database.defineCollection)({
|
|
34
|
+
name: "aiApiUserQuotaBuckets",
|
|
35
|
+
autoGenId: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: "policyId", type: "bigInt", allowNull: false, index: true },
|
|
38
|
+
{ name: "userId", type: "bigInt", allowNull: false, index: true },
|
|
39
|
+
{ name: "periodStart", type: "datetimeTz", allowNull: false, index: true },
|
|
40
|
+
{ name: "periodEnd", type: "datetimeTz", allowNull: false, index: true },
|
|
41
|
+
{ name: "requestCount", type: "bigInt", allowNull: false, defaultValue: 0 },
|
|
42
|
+
{ name: "totalTokens", type: "bigInt", allowNull: false, defaultValue: 0 },
|
|
43
|
+
{ name: "cost", type: "decimal", precision: 20, scale: 8, allowNull: false, defaultValue: 0 },
|
|
44
|
+
{ name: "reservedRequests", type: "bigInt", allowNull: false, defaultValue: 0 },
|
|
45
|
+
{ name: "reservedTokens", type: "bigInt", allowNull: false, defaultValue: 0 },
|
|
46
|
+
{ name: "reservedCost", type: "decimal", precision: 20, scale: 8, allowNull: false, defaultValue: 0 }
|
|
47
|
+
],
|
|
48
|
+
indexes: [
|
|
49
|
+
{
|
|
50
|
+
fields: ["policyId", "periodStart"],
|
|
51
|
+
unique: true
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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_user_quota_policies_exports = {};
|
|
28
|
+
__export(ai_api_user_quota_policies_exports, {
|
|
29
|
+
default: () => ai_api_user_quota_policies_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_user_quota_policies_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var ai_api_user_quota_policies_default = (0, import_database.defineCollection)({
|
|
34
|
+
name: "aiApiUserQuotaPolicies",
|
|
35
|
+
autoGenId: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: "userId", type: "bigInt", allowNull: false, index: true },
|
|
38
|
+
{
|
|
39
|
+
name: "user",
|
|
40
|
+
type: "belongsTo",
|
|
41
|
+
target: "users",
|
|
42
|
+
targetKey: "id",
|
|
43
|
+
foreignKey: "userId",
|
|
44
|
+
constraints: false
|
|
45
|
+
},
|
|
46
|
+
{ name: "enabled", type: "boolean", defaultValue: true, index: true },
|
|
47
|
+
{ name: "periodType", type: "string", allowNull: false, defaultValue: "monthly" },
|
|
48
|
+
{ name: "timezone", type: "string", allowNull: false, defaultValue: "UTC" },
|
|
49
|
+
{ name: "requestLimit", type: "bigInt", allowNull: true },
|
|
50
|
+
{ name: "totalTokenLimit", type: "bigInt", allowNull: true },
|
|
51
|
+
{ name: "costLimit", type: "decimal", precision: 20, scale: 8, allowNull: true },
|
|
52
|
+
{ name: "currency", type: "string", allowNull: false, defaultValue: "USD" },
|
|
53
|
+
{ name: "rejectUnpricedModel", type: "boolean", defaultValue: true },
|
|
54
|
+
{ name: "missingUsageBehavior", type: "string", allowNull: false, defaultValue: "use_reserved" }
|
|
55
|
+
],
|
|
56
|
+
indexes: [
|
|
57
|
+
{
|
|
58
|
+
fields: ["userId"],
|
|
59
|
+
unique: true
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
});
|