plugin-ai-api 1.0.24 → 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.6568d3504ad29352.js +10 -0
- 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.f2bc9cfba07004b0.js +10 -0
- 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/externalVersion.js +8 -8
- package/dist/locale/en-US.json +27 -8
- package/dist/locale/vi-VN.json +27 -8
- package/dist/locale/zh-CN.json +27 -8
- package/dist/server/billing.js +31 -33
- package/dist/server/collections/ai-api-config.js +7 -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 +2 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
- 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 +100 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +5 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/resource/ai-api-usage-monitor.js +3 -1
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +121 -42
- package/dist/server/routes/completions.js +48 -29
- 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 +426 -0
- package/dist/server/usage.js +37 -3
- package/dist/server/utils/direct-llm-context.js +163 -26
- package/dist/server/utils/openai-format.js +21 -2
- 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 +48 -10
- 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/pages/UsagePage.tsx +9 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +27 -8
- package/src/locale/vi-VN.json +27 -8
- package/src/locale/zh-CN.json +27 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +209 -10
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/openai-format.test.ts +12 -2
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/request-body.test.ts +45 -2
- 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 +382 -5
- package/src/server/__tests__/usage.test.ts +57 -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 +36 -39
- package/src/server/collections/ai-api-config.ts +9 -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-role-permissions.ts +41 -41
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +2 -0
- package/src/server/index.ts +10 -10
- package/src/server/middleware/rate-limit.ts +68 -70
- package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
- 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 +121 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +5 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/resource/ai-api-usage-monitor.ts +3 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +173 -47
- package/src/server/routes/completions.ts +50 -27
- 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/__tests__/file-processor.test.ts +184 -0
- package/src/server/services/file-processor.ts +513 -0
- package/src/server/usage.ts +51 -1
- package/src/server/utils/direct-llm-context.ts +218 -31
- package/src/server/utils/openai-format.ts +25 -2
- package/src/server/utils/rate-limiter.ts +83 -83
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +83 -82
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +52 -11
- 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/757.a01403fb7a1bea01.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/757.a117ce1cf7119cea.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
|
@@ -27,9 +27,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var direct_llm_context_exports = {};
|
|
28
28
|
__export(direct_llm_context_exports, {
|
|
29
29
|
DirectLlmContextError: () => DirectLlmContextError,
|
|
30
|
+
parseImageDimensions: () => parseImageDimensions,
|
|
30
31
|
prepareDirectLlmContext: () => prepareDirectLlmContext
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(direct_llm_context_exports);
|
|
34
|
+
var import_request_cache = require("./request-cache");
|
|
33
35
|
class DirectLlmContextError extends Error {
|
|
34
36
|
constructor(code, message) {
|
|
35
37
|
super(message);
|
|
@@ -45,20 +47,159 @@ function positiveInteger(value) {
|
|
|
45
47
|
const parsed = Number(value);
|
|
46
48
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
47
49
|
}
|
|
48
|
-
function hasImageContent(value) {
|
|
49
|
-
return Array.isArray(value) && value.some(
|
|
50
|
-
(block) => typeof block === "object" && block !== null && block.type === "image_url"
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
50
|
function estimateValueTokens(value) {
|
|
54
51
|
if (value === void 0) return 0;
|
|
55
52
|
return Math.ceil(Buffer.byteLength(JSON.stringify(value), "utf8") / 3);
|
|
56
53
|
}
|
|
57
|
-
function
|
|
58
|
-
return
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function readBigEndian(buf, offset) {
|
|
58
|
+
return buf.readUInt32BE(offset);
|
|
59
|
+
}
|
|
60
|
+
function readUInt16BE(buf, offset) {
|
|
61
|
+
return buf.readUInt16BE(offset);
|
|
62
|
+
}
|
|
63
|
+
function readLittleEndian(buf, offset) {
|
|
64
|
+
return buf.readUInt16LE(offset);
|
|
65
|
+
}
|
|
66
|
+
function parsePngDimensions(buffer) {
|
|
67
|
+
if (buffer.length < 24) return void 0;
|
|
68
|
+
return { width: readBigEndian(buffer, 16), height: readBigEndian(buffer, 20) };
|
|
69
|
+
}
|
|
70
|
+
function parseJpegDimensions(buffer) {
|
|
71
|
+
let offset = 2;
|
|
72
|
+
while (offset < buffer.length) {
|
|
73
|
+
if (buffer[offset] !== 255) {
|
|
74
|
+
offset++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const marker = buffer[offset + 1];
|
|
78
|
+
if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
|
|
79
|
+
if (offset + 9 <= buffer.length) {
|
|
80
|
+
return { height: readUInt16BE(buffer, offset + 5), width: readUInt16BE(buffer, offset + 7) };
|
|
81
|
+
}
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
if (marker === 217 || offset + 4 >= buffer.length) break;
|
|
85
|
+
const segmentLength = buffer.readUInt16BE(offset + 2);
|
|
86
|
+
offset += 2 + segmentLength;
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
59
89
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
90
|
+
function parseGifDimensions(buffer) {
|
|
91
|
+
if (buffer.length < 10) return void 0;
|
|
92
|
+
return { width: readLittleEndian(buffer, 6), height: readLittleEndian(buffer, 8) };
|
|
93
|
+
}
|
|
94
|
+
function parseWebpDimensions(buffer) {
|
|
95
|
+
if (buffer.length < 30) return void 0;
|
|
96
|
+
const riff = buffer.toString("ascii", 0, 4);
|
|
97
|
+
const webp = buffer.toString("ascii", 8, 12);
|
|
98
|
+
if (riff !== "RIFF" || webp !== "WEBP") return void 0;
|
|
99
|
+
const chunkType = buffer.toString("ascii", 12, 16);
|
|
100
|
+
if (chunkType === "VP8 " && buffer.length >= 26) {
|
|
101
|
+
return { width: readLittleEndian(buffer, 26), height: readLittleEndian(buffer, 28) };
|
|
102
|
+
}
|
|
103
|
+
if (chunkType === "VP8L" && buffer.length >= 24) {
|
|
104
|
+
const bits = buffer.readUInt32LE(21);
|
|
105
|
+
return {
|
|
106
|
+
width: (bits & 16383) + 1,
|
|
107
|
+
height: (bits >> 14 & 16383) + 1
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (chunkType === "VP8X" && buffer.length >= 30) {
|
|
111
|
+
return {
|
|
112
|
+
width: ((buffer[24] | buffer[25] << 8 | buffer[26] << 16) & 16777215) + 1,
|
|
113
|
+
height: ((buffer[27] | buffer[28] << 8 | buffer[29] << 16) & 16777215) + 1
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
function parseImageDimensions(buffer) {
|
|
119
|
+
if (buffer.length < 12) return void 0;
|
|
120
|
+
const header = buffer.toString("binary", 0, 4);
|
|
121
|
+
if (header === "\x89PNG") return parsePngDimensions(buffer);
|
|
122
|
+
if (header === "GIF8") return parseGifDimensions(buffer);
|
|
123
|
+
if (header === "RIFF") return parseWebpDimensions(buffer);
|
|
124
|
+
if (buffer[0] === 255 && buffer[1] === 216) return parseJpegDimensions(buffer);
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
const VISION_TILE_SIZE = 512;
|
|
128
|
+
const VISION_LOW_DETAIL_TOKENS = 85;
|
|
129
|
+
const VISION_TILE_TOKENS = 170;
|
|
130
|
+
const VISION_HTTP_URL_ESTIMATE = 1024;
|
|
131
|
+
const FILE_BASE64_FALLBACK_TOKENS = 1024;
|
|
132
|
+
function estimateVisionTokensForDimensions(width, height) {
|
|
133
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
134
|
+
return VISION_LOW_DETAIL_TOKENS;
|
|
135
|
+
}
|
|
136
|
+
const tilesX = Math.ceil(width / VISION_TILE_SIZE);
|
|
137
|
+
const tilesY = Math.ceil(height / VISION_TILE_SIZE);
|
|
138
|
+
return VISION_LOW_DETAIL_TOKENS + tilesX * tilesY * VISION_TILE_TOKENS;
|
|
139
|
+
}
|
|
140
|
+
function decodeBase64DataUrl(url) {
|
|
141
|
+
const match = /^data:([^;]+);base64,([A-Za-z0-9+/]+=?=?)$/.exec(url);
|
|
142
|
+
if (!match) return void 0;
|
|
143
|
+
try {
|
|
144
|
+
const buffer = Buffer.from(match[2], "base64");
|
|
145
|
+
return { mimeType: match[1].toLowerCase(), buffer };
|
|
146
|
+
} catch {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function estimateImageUrlTokens(imageUrl) {
|
|
151
|
+
const url = typeof imageUrl === "string" ? imageUrl : isRecord(imageUrl) ? String(imageUrl.url ?? "") : "";
|
|
152
|
+
if (!url) return 0;
|
|
153
|
+
if (url.startsWith("data:")) {
|
|
154
|
+
const decoded = decodeBase64DataUrl(url);
|
|
155
|
+
if (!decoded) return FILE_BASE64_FALLBACK_TOKENS;
|
|
156
|
+
if (!decoded.mimeType.startsWith("image/")) return FILE_BASE64_FALLBACK_TOKENS;
|
|
157
|
+
const dimensions = parseImageDimensions(decoded.buffer);
|
|
158
|
+
return dimensions ? estimateVisionTokensForDimensions(dimensions.width, dimensions.height) : VISION_LOW_DETAIL_TOKENS;
|
|
159
|
+
}
|
|
160
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
161
|
+
return VISION_HTTP_URL_ESTIMATE;
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
function estimateFileBlockTokens(block) {
|
|
166
|
+
const file = isRecord(block.file) ? block.file : void 0;
|
|
167
|
+
if (!file) return 0;
|
|
168
|
+
const fileData = String(file.file_data ?? "");
|
|
169
|
+
if (fileData.startsWith("data:")) {
|
|
170
|
+
const decoded = decodeBase64DataUrl(fileData);
|
|
171
|
+
if (decoded) {
|
|
172
|
+
return Math.max(1, Math.ceil(decoded.buffer.length / 3));
|
|
173
|
+
}
|
|
174
|
+
return FILE_BASE64_FALLBACK_TOKENS;
|
|
175
|
+
}
|
|
176
|
+
return FILE_BASE64_FALLBACK_TOKENS;
|
|
177
|
+
}
|
|
178
|
+
function estimateContentBlockTokens(block) {
|
|
179
|
+
if (!isRecord(block)) return estimateValueTokens(block);
|
|
180
|
+
const type = typeof block.type === "string" ? block.type : void 0;
|
|
181
|
+
if (type === "text") {
|
|
182
|
+
return typeof block.text === "string" ? estimateValueTokens(block.text) + 4 : 4;
|
|
183
|
+
}
|
|
184
|
+
if (type === "image_url") {
|
|
185
|
+
return estimateImageUrlTokens(block.image_url) + 4;
|
|
186
|
+
}
|
|
187
|
+
if (type === "file") {
|
|
188
|
+
return estimateFileBlockTokens(block) + 4;
|
|
189
|
+
}
|
|
190
|
+
if (type === "file_url") {
|
|
191
|
+
return VISION_HTTP_URL_ESTIMATE + 4;
|
|
192
|
+
}
|
|
193
|
+
return estimateValueTokens(block) + 4;
|
|
194
|
+
}
|
|
195
|
+
function estimateMessageTokens(message) {
|
|
196
|
+
if (Array.isArray(message.content)) {
|
|
197
|
+
return message.content.reduce((total, block) => total + estimateContentBlockTokens(block), 4);
|
|
198
|
+
}
|
|
199
|
+
return estimateValueTokens(message) + 4;
|
|
200
|
+
}
|
|
201
|
+
function estimateMessagesTokens(messages) {
|
|
202
|
+
return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
|
|
62
203
|
}
|
|
63
204
|
function isInstruction(message) {
|
|
64
205
|
return message.role === "system" || message.role === "developer";
|
|
@@ -97,16 +238,16 @@ async function loadModelMetadata(ctx, serviceName, modelId) {
|
|
|
97
238
|
`Context metadata is not configured for '${serviceName}/${modelId}'. Configure context window and max completion tokens.`
|
|
98
239
|
);
|
|
99
240
|
}
|
|
100
|
-
|
|
241
|
+
const systemPromptValue = getValue(row, "systemPrompt");
|
|
242
|
+
const systemPrompt = typeof systemPromptValue === "string" ? systemPromptValue.trim() : "";
|
|
243
|
+
return { contextWindow, maxCompletionTokens, ...systemPrompt ? { systemPrompt } : {} };
|
|
101
244
|
}
|
|
102
245
|
async function resolveOverflowBehavior(ctx) {
|
|
103
246
|
var _a;
|
|
104
247
|
const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
|
|
105
248
|
if (userId === null || userId === void 0) return "reject";
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
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";
|
|
110
251
|
}
|
|
111
252
|
function resolveReservedOutputTokens(options, metadata) {
|
|
112
253
|
const requested = positiveInteger(options.maxCompletionTokens ?? options.maxTokens);
|
|
@@ -119,12 +260,6 @@ function resolveReservedOutputTokens(options, metadata) {
|
|
|
119
260
|
return requested ?? metadata.maxCompletionTokens;
|
|
120
261
|
}
|
|
121
262
|
async function prepareDirectLlmContext(ctx, options) {
|
|
122
|
-
if (containsUnsupportedContent(options.messages)) {
|
|
123
|
-
throw new DirectLlmContextError(
|
|
124
|
-
"context_estimation_unsupported",
|
|
125
|
-
"Context enforcement does not support image_url content without a model-specific vision token estimator."
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
263
|
const [metadata, behavior] = await Promise.all([
|
|
129
264
|
loadModelMetadata(ctx, options.serviceName, options.modelId),
|
|
130
265
|
resolveOverflowBehavior(ctx)
|
|
@@ -138,10 +273,11 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
138
273
|
);
|
|
139
274
|
}
|
|
140
275
|
const fixedOverheadTokens = estimateValueTokens(options.tools) + (options.tools === void 0 ? 0 : 4);
|
|
141
|
-
const
|
|
276
|
+
const baseMessages = metadata.systemPrompt ? [{ role: "system", content: metadata.systemPrompt }, ...options.messages] : options.messages;
|
|
277
|
+
const originalEstimate = estimateMessagesTokens(baseMessages) + fixedOverheadTokens;
|
|
142
278
|
if (originalEstimate <= inputTokenBudget) {
|
|
143
279
|
return {
|
|
144
|
-
messages:
|
|
280
|
+
messages: baseMessages,
|
|
145
281
|
estimatedInputTokens: originalEstimate,
|
|
146
282
|
inputTokenBudget,
|
|
147
283
|
reservedOutputTokens,
|
|
@@ -154,13 +290,13 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
154
290
|
`Estimated input tokens (${originalEstimate}) exceed the allowed input budget (${inputTokenBudget}).`
|
|
155
291
|
);
|
|
156
292
|
}
|
|
157
|
-
const { turns } = splitTurns(
|
|
293
|
+
const { turns } = splitTurns(baseMessages);
|
|
158
294
|
let remainingTurns = turns;
|
|
159
|
-
let messages = messagesWithTurns(
|
|
295
|
+
let messages = messagesWithTurns(baseMessages, remainingTurns);
|
|
160
296
|
let estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
|
|
161
297
|
while (remainingTurns.length > 1 && estimatedInputTokens > inputTokenBudget) {
|
|
162
298
|
remainingTurns = remainingTurns.slice(1);
|
|
163
|
-
messages = messagesWithTurns(
|
|
299
|
+
messages = messagesWithTurns(baseMessages, remainingTurns);
|
|
164
300
|
estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
|
|
165
301
|
}
|
|
166
302
|
if (estimatedInputTokens > inputTokenBudget) {
|
|
@@ -174,11 +310,12 @@ async function prepareDirectLlmContext(ctx, options) {
|
|
|
174
310
|
estimatedInputTokens,
|
|
175
311
|
inputTokenBudget,
|
|
176
312
|
reservedOutputTokens,
|
|
177
|
-
truncated: messages.length !==
|
|
313
|
+
truncated: messages.length !== baseMessages.length
|
|
178
314
|
};
|
|
179
315
|
}
|
|
180
316
|
// Annotate the CommonJS export names for ESM import in node:
|
|
181
317
|
0 && (module.exports = {
|
|
182
318
|
DirectLlmContextError,
|
|
319
|
+
parseImageDimensions,
|
|
183
320
|
prepareDirectLlmContext
|
|
184
321
|
});
|
|
@@ -99,7 +99,19 @@ function toOpenAIResponse(options) {
|
|
|
99
99
|
finish_reason: finishReason
|
|
100
100
|
}
|
|
101
101
|
],
|
|
102
|
-
usage: usage
|
|
102
|
+
usage: usage ? {
|
|
103
|
+
prompt_tokens: usage.prompt_tokens ?? null,
|
|
104
|
+
completion_tokens: usage.completion_tokens ?? null,
|
|
105
|
+
total_tokens: usage.total_tokens ?? null,
|
|
106
|
+
prompt_tokens_details: {
|
|
107
|
+
cached_tokens: usage.prompt_cache_tokens ?? null
|
|
108
|
+
}
|
|
109
|
+
} : {
|
|
110
|
+
prompt_tokens: null,
|
|
111
|
+
completion_tokens: null,
|
|
112
|
+
total_tokens: null,
|
|
113
|
+
prompt_tokens_details: { cached_tokens: null }
|
|
114
|
+
}
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
function toOpenAIStreamChunk(options) {
|
|
@@ -129,7 +141,14 @@ function toOpenAIUsageChunk(options) {
|
|
|
129
141
|
created: Math.floor(Date.now() / 1e3),
|
|
130
142
|
model,
|
|
131
143
|
choices: [],
|
|
132
|
-
usage
|
|
144
|
+
usage: {
|
|
145
|
+
prompt_tokens: usage.prompt_tokens,
|
|
146
|
+
completion_tokens: usage.completion_tokens,
|
|
147
|
+
total_tokens: usage.total_tokens,
|
|
148
|
+
prompt_tokens_details: {
|
|
149
|
+
cached_tokens: usage.prompt_cache_tokens ?? null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
133
152
|
};
|
|
134
153
|
}
|
|
135
154
|
function toOpenAIEmbeddingsResponse(options) {
|
|
@@ -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) {
|
|
@@ -28,73 +28,60 @@ var user_permissions_exports = {};
|
|
|
28
28
|
__export(user_permissions_exports, {
|
|
29
29
|
buildAccessScope: () => buildAccessScope,
|
|
30
30
|
enforceModelAccess: () => enforceModelAccess,
|
|
31
|
-
|
|
31
|
+
invalidateGroupAccessCache: () => invalidateGroupAccessCache,
|
|
32
32
|
isModelAllowed: () => isModelAllowed,
|
|
33
33
|
isServiceAllowed: () => isServiceAllowed,
|
|
34
34
|
resolveUserAccessScope: () => resolveUserAccessScope
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(user_permissions_exports);
|
|
37
37
|
var import_openai_format = require("./openai-format");
|
|
38
|
+
var import_request_cache = require("./request-cache");
|
|
38
39
|
const SCOPE_TTL_MS = 15e3;
|
|
39
40
|
const scopeCache = /* @__PURE__ */ new Map();
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
denyAll: false,
|
|
43
|
-
allowedServices: null,
|
|
41
|
+
const OPEN_SCOPE = {
|
|
42
|
+
allowedServices: [],
|
|
44
43
|
allowAllModels: true,
|
|
45
44
|
allowedModels: /* @__PURE__ */ new Set(),
|
|
46
45
|
lookupFailed: false
|
|
47
46
|
};
|
|
48
|
-
const LOOKUP_FAILED_SCOPE = { ...
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
47
|
+
const LOOKUP_FAILED_SCOPE = { ...OPEN_SCOPE, lookupFailed: true };
|
|
48
|
+
function invalidateGroupAccessCache(groupId) {
|
|
49
|
+
if (groupId === void 0 || groupId === null) {
|
|
51
50
|
scopeCache.clear();
|
|
52
51
|
return;
|
|
53
52
|
}
|
|
54
|
-
const suffix =
|
|
53
|
+
const suffix = `:group:${groupId}`;
|
|
55
54
|
for (const key of scopeCache.keys()) {
|
|
56
55
|
if (key.endsWith(suffix)) scopeCache.delete(key);
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
|
-
function valueOf(row, name) {
|
|
60
|
-
if (!row) return void 0;
|
|
61
|
-
const candidate = row;
|
|
62
|
-
if (typeof candidate.get === "function") return candidate.get(name);
|
|
63
|
-
return row[name];
|
|
64
|
-
}
|
|
65
58
|
function toStringArray(value) {
|
|
66
59
|
if (!Array.isArray(value)) return [];
|
|
67
60
|
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
68
61
|
}
|
|
69
|
-
function buildAccessScope(
|
|
70
|
-
if (!row) return NO_RECORD_SCOPE;
|
|
71
|
-
if (valueOf(row, "enabled") === false) {
|
|
72
|
-
return { ...NO_RECORD_SCOPE, hasUserRecord: true, denyAll: true, allowedServices: [] };
|
|
73
|
-
}
|
|
62
|
+
function buildAccessScope(group) {
|
|
74
63
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
allowedModels: new Set(toStringArray(valueOf(row, "allowedModels"))),
|
|
64
|
+
groupId: group.id,
|
|
65
|
+
allowedServices: toStringArray(group.allowedLlmServices),
|
|
66
|
+
allowAllModels: group.allowAllModels !== false,
|
|
67
|
+
allowedModels: new Set(toStringArray(group.allowedModels)),
|
|
80
68
|
lookupFailed: false
|
|
81
69
|
};
|
|
82
70
|
}
|
|
83
71
|
async function resolveUserAccessScope(ctx) {
|
|
84
72
|
var _a, _b, _c, _d;
|
|
85
73
|
const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
|
|
86
|
-
|
|
87
|
-
const key = `${((_b = ctx.app) == null ? void 0 : _b.name) ?? "main"}:${userId}`;
|
|
88
|
-
const cached = scopeCache.get(key);
|
|
89
|
-
if (cached && cached.expiresAt > Date.now()) return cached.scope;
|
|
90
|
-
let scope;
|
|
74
|
+
let group;
|
|
91
75
|
try {
|
|
92
|
-
|
|
93
|
-
scope = buildAccessScope(row);
|
|
76
|
+
group = await (0, import_request_cache.resolveRequestUserGroup)(ctx, userId);
|
|
94
77
|
} catch (err) {
|
|
95
|
-
(
|
|
78
|
+
(_c = (_b = ctx.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, "AI API group access lookup failed, denying access:", err);
|
|
96
79
|
return LOOKUP_FAILED_SCOPE;
|
|
97
80
|
}
|
|
81
|
+
const key = `${((_d = ctx.app) == null ? void 0 : _d.name) ?? "main"}:group:${group.id}`;
|
|
82
|
+
const cached = scopeCache.get(key);
|
|
83
|
+
if (cached && cached.expiresAt > Date.now()) return cached.scope;
|
|
84
|
+
const scope = buildAccessScope(group);
|
|
98
85
|
scopeCache.set(key, { scope, expiresAt: Date.now() + SCOPE_TTL_MS });
|
|
99
86
|
return scope;
|
|
100
87
|
}
|
|
@@ -102,15 +89,14 @@ function matchesService(list, serviceName, serviceTitle) {
|
|
|
102
89
|
return list.some((entry) => entry === serviceName || entry === serviceTitle);
|
|
103
90
|
}
|
|
104
91
|
function isServiceAllowed(scope, globalEnabledServices, service) {
|
|
105
|
-
if (scope.
|
|
92
|
+
if (scope.lookupFailed) return false;
|
|
106
93
|
const globalList = toStringArray(globalEnabledServices);
|
|
107
94
|
if (globalList.length && !matchesService(globalList, service.name, service.title)) return false;
|
|
108
|
-
if (!scope.
|
|
109
|
-
return matchesService(scope.allowedServices
|
|
95
|
+
if (!scope.allowedServices.length) return true;
|
|
96
|
+
return matchesService(scope.allowedServices, service.name, service.title);
|
|
110
97
|
}
|
|
111
98
|
function isModelAllowed(scope, fullModelId) {
|
|
112
|
-
if (scope.
|
|
113
|
-
if (!scope.hasUserRecord) return true;
|
|
99
|
+
if (scope.lookupFailed) return false;
|
|
114
100
|
if (scope.allowAllModels) return true;
|
|
115
101
|
return scope.allowedModels.has(fullModelId);
|
|
116
102
|
}
|
|
@@ -153,7 +139,7 @@ async function enforceModelAccess(ctx, globalEnabledServices, service, modelId)
|
|
|
153
139
|
0 && (module.exports = {
|
|
154
140
|
buildAccessScope,
|
|
155
141
|
enforceModelAccess,
|
|
156
|
-
|
|
142
|
+
invalidateGroupAccessCache,
|
|
157
143
|
isModelAllowed,
|
|
158
144
|
isServiceAllowed,
|
|
159
145
|
resolveUserAccessScope
|
|
@@ -88,6 +88,10 @@ function validateModelMetadata(model) {
|
|
|
88
88
|
if (!String(model.get("model") ?? "").trim()) throw new Error("model is required.");
|
|
89
89
|
requirePositiveIntegerOrNull(model.get("contextWindow"), "contextWindow");
|
|
90
90
|
requirePositiveIntegerOrNull(model.get("maxCompletionTokens"), "maxCompletionTokens");
|
|
91
|
+
const systemPrompt = model.get("systemPrompt");
|
|
92
|
+
if (systemPrompt !== null && systemPrompt !== void 0 && typeof systemPrompt !== "string") {
|
|
93
|
+
throw new Error("systemPrompt must be a string.");
|
|
94
|
+
}
|
|
91
95
|
const contextWindow = model.get("contextWindow");
|
|
92
96
|
const maxCompletionTokens = model.get("maxCompletionTokens");
|
|
93
97
|
if (contextWindow !== null && contextWindow !== void 0 && contextWindow !== "" && maxCompletionTokens !== null && maxCompletionTokens !== void 0 && maxCompletionTokens !== "" && Number(maxCompletionTokens) > Number(contextWindow)) {
|
|
@@ -98,6 +102,9 @@ function validateQuotaPolicy(model) {
|
|
|
98
102
|
if (!["daily", "monthly"].includes(String(model.get("periodType")))) {
|
|
99
103
|
throw new Error("periodType must be daily or monthly.");
|
|
100
104
|
}
|
|
105
|
+
if (!["share", "per_user"].includes(String(model.get("quotaMode")))) {
|
|
106
|
+
throw new Error("quotaMode must be share or per_user.");
|
|
107
|
+
}
|
|
101
108
|
if (!["allow", "use_reserved"].includes(String(model.get("missingUsageBehavior")))) {
|
|
102
109
|
throw new Error("missingUsageBehavior must be allow or use_reserved.");
|
|
103
110
|
}
|
package/dist/swagger.js
CHANGED
|
@@ -78,7 +78,7 @@ var swagger_default = {
|
|
|
78
78
|
get: {
|
|
79
79
|
tags: ["ai-llm"],
|
|
80
80
|
summary: "List available models",
|
|
81
|
-
description: "Returns the LLM models available to the authenticated caller across registered services. Model IDs are formatted as `serviceName/modelId`.\n\nThe catalog is user-scoped: it starts from `enabledLlmServices` in the AI API configuration, then narrows to the caller's `
|
|
81
|
+
description: "Returns the LLM models available to the authenticated caller across registered services. Model IDs are formatted as `serviceName/modelId`.\n\nThe catalog is user-scoped: it starts from `enabledLlmServices` in the AI API configuration, then narrows to the caller's usage group settings (`allowedLlmServices` / `allowedModels`). Group settings can only narrow the global whitelist, never widen it, so two users may receive different lists from the same request.",
|
|
82
82
|
security: [{ BearerAuth: [] }],
|
|
83
83
|
responses: {
|
|
84
84
|
200: {
|
|
@@ -268,16 +268,15 @@ var swagger_default = {
|
|
|
268
268
|
enum: ["llm", "agent"],
|
|
269
269
|
description: "Default AI mode"
|
|
270
270
|
},
|
|
271
|
-
defaultAiEmployee: {
|
|
271
|
+
defaultAiEmployee: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Default AI employee name (agent mode only; direct LLM mode ignores it)"
|
|
274
|
+
},
|
|
272
275
|
defaultLlmService: { type: "string", description: "Default LLM service name" },
|
|
273
276
|
enabledLlmServices: {
|
|
274
277
|
type: "array",
|
|
275
278
|
items: { type: "string" },
|
|
276
|
-
description: "List of enabled LLM service names. This is the outer bound for every caller;
|
|
277
|
-
},
|
|
278
|
-
rateLimitPerMinute: {
|
|
279
|
-
type: "integer",
|
|
280
|
-
description: "Max requests per minute per user (0 = unlimited)"
|
|
279
|
+
description: "List of enabled LLM service names. This is the outer bound for every caller; usage group settings can only narrow it further."
|
|
281
280
|
},
|
|
282
281
|
maxRequestBodyMb: {
|
|
283
282
|
type: "integer",
|
|
@@ -285,6 +284,11 @@ var swagger_default = {
|
|
|
285
284
|
maximum: 100,
|
|
286
285
|
default: 10,
|
|
287
286
|
description: "Max request body size in megabytes. Requests above this return 413. The gateway buffers each body in memory, so values above 100 are rejected."
|
|
287
|
+
},
|
|
288
|
+
pdfRenderPagesAsImages: {
|
|
289
|
+
type: "boolean",
|
|
290
|
+
default: false,
|
|
291
|
+
description: "When true, PDF file/file_url blocks are rendered to per-page PNG images and sent as image_url blocks. Requires a registered PdfToImageRenderer. When false or no renderer is available, PDFs are forwarded as file blocks."
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
},
|
|
@@ -299,9 +303,9 @@ var swagger_default = {
|
|
|
299
303
|
},
|
|
300
304
|
ContentBlock: {
|
|
301
305
|
type: "object",
|
|
302
|
-
description: "A multimodal content block.
|
|
306
|
+
description: "A multimodal content block. text, image_url, file and file_url blocks are forwarded; file and file_url blocks are first run through the configurable file processor service.",
|
|
303
307
|
properties: {
|
|
304
|
-
type: { type: "string", enum: ["text", "image_url"] },
|
|
308
|
+
type: { type: "string", enum: ["text", "image_url", "file", "file_url"] },
|
|
305
309
|
text: { type: "string" },
|
|
306
310
|
image_url: {
|
|
307
311
|
type: "object",
|
|
@@ -314,6 +318,34 @@ var swagger_default = {
|
|
|
314
318
|
detail: { type: "string", enum: ["auto", "low", "high"] }
|
|
315
319
|
},
|
|
316
320
|
required: ["url"]
|
|
321
|
+
},
|
|
322
|
+
file: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
file_data: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "A base64 data URL, e.g. data:application/pdf;base64,JVBERi0...",
|
|
328
|
+
example: "data:application/pdf;base64,JVBERi0..."
|
|
329
|
+
},
|
|
330
|
+
filename: { type: "string" },
|
|
331
|
+
mime_type: {
|
|
332
|
+
type: "string",
|
|
333
|
+
description: "MIME type of the file, e.g. application/pdf",
|
|
334
|
+
example: "application/pdf"
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
required: ["file_data"]
|
|
338
|
+
},
|
|
339
|
+
file_url: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
url: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "An http(s) URL pointing to a file. The gateway downloads the file and converts it to a file block.",
|
|
345
|
+
example: "https://example.com/document.pdf"
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
required: ["url"]
|
|
317
349
|
}
|
|
318
350
|
},
|
|
319
351
|
required: ["type"]
|
|
@@ -374,7 +406,13 @@ var swagger_default = {
|
|
|
374
406
|
properties: {
|
|
375
407
|
prompt_tokens: { type: "integer" },
|
|
376
408
|
completion_tokens: { type: "integer" },
|
|
377
|
-
total_tokens: { type: "integer" }
|
|
409
|
+
total_tokens: { type: "integer" },
|
|
410
|
+
prompt_tokens_details: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
cached_tokens: { type: "integer" }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
378
416
|
}
|
|
379
417
|
}
|
|
380
418
|
}
|