plugin-ai-api 1.0.25 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
- package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
- package/dist/client/562.44b16aad4718b4c7.js +10 -0
- package/dist/client/685.ae483e17b6b49c98.js +10 -0
- package/dist/client/{757.56952e321dc399b7.js → 757.6568d3504ad29352.js} +1 -1
- package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.3971233415999b2c.js +10 -0
- package/dist/client-v2/562.45d5c504433be38b.js +10 -0
- package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
- package/dist/client-v2/{757.db678ca1aa6c422c.js → 757.f2bc9cfba07004b0.js} +1 -1
- package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
- package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +2 -5
- package/dist/locale/en-US.json +26 -8
- package/dist/locale/vi-VN.json +26 -8
- package/dist/locale/zh-CN.json +26 -8
- package/dist/server/billing.js +25 -32
- package/dist/server/collections/ai-api-config.js +1 -7
- package/dist/server/collections/ai-api-group-members.js +62 -0
- package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
- package/dist/server/collections/ai-api-model-metadata.js +6 -0
- package/dist/server/collections/ai-api-usage-groups.js +74 -0
- package/dist/server/collections/ai-api-usage-records.js +1 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
- package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
- package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
- package/dist/server/plugin.js +90 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +0 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +32 -32
- package/dist/server/routes/completions.js +16 -19
- package/dist/server/routes/embeddings.js +2 -1
- package/dist/server/routes/models.js +2 -1
- package/dist/server/routes/router.js +3 -2
- package/dist/server/services/file-processor.js +186 -22
- package/dist/server/usage.js +5 -1
- package/dist/server/utils/direct-llm-context.js +13 -11
- package/dist/server/utils/rate-limiter.js +1 -1
- package/dist/server/utils/request-cache.js +61 -0
- package/dist/server/utils/resolve-service.js +2 -1
- package/dist/server/utils/user-permissions.js +25 -39
- package/dist/server/validation.js +7 -0
- package/dist/swagger.js +6 -7
- package/package.json +1 -1
- package/src/client/__tests__/settings-registration.test.tsx +6 -29
- package/src/client/plugin.tsx +5 -16
- package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
- package/src/client-v2/locale.ts +3 -1
- package/src/client-v2/pages/GeneralPage.tsx +0 -5
- package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
- package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +26 -8
- package/src/locale/vi-VN.json +26 -8
- package/src/locale/zh-CN.json +26 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +122 -4
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/usage-groups.test.ts +160 -0
- package/src/server/__tests__/usage-monitor.test.ts +2 -0
- package/src/server/__tests__/usage-route.test.ts +262 -2
- package/src/server/__tests__/usage.test.ts +38 -0
- package/src/server/__tests__/user-permissions.test.ts +214 -133
- package/src/server/__tests__/validation.test.ts +11 -0
- package/src/server/billing.ts +30 -38
- package/src/server/collections/ai-api-config.ts +1 -7
- package/src/server/collections/ai-api-group-members.ts +41 -0
- package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
- package/src/server/collections/ai-api-model-metadata.ts +7 -0
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +1 -0
- package/src/server/middleware/rate-limit.ts +10 -12
- package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
- package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
- package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
- package/src/server/plugin.ts +101 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +0 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +39 -36
- package/src/server/routes/completions.ts +18 -21
- package/src/server/routes/embeddings.ts +2 -1
- package/src/server/routes/models.ts +4 -3
- package/src/server/routes/router.ts +4 -3
- package/src/server/services/file-processor.ts +214 -24
- package/src/server/usage.ts +5 -1
- package/src/server/utils/direct-llm-context.ts +20 -11
- package/src/server/utils/rate-limiter.ts +1 -1
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +2 -1
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +7 -8
- package/dist/client/123.e6fe04c856ce6417.js +0 -10
- package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
- package/dist/client/562.17a0a299d2e5152c.js +0 -10
- package/dist/client/902.e74518750f1e4201.js +0 -10
- package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
- package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
- package/dist/client-v2/902.c7c00a565085438a.js +0 -10
- package/dist/server/resource/ai-api-user-permissions.js +0 -75
- package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
- package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
- package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
- package/src/server/resource/ai-api-user-permissions.ts +0 -76
|
@@ -34,6 +34,7 @@ var import_resolve_service = require("../utils/resolve-service");
|
|
|
34
34
|
var import_user_permissions = require("../utils/user-permissions");
|
|
35
35
|
var import_streaming = require("../utils/streaming");
|
|
36
36
|
var import_chat_completions = require("./chat-completions");
|
|
37
|
+
var import_request_cache = require("../utils/request-cache");
|
|
37
38
|
var import_usage = require("../usage");
|
|
38
39
|
var import_billing = require("../billing");
|
|
39
40
|
var import_direct_llm_context = require("../utils/direct-llm-context");
|
|
@@ -91,7 +92,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
91
92
|
);
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
|
-
const config = await
|
|
95
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
95
96
|
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
@@ -110,19 +111,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
110
111
|
if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
|
|
111
112
|
if (body.stop !== void 0) modelOptions.stop = body.stop;
|
|
112
113
|
const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
|
|
113
|
-
const messages = [];
|
|
114
|
-
if (config == null ? void 0 : config.defaultAiEmployee) {
|
|
115
|
-
const employee = await ctx.db.getRepository("aiEmployees").findOne({
|
|
116
|
-
filter: { username: config.defaultAiEmployee }
|
|
117
|
-
});
|
|
118
|
-
if (employee) {
|
|
119
|
-
const systemPrompt = employee.about || employee.defaultPrompt || "";
|
|
120
|
-
if (systemPrompt) {
|
|
121
|
-
messages.push({ role: "system", content: systemPrompt });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
messages.push({ role: "user", content: prompt });
|
|
114
|
+
const messages = [{ role: "user", content: prompt }];
|
|
126
115
|
const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
|
|
127
116
|
serviceName: service.name,
|
|
128
117
|
modelId,
|
|
@@ -211,7 +200,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
211
200
|
text,
|
|
212
201
|
index: 0,
|
|
213
202
|
logprobs: null,
|
|
214
|
-
finish_reason: "stop"
|
|
203
|
+
finish_reason: (0, import_chat_completions.extractFinishReason)(result) ?? "stop"
|
|
215
204
|
}
|
|
216
205
|
],
|
|
217
206
|
usage: usage ? {
|
|
@@ -232,7 +221,9 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
232
221
|
ctx.status = 200;
|
|
233
222
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
234
223
|
let usage;
|
|
224
|
+
let usageResponseMetadata;
|
|
235
225
|
let providerRequestId;
|
|
226
|
+
let providerFinishReason;
|
|
236
227
|
try {
|
|
237
228
|
const stream = await chatModel.stream(messages, {
|
|
238
229
|
...providerRequestParameters,
|
|
@@ -271,8 +262,14 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
271
262
|
);
|
|
272
263
|
}
|
|
273
264
|
if (chunk.usage_metadata) {
|
|
274
|
-
|
|
265
|
+
const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
|
|
266
|
+
if (normalized) {
|
|
267
|
+
usage = normalized;
|
|
268
|
+
usageResponseMetadata = chunk.response_metadata;
|
|
269
|
+
}
|
|
275
270
|
}
|
|
271
|
+
const chunkFinishReason = (0, import_chat_completions.extractFinishReason)(chunk);
|
|
272
|
+
if (chunkFinishReason) providerFinishReason = chunkFinishReason;
|
|
276
273
|
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
277
274
|
}
|
|
278
275
|
await (0, import_streaming.writeResponse)(
|
|
@@ -288,7 +285,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
288
285
|
text: "",
|
|
289
286
|
index: 0,
|
|
290
287
|
logprobs: null,
|
|
291
|
-
finish_reason: "stop"
|
|
288
|
+
finish_reason: providerFinishReason ?? "stop"
|
|
292
289
|
}
|
|
293
290
|
],
|
|
294
291
|
usage: null
|
|
@@ -308,7 +305,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
308
305
|
);
|
|
309
306
|
}
|
|
310
307
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
311
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
308
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
312
309
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
313
310
|
} catch (err) {
|
|
314
311
|
const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
|
|
@@ -324,7 +321,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
324
321
|
})
|
|
325
322
|
);
|
|
326
323
|
}
|
|
327
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
324
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
328
325
|
ctx.state.aiApiStreamResult = {
|
|
329
326
|
succeeded: false,
|
|
330
327
|
id: completionId,
|
|
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(embeddings_exports);
|
|
|
32
32
|
var import_openai_format = require("../utils/openai-format");
|
|
33
33
|
var import_resolve_service = require("../utils/resolve-service");
|
|
34
34
|
var import_user_permissions = require("../utils/user-permissions");
|
|
35
|
+
var import_request_cache = require("../utils/request-cache");
|
|
35
36
|
var import_usage = require("../usage");
|
|
36
37
|
async function handleEmbeddings(ctx, plugin) {
|
|
37
38
|
const body = ctx.request.body;
|
|
@@ -104,7 +105,7 @@ async function handleEmbeddings(ctx, plugin) {
|
|
|
104
105
|
}
|
|
105
106
|
let globalEnabledServices = [];
|
|
106
107
|
try {
|
|
107
|
-
const config = await
|
|
108
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
108
109
|
globalEnabledServices = (config == null ? void 0 : config.enabledLlmServices) ?? [];
|
|
109
110
|
} catch {
|
|
110
111
|
}
|
|
@@ -33,6 +33,7 @@ __export(models_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(models_exports);
|
|
34
34
|
var import_openai_format = require("../utils/openai-format");
|
|
35
35
|
var import_user_permissions = require("../utils/user-permissions");
|
|
36
|
+
var import_request_cache = require("../utils/request-cache");
|
|
36
37
|
async function handleListModels(ctx, plugin) {
|
|
37
38
|
var _a;
|
|
38
39
|
try {
|
|
@@ -198,7 +199,7 @@ function toPositiveInt(value) {
|
|
|
198
199
|
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
199
200
|
}
|
|
200
201
|
async function getPluginConfig(ctx) {
|
|
201
|
-
return
|
|
202
|
+
return (0, import_request_cache.getAiApiConfig)(ctx);
|
|
202
203
|
}
|
|
203
204
|
function resolveEnabledModels(service) {
|
|
204
205
|
const raw = service.enabledModels;
|
|
@@ -55,6 +55,7 @@ var import_rate_limit = require("../middleware/rate-limit");
|
|
|
55
55
|
var import_role_permission = require("../middleware/role-permission");
|
|
56
56
|
var import_usage = require("../usage");
|
|
57
57
|
var import_streaming = require("../utils/streaming");
|
|
58
|
+
var import_request_cache = require("../utils/request-cache");
|
|
58
59
|
var import_billing = require("../billing");
|
|
59
60
|
var import_app_observability = require("../utils/app-observability");
|
|
60
61
|
const API_PREFIX = "/api/ai-llm/v1";
|
|
@@ -273,7 +274,7 @@ async function resolveMaxBodyBytes(ctx) {
|
|
|
273
274
|
var _a, _b;
|
|
274
275
|
let configuredMb;
|
|
275
276
|
try {
|
|
276
|
-
const config = await
|
|
277
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
277
278
|
configuredMb = config == null ? void 0 : config.get("maxRequestBodyMb");
|
|
278
279
|
} catch (err) {
|
|
279
280
|
(_b = (_a = ctx.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, "AI API: could not read maxRequestBodyMb, using default:", err);
|
|
@@ -322,7 +323,7 @@ async function resolveMode(ctx) {
|
|
|
322
323
|
return headerMode;
|
|
323
324
|
}
|
|
324
325
|
try {
|
|
325
|
-
const config = await
|
|
326
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
326
327
|
if (config) {
|
|
327
328
|
const dbMode = config.get("mode") || config.mode;
|
|
328
329
|
if (dbMode === "agent" || dbMode === "llm") {
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
var __create = Object.create;
|
|
10
11
|
var __defProp = Object.defineProperty;
|
|
11
12
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
13
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
15
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
16
|
var __export = (target, all) => {
|
|
15
17
|
for (var name in all)
|
|
@@ -23,6 +25,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
23
25
|
}
|
|
24
26
|
return to;
|
|
25
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
|
+
));
|
|
26
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
37
|
var file_processor_exports = {};
|
|
28
38
|
__export(file_processor_exports, {
|
|
@@ -31,10 +41,14 @@ __export(file_processor_exports, {
|
|
|
31
41
|
base64FileForwarder: () => base64FileForwarder,
|
|
32
42
|
fetchFileAsBase64: () => fetchFileAsBase64,
|
|
33
43
|
httpFileUrlFetcher: () => httpFileUrlFetcher,
|
|
44
|
+
isBlockedAddress: () => isBlockedAddress,
|
|
34
45
|
pdfFileProcessor: () => pdfFileProcessor
|
|
35
46
|
});
|
|
36
47
|
module.exports = __toCommonJS(file_processor_exports);
|
|
48
|
+
var import_dns = __toESM(require("dns"));
|
|
49
|
+
var import_net = require("net");
|
|
37
50
|
var import_path = require("path");
|
|
51
|
+
var import_request_cache = require("../utils/request-cache");
|
|
38
52
|
class FileProcessorError extends Error {
|
|
39
53
|
constructor(code, message) {
|
|
40
54
|
super(message);
|
|
@@ -44,7 +58,9 @@ class FileProcessorError extends Error {
|
|
|
44
58
|
}
|
|
45
59
|
const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
46
60
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
61
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
47
62
|
const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
63
|
+
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
48
64
|
class FileProcessorService {
|
|
49
65
|
processors = [];
|
|
50
66
|
pdfRenderer = null;
|
|
@@ -82,11 +98,6 @@ class FileProcessorService {
|
|
|
82
98
|
function isRecord(value) {
|
|
83
99
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
100
|
}
|
|
85
|
-
function getUrlString(value) {
|
|
86
|
-
if (typeof value === "string") return value;
|
|
87
|
-
if (isRecord(value) && typeof value.url === "string") return value.url;
|
|
88
|
-
return void 0;
|
|
89
|
-
}
|
|
90
101
|
function extractFilename(url, contentDisposition) {
|
|
91
102
|
if (contentDisposition) {
|
|
92
103
|
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
@@ -99,35 +110,187 @@ function extractFilename(url, contentDisposition) {
|
|
|
99
110
|
}
|
|
100
111
|
return void 0;
|
|
101
112
|
}
|
|
113
|
+
function parseIpv4Octets(ip) {
|
|
114
|
+
const parts = ip.split(".");
|
|
115
|
+
if (parts.length !== 4) return void 0;
|
|
116
|
+
const octets = [];
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
if (!/^\d{1,3}$/.test(part)) return void 0;
|
|
119
|
+
const value = Number(part);
|
|
120
|
+
if (value > 255) return void 0;
|
|
121
|
+
octets.push(value);
|
|
122
|
+
}
|
|
123
|
+
return octets;
|
|
124
|
+
}
|
|
125
|
+
function isBlockedIpv4(ip) {
|
|
126
|
+
const octets = parseIpv4Octets(ip);
|
|
127
|
+
if (!octets) return true;
|
|
128
|
+
const [a, b] = octets;
|
|
129
|
+
if (a === 0) return true;
|
|
130
|
+
if (a === 10) return true;
|
|
131
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
132
|
+
if (a === 127) return true;
|
|
133
|
+
if (a === 169 && b === 254) return true;
|
|
134
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
135
|
+
if (a === 192 && b === 0) return true;
|
|
136
|
+
if (a === 192 && b === 168) return true;
|
|
137
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
138
|
+
if (a >= 224) return true;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function parseHexGroup(group) {
|
|
142
|
+
if (group.length < 1 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return void 0;
|
|
143
|
+
return parseInt(group, 16);
|
|
144
|
+
}
|
|
145
|
+
function expandIpv6(input) {
|
|
146
|
+
let address = input;
|
|
147
|
+
const zoneIndex = address.indexOf("%");
|
|
148
|
+
if (zoneIndex !== -1) address = address.slice(0, zoneIndex);
|
|
149
|
+
if (!address) return void 0;
|
|
150
|
+
const lastColon = address.lastIndexOf(":");
|
|
151
|
+
if (lastColon !== -1 && address.includes(".", lastColon)) {
|
|
152
|
+
const octets = parseIpv4Octets(address.slice(lastColon + 1));
|
|
153
|
+
if (!octets) return void 0;
|
|
154
|
+
const high = (octets[0] << 8 | octets[1]).toString(16);
|
|
155
|
+
const low = (octets[2] << 8 | octets[3]).toString(16);
|
|
156
|
+
address = `${address.slice(0, lastColon + 1)}${high}:${low}`;
|
|
157
|
+
}
|
|
158
|
+
const groups = [];
|
|
159
|
+
const doubleColonIndex = address.indexOf("::");
|
|
160
|
+
if (doubleColonIndex !== -1) {
|
|
161
|
+
if (address.indexOf("::", doubleColonIndex + 1) !== -1) return void 0;
|
|
162
|
+
const head = address.slice(0, doubleColonIndex);
|
|
163
|
+
const tail = address.slice(doubleColonIndex + 2);
|
|
164
|
+
const headGroups = head === "" ? [] : head.split(":");
|
|
165
|
+
const tailGroups = tail === "" ? [] : tail.split(":");
|
|
166
|
+
const fillCount = 8 - headGroups.length - tailGroups.length;
|
|
167
|
+
if (fillCount < 1) return void 0;
|
|
168
|
+
const allGroups = [...headGroups];
|
|
169
|
+
for (let i = 0; i < fillCount; i += 1) allGroups.push("0");
|
|
170
|
+
allGroups.push(...tailGroups);
|
|
171
|
+
for (const group of allGroups) {
|
|
172
|
+
const parsed = parseHexGroup(group);
|
|
173
|
+
if (parsed === void 0) return void 0;
|
|
174
|
+
groups.push(parsed);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
for (const group of address.split(":")) {
|
|
178
|
+
const parsed = parseHexGroup(group);
|
|
179
|
+
if (parsed === void 0) return void 0;
|
|
180
|
+
groups.push(parsed);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return groups.length === 8 ? groups : void 0;
|
|
184
|
+
}
|
|
185
|
+
function isBlockedIpv6(ip) {
|
|
186
|
+
const groups = expandIpv6(ip);
|
|
187
|
+
if (!groups) return true;
|
|
188
|
+
const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
|
|
189
|
+
if (groups.every((group) => group === 0)) return true;
|
|
190
|
+
if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0 && g7 === 1) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const isIpv4Mapped = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 65535;
|
|
194
|
+
const isIpv4Compatible = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
|
|
195
|
+
const isNat64 = g0 === 100 && g1 === 65435 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
|
|
196
|
+
if (isIpv4Mapped || isIpv4Compatible || isNat64) {
|
|
197
|
+
const embedded = `${g6 >> 8 & 255}.${g6 & 255}.${g7 >> 8 & 255}.${g7 & 255}`;
|
|
198
|
+
return isBlockedIpv4(embedded);
|
|
199
|
+
}
|
|
200
|
+
if ((g0 & 65024) === 64512) return true;
|
|
201
|
+
if ((g0 & 65472) === 65152) return true;
|
|
202
|
+
if ((g0 & 65280) === 65280) return true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
function isBlockedAddress(ip) {
|
|
206
|
+
const version = (0, import_net.isIP)(ip);
|
|
207
|
+
if (version === 4) return isBlockedIpv4(ip);
|
|
208
|
+
if (version === 6) return isBlockedIpv6(ip);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
async function assertHostAllowed(hostname) {
|
|
212
|
+
if ((0, import_net.isIP)(hostname) !== 0) {
|
|
213
|
+
if (isBlockedAddress(hostname)) {
|
|
214
|
+
throw new FileProcessorError("blocked_host", `Host '${hostname}' is a blocked address.`);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let addresses;
|
|
219
|
+
try {
|
|
220
|
+
addresses = await import_dns.default.promises.lookup(hostname, { all: true });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const message = error instanceof Error && error.message ? error.message : String(error);
|
|
223
|
+
throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}': ${message}`);
|
|
224
|
+
}
|
|
225
|
+
if (!addresses.length) {
|
|
226
|
+
throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}'.`);
|
|
227
|
+
}
|
|
228
|
+
for (const { address } of addresses) {
|
|
229
|
+
if (isBlockedAddress(address)) {
|
|
230
|
+
throw new FileProcessorError("blocked_host", `Host '${hostname}' resolves to blocked address '${address}'.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function fetchWithSsrfGuard(initialUrl, options) {
|
|
235
|
+
let currentUrl = initialUrl;
|
|
236
|
+
for (let hop = 0; ; hop += 1) {
|
|
237
|
+
let parsed;
|
|
238
|
+
try {
|
|
239
|
+
parsed = new URL(currentUrl);
|
|
240
|
+
} catch {
|
|
241
|
+
throw new FileProcessorError("invalid_url", `File URL '${currentUrl}' is not a valid URL.`);
|
|
242
|
+
}
|
|
243
|
+
if (!options.allowedProtocols.has(parsed.protocol)) {
|
|
244
|
+
throw new FileProcessorError("unsupported_protocol", `File URL protocol '${parsed.protocol}' is not allowed.`);
|
|
245
|
+
}
|
|
246
|
+
const hostname = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
|
247
|
+
await assertHostAllowed(hostname);
|
|
248
|
+
const response = await fetch(currentUrl, { signal: options.signal, redirect: "manual" });
|
|
249
|
+
if (!REDIRECT_STATUSES.has(response.status)) {
|
|
250
|
+
return { response, finalUrl: currentUrl };
|
|
251
|
+
}
|
|
252
|
+
if (hop >= options.maxRedirects) {
|
|
253
|
+
throw new FileProcessorError(
|
|
254
|
+
"too_many_redirects",
|
|
255
|
+
`File URL '${initialUrl}' exceeded the limit of ${options.maxRedirects} redirects.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const location = response.headers.get("location");
|
|
259
|
+
if (!location) {
|
|
260
|
+
throw new FileProcessorError("fetch_failed", `Redirect from '${currentUrl}' is missing a Location header.`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
264
|
+
} catch {
|
|
265
|
+
throw new FileProcessorError(
|
|
266
|
+
"invalid_url",
|
|
267
|
+
`Redirect Location '${location}' from '${currentUrl}' is not a valid URL.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
102
272
|
async function fetchFileAsBase64(url, options = {}) {
|
|
103
273
|
const maxSize = options.maxSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
|
104
274
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
275
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
105
276
|
const allowedProtocols = options.allowedProtocols ? new Set(options.allowedProtocols) : ALLOWED_PROTOCOLS;
|
|
106
|
-
let protocol;
|
|
107
|
-
try {
|
|
108
|
-
protocol = new URL(url).protocol;
|
|
109
|
-
} catch {
|
|
110
|
-
throw new FileProcessorError("invalid_url", `File URL '${url}' is not a valid URL.`);
|
|
111
|
-
}
|
|
112
|
-
if (!allowedProtocols.has(protocol)) {
|
|
113
|
-
throw new FileProcessorError("unsupported_protocol", `File URL protocol '${protocol}' is not allowed.`);
|
|
114
|
-
}
|
|
115
277
|
const controller = new AbortController();
|
|
116
278
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
117
279
|
try {
|
|
118
|
-
const response = await
|
|
119
|
-
|
|
120
|
-
|
|
280
|
+
const { response, finalUrl } = await fetchWithSsrfGuard(url, {
|
|
281
|
+
allowedProtocols,
|
|
282
|
+
maxRedirects,
|
|
283
|
+
signal: controller.signal
|
|
121
284
|
});
|
|
122
285
|
if (!response.ok) {
|
|
123
286
|
throw new FileProcessorError(
|
|
124
287
|
"fetch_failed",
|
|
125
|
-
`Failed to fetch file from '${
|
|
288
|
+
`Failed to fetch file from '${finalUrl}': ${response.status} ${response.statusText}`
|
|
126
289
|
);
|
|
127
290
|
}
|
|
128
291
|
const contentLength = response.headers.get("content-length");
|
|
129
292
|
if (contentLength && Number(contentLength) > maxSize) {
|
|
130
|
-
throw new FileProcessorError("file_too_large", `File at '${
|
|
293
|
+
throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
|
|
131
294
|
}
|
|
132
295
|
const contentType = response.headers.get("content-type") || void 0;
|
|
133
296
|
if (options.allowedContentTypes && contentType && !options.allowedContentTypes.some((type) => contentType.includes(type))) {
|
|
@@ -135,11 +298,11 @@ async function fetchFileAsBase64(url, options = {}) {
|
|
|
135
298
|
}
|
|
136
299
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
137
300
|
if (buffer.length > maxSize) {
|
|
138
|
-
throw new FileProcessorError("file_too_large", `File at '${
|
|
301
|
+
throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
|
|
139
302
|
}
|
|
140
303
|
const mimeType = (contentType == null ? void 0 : contentType.split(";")[0].trim()) ?? "application/octet-stream";
|
|
141
304
|
const contentDisposition = response.headers.get("content-disposition");
|
|
142
|
-
const filename = extractFilename(
|
|
305
|
+
const filename = extractFilename(finalUrl, contentDisposition) ?? "file";
|
|
143
306
|
return {
|
|
144
307
|
fileData: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
145
308
|
mimeType,
|
|
@@ -225,7 +388,7 @@ const pdfFileProcessor = {
|
|
|
225
388
|
},
|
|
226
389
|
async process(block, context) {
|
|
227
390
|
var _a, _b, _c, _d;
|
|
228
|
-
const config = await context.ctx
|
|
391
|
+
const config = await (0, import_request_cache.getAiApiConfig)(context.ctx);
|
|
229
392
|
if (!(config == null ? void 0 : config.pdfRenderPagesAsImages)) {
|
|
230
393
|
return block;
|
|
231
394
|
}
|
|
@@ -258,5 +421,6 @@ const pdfFileProcessor = {
|
|
|
258
421
|
base64FileForwarder,
|
|
259
422
|
fetchFileAsBase64,
|
|
260
423
|
httpFileUrlFetcher,
|
|
424
|
+
isBlockedAddress,
|
|
261
425
|
pdfFileProcessor
|
|
262
426
|
});
|
package/dist/server/usage.js
CHANGED
|
@@ -74,7 +74,10 @@ function normalizeUsage(value) {
|
|
|
74
74
|
prompt_tokens: prompt,
|
|
75
75
|
completion_tokens: completion,
|
|
76
76
|
total_tokens: total,
|
|
77
|
-
|
|
77
|
+
// Keep normalizeUsage idempotent: streaming routes normalize the chunk
|
|
78
|
+
// usage once and hand the result to setAiApiUsageResult, which normalizes
|
|
79
|
+
// again — an already-extracted prompt_cache_tokens must survive that pass.
|
|
80
|
+
prompt_cache_tokens: extractPromptCacheTokens(source) ?? normalizeTokenCount(source.prompt_cache_tokens)
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
function setAiApiUsageResult(ctx, value, metadata = {}, responseMetadata) {
|
|
@@ -171,6 +174,7 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
|
171
174
|
costStatus: billing.costStatus ?? null,
|
|
172
175
|
modelPriceId: billing.modelPriceId ?? null,
|
|
173
176
|
quotaPolicyId: billing.quotaPolicyId ?? null,
|
|
177
|
+
groupId: billing.groupId ?? null,
|
|
174
178
|
inputPricePerMillionTokens: billing.inputPricePerMillionTokens ?? null,
|
|
175
179
|
outputPricePerMillionTokens: billing.outputPricePerMillionTokens ?? null,
|
|
176
180
|
fixedCostPerRequest: billing.fixedCostPerRequest ?? null,
|
|
@@ -31,6 +31,7 @@ __export(direct_llm_context_exports, {
|
|
|
31
31
|
prepareDirectLlmContext: () => prepareDirectLlmContext
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(direct_llm_context_exports);
|
|
34
|
+
var import_request_cache = require("./request-cache");
|
|
34
35
|
class DirectLlmContextError extends Error {
|
|
35
36
|
constructor(code, message) {
|
|
36
37
|
super(message);
|
|
@@ -237,16 +238,16 @@ async function loadModelMetadata(ctx, serviceName, modelId) {
|
|
|
237
238
|
`Context metadata is not configured for '${serviceName}/${modelId}'. Configure context window and max completion tokens.`
|
|
238
239
|
);
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
+
const systemPromptValue = getValue(row, "systemPrompt");
|
|
242
|
+
const systemPrompt = typeof systemPromptValue === "string" ? systemPromptValue.trim() : "";
|
|
243
|
+
return { contextWindow, maxCompletionTokens, ...systemPrompt ? { systemPrompt } : {} };
|
|
241
244
|
}
|
|
242
245
|
async function resolveOverflowBehavior(ctx) {
|
|
243
246
|
var _a;
|
|
244
247
|
const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
|
|
245
248
|
if (userId === null || userId === void 0) return "reject";
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
return getValue(policy, "contextOverflowBehavior") === "truncate" ? "truncate" : "reject";
|
|
249
|
+
const group = await (0, import_request_cache.resolveRequestUserGroup)(ctx, userId);
|
|
250
|
+
return group.contextOverflowBehavior === "truncate" ? "truncate" : "reject";
|
|
250
251
|
}
|
|
251
252
|
function resolveReservedOutputTokens(options, metadata) {
|
|
252
253
|
const requested = positiveInteger(options.maxCompletionTokens ?? options.maxTokens);
|
|
@@ -272,10 +273,11 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
272
273
|
);
|
|
273
274
|
}
|
|
274
275
|
const fixedOverheadTokens = estimateValueTokens(options.tools) + (options.tools === void 0 ? 0 : 4);
|
|
275
|
-
const
|
|
276
|
+
const baseMessages = metadata.systemPrompt ? [{ role: "system", content: metadata.systemPrompt }, ...options.messages] : options.messages;
|
|
277
|
+
const originalEstimate = estimateMessagesTokens(baseMessages) + fixedOverheadTokens;
|
|
276
278
|
if (originalEstimate <= inputTokenBudget) {
|
|
277
279
|
return {
|
|
278
|
-
messages:
|
|
280
|
+
messages: baseMessages,
|
|
279
281
|
estimatedInputTokens: originalEstimate,
|
|
280
282
|
inputTokenBudget,
|
|
281
283
|
reservedOutputTokens,
|
|
@@ -288,13 +290,13 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
288
290
|
`Estimated input tokens (${originalEstimate}) exceed the allowed input budget (${inputTokenBudget}).`
|
|
289
291
|
);
|
|
290
292
|
}
|
|
291
|
-
const { turns } = splitTurns(
|
|
293
|
+
const { turns } = splitTurns(baseMessages);
|
|
292
294
|
let remainingTurns = turns;
|
|
293
|
-
let messages = messagesWithTurns(
|
|
295
|
+
let messages = messagesWithTurns(baseMessages, remainingTurns);
|
|
294
296
|
let estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
|
|
295
297
|
while (remainingTurns.length > 1 && estimatedInputTokens > inputTokenBudget) {
|
|
296
298
|
remainingTurns = remainingTurns.slice(1);
|
|
297
|
-
messages = messagesWithTurns(
|
|
299
|
+
messages = messagesWithTurns(baseMessages, remainingTurns);
|
|
298
300
|
estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
|
|
299
301
|
}
|
|
300
302
|
if (estimatedInputTokens > inputTokenBudget) {
|
|
@@ -308,7 +310,7 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
308
310
|
estimatedInputTokens,
|
|
309
311
|
inputTokenBudget,
|
|
310
312
|
reservedOutputTokens,
|
|
311
|
-
truncated: messages.length !==
|
|
313
|
+
truncated: messages.length !== baseMessages.length
|
|
312
314
|
};
|
|
313
315
|
}
|
|
314
316
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -39,7 +39,7 @@ class RateLimiter {
|
|
|
39
39
|
* Check and record a request for a user.
|
|
40
40
|
*
|
|
41
41
|
* @param userId The user ID (string or numeric)
|
|
42
|
-
* @param limit Max allowed requests per window (from
|
|
42
|
+
* @param limit Max allowed requests per window (from the user's usage group)
|
|
43
43
|
* @returns { allowed: true } or { allowed: false, retryAfterMs: number }
|
|
44
44
|
*/
|
|
45
45
|
check(userId, limit) {
|
|
@@ -0,0 +1,61 @@
|
|
|
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 request_cache_exports = {};
|
|
28
|
+
__export(request_cache_exports, {
|
|
29
|
+
getAiApiConfig: () => getAiApiConfig,
|
|
30
|
+
resolveRequestUserGroup: () => resolveRequestUserGroup
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(request_cache_exports);
|
|
33
|
+
var import_quota_groups = require("../quota-groups");
|
|
34
|
+
function getCache(ctx) {
|
|
35
|
+
if (!ctx.state.aiApiRequestCache) {
|
|
36
|
+
ctx.state.aiApiRequestCache = { configLoaded: false, config: null };
|
|
37
|
+
}
|
|
38
|
+
return ctx.state.aiApiRequestCache;
|
|
39
|
+
}
|
|
40
|
+
async function getAiApiConfig(ctx) {
|
|
41
|
+
const cache = getCache(ctx);
|
|
42
|
+
if (!cache.configLoaded) {
|
|
43
|
+
cache.config = await ctx.db.getRepository("aiApiConfig").findOne() ?? null;
|
|
44
|
+
cache.configLoaded = true;
|
|
45
|
+
}
|
|
46
|
+
return cache.config;
|
|
47
|
+
}
|
|
48
|
+
async function resolveRequestUserGroup(ctx, userId) {
|
|
49
|
+
const cache = getCache(ctx);
|
|
50
|
+
const groupKey = userId === void 0 || userId === null ? "" : String(userId);
|
|
51
|
+
if (!cache.group || cache.groupKey !== groupKey) {
|
|
52
|
+
cache.group = await (0, import_quota_groups.resolveUserGroup)(ctx, userId);
|
|
53
|
+
cache.groupKey = groupKey;
|
|
54
|
+
}
|
|
55
|
+
return cache.group;
|
|
56
|
+
}
|
|
57
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
58
|
+
0 && (module.exports = {
|
|
59
|
+
getAiApiConfig,
|
|
60
|
+
resolveRequestUserGroup
|
|
61
|
+
});
|
|
@@ -30,6 +30,7 @@ __export(resolve_service_exports, {
|
|
|
30
30
|
resolveModelString: () => resolveModelString
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(resolve_service_exports);
|
|
33
|
+
var import_request_cache = require("./request-cache");
|
|
33
34
|
async function resolveLlmService(ctx, serviceKey) {
|
|
34
35
|
const repo = ctx.db.getRepository("llmServices");
|
|
35
36
|
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
@@ -60,7 +61,7 @@ async function resolveModelString(ctx, modelString) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
const config = await
|
|
64
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
64
65
|
if (config == null ? void 0 : config.defaultLlmService) {
|
|
65
66
|
const service = await repo.findOne({ filter: { name: config.defaultLlmService } });
|
|
66
67
|
if (service) {
|