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.
Files changed (60) hide show
  1. package/dist/client/302.25edd5d75460acbf.js +10 -0
  2. package/dist/client/757.71e30f2a1306562d.js +10 -0
  3. package/dist/client/902.4238b04ac667c30a.js +10 -0
  4. package/dist/client/97.37cda285d7da3a26.js +10 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/client-v2/302.9b27a263901d54d8.js +10 -0
  7. package/dist/client-v2/757.c377e2f2b054d89d.js +10 -0
  8. package/dist/client-v2/902.d40d7bda106124c8.js +10 -0
  9. package/dist/client-v2/97.fc922c37ced86831.js +10 -0
  10. package/dist/client-v2/index.js +1 -1
  11. package/dist/externalVersion.js +9 -9
  12. package/dist/locale/en-US.json +78 -2
  13. package/dist/locale/vi-VN.json +86 -0
  14. package/dist/locale/zh-CN.json +86 -10
  15. package/dist/server/billing.js +331 -0
  16. package/dist/server/collections/ai-api-config.js +12 -0
  17. package/dist/server/collections/ai-api-model-prices.js +55 -0
  18. package/dist/server/collections/ai-api-usage-records.js +9 -0
  19. package/dist/server/collections/ai-api-user-quota-buckets.js +54 -0
  20. package/dist/server/collections/ai-api-user-quota-policies.js +62 -0
  21. package/dist/server/plugin.js +23 -2
  22. package/dist/server/resource/ai-api-config.js +8 -0
  23. package/dist/server/resource/ai-api-usage-monitor.js +86 -0
  24. package/dist/server/routes/chat-completions.js +12 -2
  25. package/dist/server/routes/completions.js +12 -2
  26. package/dist/server/routes/router.js +14 -1
  27. package/dist/server/usage.js +17 -2
  28. package/dist/server/validation.js +102 -0
  29. package/package.json +1 -1
  30. package/src/client/plugin.tsx +73 -48
  31. package/src/client-v2/locale.ts +1 -0
  32. package/src/client-v2/pages/GeneralPage.tsx +170 -0
  33. package/src/client-v2/pages/ModelPricingPage.tsx +285 -0
  34. package/src/client-v2/pages/UsagePage.tsx +248 -0
  35. package/src/client-v2/pages/UserQuotasPage.tsx +258 -0
  36. package/src/client-v2/pages/api.ts +16 -0
  37. package/src/client-v2/plugin.tsx +21 -3
  38. package/src/locale/en-US.json +78 -2
  39. package/src/locale/vi-VN.json +86 -0
  40. package/src/locale/zh-CN.json +86 -10
  41. package/src/server/__tests__/billing-quota.test.ts +134 -0
  42. package/src/server/__tests__/billing.test.ts +33 -0
  43. package/src/server/__tests__/usage-monitor.test.ts +63 -0
  44. package/src/server/__tests__/usage-route.test.ts +4 -0
  45. package/src/server/billing.ts +387 -0
  46. package/src/server/collections/ai-api-config.ts +63 -51
  47. package/src/server/collections/ai-api-model-prices.ts +25 -0
  48. package/src/server/collections/ai-api-usage-records.ts +9 -0
  49. package/src/server/collections/ai-api-user-quota-buckets.ts +24 -0
  50. package/src/server/collections/ai-api-user-quota-policies.ts +32 -0
  51. package/src/server/plugin.ts +24 -2
  52. package/src/server/resource/ai-api-config.ts +82 -74
  53. package/src/server/resource/ai-api-usage-monitor.ts +74 -0
  54. package/src/server/routes/chat-completions.ts +13 -2
  55. package/src/server/routes/completions.ts +13 -2
  56. package/src/server/routes/router.ts +16 -1
  57. package/src/server/usage.ts +17 -1
  58. package/src/server/validation.ts +62 -0
  59. package/dist/client/950.83390c5f1d5a97fb.js +0 -10
  60. package/dist/client-v2/950.42b30b5cc9e32b8f.js +0 -10
@@ -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: ["aiApiConfig:*", "aiApiRolePermissions:*"]
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
- ctx.status = 500;
199
- ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
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
- ctx.status = 500;
150
- ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
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("Access-Control-Expose-Headers", "X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After");
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
  };
@@ -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 usage = usageResult.source === "provider" ? usageResult.usage : void 0;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-ai-api",
3
- "version": "1.0.14",
3
+ "version": "1.0.20",
4
4
  "main": "dist/server/index.js",
5
5
  "dependencies": {},
6
6
  "peerDependencies": {
@@ -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('./AiApiConfigPage'));
15
- const { AiApiRolePermissions } = lazy(() => import('./components/AiApiRolePermissions'), 'AiApiRolePermissions');
16
-
17
- export class PluginAiApiClient extends Plugin {
18
- async load() {
19
- this.app.pluginSettingsManager.add('ai-api', {
20
- icon: 'ApiOutlined',
21
- title: this.t('AI API Gateway'),
22
- aclSnippet: 'pm.ai-api.configuration',
23
- });
24
-
25
- this.app.pluginSettingsManager.add('ai-api.config', {
26
- title: this.t('Configuration'),
27
- Component: AiApiConfigPage,
28
- });
29
-
30
- // Add "AI API" tab in Settings → Users & Permissions → [Role]
31
- const aclPlugin = this.app.pm.get(PluginACLClient);
32
- if (aclPlugin?.settingsUI) {
33
- aclPlugin.settingsUI.addPermissionsTab(({ t, TabLayout, activeRole }) => ({
34
- key: 'aiApi',
35
- label: 'AI API',
36
- sort: 25,
37
- children: (
38
- <TabLayout>
39
- <AiApiRolePermissions role={activeRole} />
40
- </TabLayout>
41
- ),
42
- }));
43
- }
44
- }
45
-
46
- }
47
-
48
- export default PluginAiApiClient;
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';