plugin-ai-api 1.0.15 → 1.0.21
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 +10 -0
- package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
- package/dist/client/562.17a0a299d2e5152c.js +10 -0
- package/dist/client/757.a01403fb7a1bea01.js +10 -0
- package/dist/client/902.92e1daaf1ab16ebf.js +10 -0
- package/dist/client/97.72979a11a067a7c9.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
- package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
- package/dist/client-v2/757.a117ce1cf7119cea.js +10 -0
- package/dist/client-v2/902.9054d990ddc223ac.js +10 -0
- package/dist/client-v2/952.94100128b7757f56.js +10 -0
- package/dist/client-v2/97.29c663318eebbd57.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +36 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +105 -10
- package/dist/locale/vi-VN.json +105 -0
- package/dist/locale/zh-CN.json +105 -10
- package/dist/server/billing.js +331 -0
- package/dist/server/collections/ai-api-config.js +18 -0
- package/dist/server/collections/ai-api-model-metadata.js +83 -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 +36 -3
- package/dist/server/resource/ai-api-config.js +25 -0
- package/dist/server/resource/ai-api-usage-monitor.js +86 -0
- package/dist/server/routes/agent-completions.js +62 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +157 -6
- package/dist/server/routes/completions.js +20 -3
- package/dist/server/routes/models.js +78 -20
- package/dist/server/routes/router.js +108 -23
- package/dist/server/usage.js +19 -2
- package/dist/server/utils/app-observability.js +110 -0
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/validation.js +120 -0
- package/dist/swagger.js +32 -1
- package/package.json +1 -1
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +82 -48
- package/src/client-v2/__tests__/settings-registration.test.tsx +58 -0
- package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
- package/src/client-v2/locale.ts +21 -0
- package/src/client-v2/pages/GeneralPage.tsx +183 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
- package/src/client-v2/pages/ModelPricingPage.tsx +285 -0
- package/src/client-v2/pages/RolePermissionsTab.tsx +14 -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 +62 -4
- package/src/constants.ts +21 -0
- package/src/locale/en-US.json +105 -10
- package/src/locale/vi-VN.json +105 -0
- package/src/locale/zh-CN.json +105 -10
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/billing-quota.test.ts +134 -0
- package/src/server/__tests__/billing.test.ts +33 -0
- package/src/server/__tests__/models.test.ts +74 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -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 +69 -51
- package/src/server/collections/ai-api-model-metadata.ts +72 -0
- 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 +47 -5
- package/src/server/resource/ai-api-config.ts +105 -74
- package/src/server/resource/ai-api-usage-monitor.ts +74 -0
- package/src/server/routes/agent-completions.ts +77 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +275 -6
- package/src/server/routes/completions.ts +27 -4
- package/src/server/routes/models.ts +290 -195
- package/src/server/routes/router.ts +152 -27
- package/src/server/usage.ts +19 -1
- package/src/server/utils/app-observability.ts +105 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/validation.ts +89 -0
- package/src/swagger.ts +38 -1
- package/dist/client/778.5c452944cb747975.js +0 -10
- package/dist/client/950.83390c5f1d5a97fb.js +0 -10
- package/dist/client-v2/950.42b30b5cc9e32b8f.js +0 -10
- package/src/client/AiApiConfigPage.tsx +0 -309
|
@@ -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,24 @@ 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: "maxRequestBodyMb",
|
|
67
|
+
type: "integer",
|
|
68
|
+
defaultValue: 10,
|
|
69
|
+
comment: "Max request body size in MB. Raise this to accept inline base64 images in vision requests."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "quotaEnabled",
|
|
73
|
+
type: "boolean",
|
|
74
|
+
defaultValue: false,
|
|
75
|
+
comment: "Enable per-user request, token, and cost quotas for direct LLM mode"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "defaultReservationOutputTokens",
|
|
79
|
+
type: "integer",
|
|
80
|
+
defaultValue: 4096,
|
|
81
|
+
comment: "Output tokens reserved when a request does not specify a maximum"
|
|
82
|
+
},
|
|
65
83
|
{
|
|
66
84
|
name: "options",
|
|
67
85
|
type: "jsonb",
|
|
@@ -0,0 +1,83 @@
|
|
|
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_metadata_exports = {};
|
|
28
|
+
__export(ai_api_model_metadata_exports, {
|
|
29
|
+
default: () => ai_api_model_metadata_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_model_metadata_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var ai_api_model_metadata_default = (0, import_database.defineCollection)({
|
|
34
|
+
name: "aiApiModelMetadata",
|
|
35
|
+
autoGenId: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: "llmService", type: "string", allowNull: false, index: true },
|
|
38
|
+
{ name: "model", type: "string", allowNull: false, index: true },
|
|
39
|
+
{
|
|
40
|
+
name: "contextWindow",
|
|
41
|
+
type: "integer",
|
|
42
|
+
allowNull: true,
|
|
43
|
+
comment: "Override for the model context window (input+output token capacity). Returned as context_window / context_length."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "maxCompletionTokens",
|
|
47
|
+
type: "integer",
|
|
48
|
+
allowNull: true,
|
|
49
|
+
comment: "Override for the maximum output tokens. Returned as max_completion_tokens."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "ownedByOverride",
|
|
53
|
+
type: "string",
|
|
54
|
+
allowNull: true,
|
|
55
|
+
comment: "Override for the owned_by field in the OpenAI model object."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "displayName",
|
|
59
|
+
type: "string",
|
|
60
|
+
allowNull: true,
|
|
61
|
+
comment: "Friendly display name returned as display_name / name in the model object."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "description",
|
|
65
|
+
type: "text",
|
|
66
|
+
allowNull: true,
|
|
67
|
+
comment: "Human-readable description returned as description in the model object."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "enabled",
|
|
71
|
+
type: "boolean",
|
|
72
|
+
defaultValue: true,
|
|
73
|
+
index: true,
|
|
74
|
+
comment: "When false, the model is hidden from /v1/models and reported as active:false."
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
indexes: [
|
|
78
|
+
{
|
|
79
|
+
fields: ["llmService", "model"],
|
|
80
|
+
unique: true
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
});
|
|
@@ -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
|
+
});
|