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,6 +27,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var chat_completions_exports = {};
|
|
28
28
|
__export(chat_completions_exports, {
|
|
29
29
|
applyProviderRequestParameters: () => applyProviderRequestParameters,
|
|
30
|
+
extractFinishReason: () => extractFinishReason,
|
|
30
31
|
findContentBlockProblem: () => findContentBlockProblem,
|
|
31
32
|
findMessageProblem: () => findMessageProblem,
|
|
32
33
|
getProviderRequestParameters: () => getProviderRequestParameters,
|
|
@@ -37,12 +38,13 @@ module.exports = __toCommonJS(chat_completions_exports);
|
|
|
37
38
|
var import_openai_format = require("../utils/openai-format");
|
|
38
39
|
var import_resolve_service = require("../utils/resolve-service");
|
|
39
40
|
var import_streaming = require("../utils/streaming");
|
|
40
|
-
var import_role_permission = require("../middleware/role-permission");
|
|
41
41
|
var import_user_permissions = require("../utils/user-permissions");
|
|
42
|
+
var import_request_cache = require("../utils/request-cache");
|
|
42
43
|
var import_usage = require("../usage");
|
|
43
44
|
var import_billing = require("../billing");
|
|
44
45
|
var import_direct_llm_context = require("../utils/direct-llm-context");
|
|
45
46
|
var import_app_observability = require("../utils/app-observability");
|
|
47
|
+
var import_file_processor = require("../services/file-processor");
|
|
46
48
|
async function handleChatCompletions(ctx, plugin) {
|
|
47
49
|
var _a;
|
|
48
50
|
const body = ctx.request.body;
|
|
@@ -118,7 +120,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
118
120
|
);
|
|
119
121
|
return;
|
|
120
122
|
}
|
|
121
|
-
const config = await
|
|
123
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
122
124
|
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
@@ -147,30 +149,13 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
147
149
|
if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
|
|
148
150
|
if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
|
|
149
151
|
if (body.stop !== void 0) modelOptions.stop = body.stop;
|
|
150
|
-
let systemPrompt = "";
|
|
151
|
-
if (config == null ? void 0 : config.defaultAiEmployee) {
|
|
152
|
-
if (!(0, import_role_permission.checkEmployeeAccess)(ctx, config.defaultAiEmployee)) {
|
|
153
|
-
ctx.status = 403;
|
|
154
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
155
|
-
403,
|
|
156
|
-
`Role is not permitted to use AI Employee '${config.defaultAiEmployee}'. An admin must grant access in Settings \u2192 Users & Permissions \u2192 [Role] \u2192 AI API.`,
|
|
157
|
-
"permission_denied",
|
|
158
|
-
"employee_not_permitted"
|
|
159
|
-
);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const employee = await ctx.db.getRepository("aiEmployees").findOne({
|
|
163
|
-
filter: { username: config.defaultAiEmployee }
|
|
164
|
-
});
|
|
165
|
-
if (employee) {
|
|
166
|
-
systemPrompt = employee.about || employee.defaultPrompt || "";
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
152
|
let messages = [...body.messages];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
153
|
+
messages = await Promise.all(
|
|
154
|
+
messages.map(async (msg) => ({
|
|
155
|
+
...msg,
|
|
156
|
+
content: await processMessageContentFileBlocks(msg.content, ctx, plugin)
|
|
157
|
+
}))
|
|
158
|
+
);
|
|
174
159
|
const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
|
|
175
160
|
serviceName: service.name,
|
|
176
161
|
modelId,
|
|
@@ -232,13 +217,14 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
232
217
|
if (!((_a = ctx.res) == null ? void 0 : _a.headersSent)) {
|
|
233
218
|
const isQuotaError = err instanceof import_billing.AiApiQuotaError;
|
|
234
219
|
const isContextError = err instanceof import_direct_llm_context.DirectLlmContextError;
|
|
235
|
-
|
|
220
|
+
const isFileError = err instanceof import_file_processor.FileProcessorError;
|
|
221
|
+
ctx.status = isQuotaError ? 429 : isContextError || isFileError ? 400 : 500;
|
|
236
222
|
if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
|
|
237
223
|
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
238
224
|
ctx.status,
|
|
239
225
|
getErrorMessage(err, "Internal server error"),
|
|
240
|
-
isQuotaError ? "quota_error" : isContextError ? "invalid_request_error" : "server_error",
|
|
241
|
-
isQuotaError || isContextError ? err.code : void 0
|
|
226
|
+
isQuotaError ? "quota_error" : isContextError || isFileError ? "invalid_request_error" : "server_error",
|
|
227
|
+
isQuotaError || isContextError || isFileError ? err.code : void 0
|
|
242
228
|
);
|
|
243
229
|
}
|
|
244
230
|
}
|
|
@@ -252,18 +238,25 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
|
|
|
252
238
|
const textPart = result.content.find((c) => c.type === "text");
|
|
253
239
|
content = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
|
|
254
240
|
}
|
|
255
|
-
const usage = (0, import_usage.setAiApiUsageResult)(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
241
|
+
const usage = (0, import_usage.setAiApiUsageResult)(
|
|
242
|
+
ctx,
|
|
243
|
+
result.usage_metadata,
|
|
244
|
+
{
|
|
245
|
+
gatewayResponseId: completionId,
|
|
246
|
+
providerRequestId: (0, import_usage.extractProviderRequestId)(result)
|
|
247
|
+
},
|
|
248
|
+
result.response_metadata
|
|
249
|
+
);
|
|
259
250
|
ctx.status = 200;
|
|
260
251
|
const toolCalls = normalizeToolCalls(result.tool_calls);
|
|
252
|
+
const providerFinishReason = extractFinishReason(result);
|
|
261
253
|
ctx.body = (0, import_openai_format.toOpenAIResponse)({
|
|
262
254
|
id: completionId,
|
|
263
255
|
model: modelName,
|
|
264
256
|
content,
|
|
265
257
|
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
266
|
-
toolCalls
|
|
258
|
+
toolCalls,
|
|
259
|
+
...providerFinishReason ? { finishReason: providerFinishReason } : {}
|
|
267
260
|
});
|
|
268
261
|
}
|
|
269
262
|
async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
|
|
@@ -287,8 +280,10 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
287
280
|
);
|
|
288
281
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
289
282
|
let usage;
|
|
283
|
+
let usageResponseMetadata;
|
|
290
284
|
let providerRequestId;
|
|
291
|
-
let
|
|
285
|
+
let providerFinishReason;
|
|
286
|
+
let sawToolCalls = false;
|
|
292
287
|
try {
|
|
293
288
|
const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
|
|
294
289
|
for await (const chunk of stream) {
|
|
@@ -316,7 +311,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
316
311
|
const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
|
|
317
312
|
if (toolCallChunks.length) {
|
|
318
313
|
(0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
|
|
319
|
-
|
|
314
|
+
sawToolCalls = true;
|
|
320
315
|
await (0, import_streaming.writeResponse)(
|
|
321
316
|
ctx,
|
|
322
317
|
(0, import_openai_format.formatSSE)(
|
|
@@ -329,8 +324,14 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
329
324
|
);
|
|
330
325
|
}
|
|
331
326
|
if (chunk.usage_metadata) {
|
|
332
|
-
|
|
327
|
+
const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
|
|
328
|
+
if (normalized) {
|
|
329
|
+
usage = normalized;
|
|
330
|
+
usageResponseMetadata = chunk.response_metadata;
|
|
331
|
+
}
|
|
333
332
|
}
|
|
333
|
+
const chunkFinishReason = extractFinishReason(chunk);
|
|
334
|
+
if (chunkFinishReason) providerFinishReason = chunkFinishReason;
|
|
334
335
|
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
335
336
|
}
|
|
336
337
|
await (0, import_streaming.writeResponse)(
|
|
@@ -340,7 +341,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
340
341
|
id: completionId,
|
|
341
342
|
model: modelName,
|
|
342
343
|
delta: {},
|
|
343
|
-
finishReason
|
|
344
|
+
finishReason: providerFinishReason ?? (sawToolCalls ? "tool_calls" : "stop")
|
|
344
345
|
})
|
|
345
346
|
)
|
|
346
347
|
);
|
|
@@ -357,7 +358,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
357
358
|
);
|
|
358
359
|
}
|
|
359
360
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
360
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
361
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
361
362
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
362
363
|
} catch (err) {
|
|
363
364
|
const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
|
|
@@ -373,7 +374,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
373
374
|
})
|
|
374
375
|
);
|
|
375
376
|
}
|
|
376
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
377
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
377
378
|
ctx.state.aiApiStreamResult = {
|
|
378
379
|
succeeded: false,
|
|
379
380
|
id: completionId,
|
|
@@ -387,8 +388,20 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
387
388
|
function getErrorMessage(error, fallback) {
|
|
388
389
|
return error instanceof Error && error.message ? error.message : fallback;
|
|
389
390
|
}
|
|
390
|
-
|
|
391
|
+
function extractFinishReason(value) {
|
|
392
|
+
if (!isRecord(value)) return void 0;
|
|
393
|
+
const candidates = [
|
|
394
|
+
isRecord(value.response_metadata) ? value.response_metadata.finish_reason : void 0,
|
|
395
|
+
isRecord(value.additional_kwargs) ? value.additional_kwargs.finish_reason : void 0
|
|
396
|
+
];
|
|
397
|
+
for (const candidate of candidates) {
|
|
398
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
399
|
+
}
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
const SUPPORTED_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image_url", "file", "file_url"]);
|
|
391
403
|
const BASE64_DATA_URL_PATTERN = /^data:(\w+\/\w+);base64,([A-Za-z0-9+/]+=*)$/;
|
|
404
|
+
const FILE_BASE64_DATA_URL_PATTERN = /^data:([^;\s]+);base64,([A-Za-z0-9+/]+=*)$/;
|
|
392
405
|
function isDecodableBase64(payload) {
|
|
393
406
|
try {
|
|
394
407
|
return Buffer.from(payload, "base64").toString("base64") === payload;
|
|
@@ -440,13 +453,50 @@ function describeContentBlockProblem(block) {
|
|
|
440
453
|
const type = typeof block.type === "string" ? block.type : void 0;
|
|
441
454
|
if (!type) return "each content block requires a 'type' field";
|
|
442
455
|
if (!SUPPORTED_CONTENT_BLOCK_TYPES.has(type)) {
|
|
443
|
-
return `content block type '${type}' is not supported \u2014 this gateway forwards 'text' and '
|
|
456
|
+
return `content block type '${type}' is not supported \u2014 this gateway forwards 'text', 'image_url', 'file', and 'file_url' only. Send documents as text, or inline them as a 'file' / 'file_url' block`;
|
|
444
457
|
}
|
|
445
458
|
if (type === "text") {
|
|
446
459
|
return typeof block.text === "string" ? void 0 : "a 'text' block requires a string 'text' field";
|
|
447
460
|
}
|
|
461
|
+
if (type === "file") {
|
|
462
|
+
return describeFileProblem(block.file);
|
|
463
|
+
}
|
|
464
|
+
if (type === "file_url") {
|
|
465
|
+
return describeFileUrlProblem(block.file_url);
|
|
466
|
+
}
|
|
448
467
|
return describeImageUrlProblem(block.image_url);
|
|
449
468
|
}
|
|
469
|
+
function describeFileProblem(file) {
|
|
470
|
+
if (!isRecord(file)) return "a 'file' block requires an object 'file' field";
|
|
471
|
+
const fileData = typeof file.file_data === "string" ? file.file_data : void 0;
|
|
472
|
+
if (!fileData) return "a 'file' block requires a string 'file.file_data' field";
|
|
473
|
+
if (!fileData.startsWith("data:")) {
|
|
474
|
+
return "a 'file' block's 'file_data' must be a base64 data URL starting with 'data:'";
|
|
475
|
+
}
|
|
476
|
+
const match = FILE_BASE64_DATA_URL_PATTERN.exec(fileData);
|
|
477
|
+
if (!match || !match[1].includes("/")) {
|
|
478
|
+
return `malformed base64 data URL. Expected 'data:<mime-type>;base64,<base64>' with a valid type/subtype and standard base64 (no whitespace or URL-safe characters)`;
|
|
479
|
+
}
|
|
480
|
+
if (!isDecodableBase64(match[2])) {
|
|
481
|
+
return `base64 payload is not decodable. Check the padding and length \u2014 the data must be a multiple of 4 characters with at most two trailing '='`;
|
|
482
|
+
}
|
|
483
|
+
return void 0;
|
|
484
|
+
}
|
|
485
|
+
function describeFileUrlProblem(fileUrl) {
|
|
486
|
+
if (!isRecord(fileUrl)) return "a 'file_url' block requires an object 'file_url' field";
|
|
487
|
+
const url = typeof fileUrl.url === "string" ? fileUrl.url : void 0;
|
|
488
|
+
if (!url) return "a 'file_url' block requires a string 'file_url.url' field";
|
|
489
|
+
let protocol;
|
|
490
|
+
try {
|
|
491
|
+
protocol = new URL(url).protocol;
|
|
492
|
+
} catch {
|
|
493
|
+
return `'${url}' is not a valid URL. Use an http(s) URL`;
|
|
494
|
+
}
|
|
495
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
496
|
+
return `URL protocol '${protocol}' is not supported. Use an http(s) URL`;
|
|
497
|
+
}
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
450
500
|
function describeImageUrlProblem(imageUrl) {
|
|
451
501
|
const url = typeof imageUrl === "string" ? imageUrl : isRecord(imageUrl) ? imageUrl.url : void 0;
|
|
452
502
|
if (typeof url !== "string" || url === "") {
|
|
@@ -492,7 +542,35 @@ function normalizeMessageContent(content) {
|
|
|
492
542
|
if (content === null || content === void 0) return "";
|
|
493
543
|
return JSON.stringify(content);
|
|
494
544
|
}
|
|
495
|
-
|
|
545
|
+
async function processMessageContentFileBlocks(content, ctx, plugin) {
|
|
546
|
+
if (!Array.isArray(content)) return content;
|
|
547
|
+
const processed = [];
|
|
548
|
+
for (const block of content) {
|
|
549
|
+
processed.push(...await processFileBlockChain(block, ctx, plugin, 0));
|
|
550
|
+
}
|
|
551
|
+
return processed;
|
|
552
|
+
}
|
|
553
|
+
const MAX_FILE_PROCESSOR_CHAIN_DEPTH = 3;
|
|
554
|
+
async function processFileBlockChain(block, ctx, plugin, depth) {
|
|
555
|
+
if (!isRecord(block) || block.type !== "file" && block.type !== "file_url") {
|
|
556
|
+
return [block];
|
|
557
|
+
}
|
|
558
|
+
if (depth > MAX_FILE_PROCESSOR_CHAIN_DEPTH) {
|
|
559
|
+
return [block];
|
|
560
|
+
}
|
|
561
|
+
const result = await plugin.fileProcessorService.process(block, { ctx });
|
|
562
|
+
const results = Array.isArray(result) ? result : [result];
|
|
563
|
+
const next = [];
|
|
564
|
+
for (const item of results) {
|
|
565
|
+
if (isRecord(item) && (item.type === "file" || item.type === "file_url")) {
|
|
566
|
+
next.push(...await processFileBlockChain(item, ctx, plugin, depth + 1));
|
|
567
|
+
} else {
|
|
568
|
+
next.push(item);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return next;
|
|
572
|
+
}
|
|
573
|
+
const GATEWAY_MANAGED_PARAMETERS = /* @__PURE__ */ new Set(["model", "messages", "prompt", "tools", "tool_choice", "stream", "n"]);
|
|
496
574
|
function getProviderRequestParameters(body) {
|
|
497
575
|
return Object.fromEntries(
|
|
498
576
|
Object.entries(body).filter(([name, value]) => !GATEWAY_MANAGED_PARAMETERS.has(name) && value !== void 0)
|
|
@@ -562,6 +640,7 @@ function serializeToolArguments(value) {
|
|
|
562
640
|
// Annotate the CommonJS export names for ESM import in node:
|
|
563
641
|
0 && (module.exports = {
|
|
564
642
|
applyProviderRequestParameters,
|
|
643
|
+
extractFinishReason,
|
|
565
644
|
findContentBlockProblem,
|
|
566
645
|
findMessageProblem,
|
|
567
646
|
getProviderRequestParameters,
|
|
@@ -33,6 +33,8 @@ 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
35
|
var import_streaming = require("../utils/streaming");
|
|
36
|
+
var import_chat_completions = require("./chat-completions");
|
|
37
|
+
var import_request_cache = require("../utils/request-cache");
|
|
36
38
|
var import_usage = require("../usage");
|
|
37
39
|
var import_billing = require("../billing");
|
|
38
40
|
var import_direct_llm_context = require("../utils/direct-llm-context");
|
|
@@ -90,7 +92,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
90
92
|
);
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
|
-
const config = await
|
|
95
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
94
96
|
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
|
|
95
97
|
return;
|
|
96
98
|
}
|
|
@@ -109,19 +111,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
109
111
|
if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
|
|
110
112
|
if (body.stop !== void 0) modelOptions.stop = body.stop;
|
|
111
113
|
const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
|
|
112
|
-
const messages = [];
|
|
113
|
-
if (config == null ? void 0 : config.defaultAiEmployee) {
|
|
114
|
-
const employee = await ctx.db.getRepository("aiEmployees").findOne({
|
|
115
|
-
filter: { username: config.defaultAiEmployee }
|
|
116
|
-
});
|
|
117
|
-
if (employee) {
|
|
118
|
-
const systemPrompt = employee.about || employee.defaultPrompt || "";
|
|
119
|
-
if (systemPrompt) {
|
|
120
|
-
messages.push({ role: "system", content: systemPrompt });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
messages.push({ role: "user", content: prompt });
|
|
114
|
+
const messages = [{ role: "user", content: prompt }];
|
|
125
115
|
const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
|
|
126
116
|
serviceName: service.name,
|
|
127
117
|
modelId,
|
|
@@ -141,6 +131,8 @@ async function handleCompletions(ctx, plugin) {
|
|
|
141
131
|
]);
|
|
142
132
|
const completionId = (0, import_openai_format.generateCompletionId)().replace("chatcmpl-", "cmpl-");
|
|
143
133
|
const chatModel = provider.createModel();
|
|
134
|
+
const providerRequestParameters = (0, import_chat_completions.getProviderRequestParameters)(body);
|
|
135
|
+
(0, import_chat_completions.applyProviderRequestParameters)(chatModel, providerRequestParameters);
|
|
144
136
|
(0, import_billing.markLlmProviderAttempted)(ctx);
|
|
145
137
|
if (stream) {
|
|
146
138
|
await handleStreamingTextCompletion(
|
|
@@ -149,10 +141,18 @@ async function handleCompletions(ctx, plugin) {
|
|
|
149
141
|
langchainMessages,
|
|
150
142
|
completionId,
|
|
151
143
|
body.model,
|
|
152
|
-
body.stream_options
|
|
144
|
+
body.stream_options,
|
|
145
|
+
providerRequestParameters
|
|
153
146
|
);
|
|
154
147
|
} else {
|
|
155
|
-
await handleNonStreamingTextCompletion(
|
|
148
|
+
await handleNonStreamingTextCompletion(
|
|
149
|
+
ctx,
|
|
150
|
+
chatModel,
|
|
151
|
+
langchainMessages,
|
|
152
|
+
completionId,
|
|
153
|
+
body.model,
|
|
154
|
+
providerRequestParameters
|
|
155
|
+
);
|
|
156
156
|
}
|
|
157
157
|
} catch (err) {
|
|
158
158
|
ctx.log.error("AI API completions error:", err);
|
|
@@ -170,8 +170,8 @@ async function handleCompletions(ctx, plugin) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
async function handleNonStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
|
|
174
|
-
const result = await chatModel.invoke(messages);
|
|
173
|
+
async function handleNonStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
|
|
174
|
+
const result = await chatModel.invoke(messages, providerRequestParameters);
|
|
175
175
|
let text = "";
|
|
176
176
|
if (typeof result.content === "string") {
|
|
177
177
|
text = result.content;
|
|
@@ -179,10 +179,15 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
179
179
|
const textPart = result.content.find((c) => c.type === "text");
|
|
180
180
|
text = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
|
|
181
181
|
}
|
|
182
|
-
const usage = (0, import_usage.setAiApiUsageResult)(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
const usage = (0, import_usage.setAiApiUsageResult)(
|
|
183
|
+
ctx,
|
|
184
|
+
result.usage_metadata,
|
|
185
|
+
{
|
|
186
|
+
gatewayResponseId: completionId,
|
|
187
|
+
providerRequestId: (0, import_usage.extractProviderRequestId)(result)
|
|
188
|
+
},
|
|
189
|
+
result.response_metadata
|
|
190
|
+
);
|
|
186
191
|
ctx.status = 200;
|
|
187
192
|
ctx.body = {
|
|
188
193
|
id: completionId,
|
|
@@ -195,13 +200,18 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
195
200
|
text,
|
|
196
201
|
index: 0,
|
|
197
202
|
logprobs: null,
|
|
198
|
-
finish_reason: "stop"
|
|
203
|
+
finish_reason: (0, import_chat_completions.extractFinishReason)(result) ?? "stop"
|
|
199
204
|
}
|
|
200
205
|
],
|
|
201
|
-
usage: usage
|
|
206
|
+
usage: usage ? {
|
|
207
|
+
prompt_tokens: usage.prompt_tokens,
|
|
208
|
+
completion_tokens: usage.completion_tokens,
|
|
209
|
+
total_tokens: usage.total_tokens,
|
|
210
|
+
prompt_tokens_details: { cached_tokens: usage.prompt_cache_tokens ?? null }
|
|
211
|
+
} : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: null } }
|
|
202
212
|
};
|
|
203
213
|
}
|
|
204
|
-
async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions) {
|
|
214
|
+
async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions, providerRequestParameters) {
|
|
205
215
|
ctx.set({
|
|
206
216
|
"Content-Type": "text/event-stream",
|
|
207
217
|
"Cache-Control": "no-cache",
|
|
@@ -211,9 +221,12 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
211
221
|
ctx.status = 200;
|
|
212
222
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
213
223
|
let usage;
|
|
224
|
+
let usageResponseMetadata;
|
|
214
225
|
let providerRequestId;
|
|
226
|
+
let providerFinishReason;
|
|
215
227
|
try {
|
|
216
228
|
const stream = await chatModel.stream(messages, {
|
|
229
|
+
...providerRequestParameters,
|
|
217
230
|
stream_options: { ...streamOptions, include_usage: true },
|
|
218
231
|
signal: requestAbort.signal
|
|
219
232
|
});
|
|
@@ -249,8 +262,14 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
249
262
|
);
|
|
250
263
|
}
|
|
251
264
|
if (chunk.usage_metadata) {
|
|
252
|
-
|
|
265
|
+
const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
|
|
266
|
+
if (normalized) {
|
|
267
|
+
usage = normalized;
|
|
268
|
+
usageResponseMetadata = chunk.response_metadata;
|
|
269
|
+
}
|
|
253
270
|
}
|
|
271
|
+
const chunkFinishReason = (0, import_chat_completions.extractFinishReason)(chunk);
|
|
272
|
+
if (chunkFinishReason) providerFinishReason = chunkFinishReason;
|
|
254
273
|
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
255
274
|
}
|
|
256
275
|
await (0, import_streaming.writeResponse)(
|
|
@@ -266,7 +285,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
266
285
|
text: "",
|
|
267
286
|
index: 0,
|
|
268
287
|
logprobs: null,
|
|
269
|
-
finish_reason: "stop"
|
|
288
|
+
finish_reason: providerFinishReason ?? "stop"
|
|
270
289
|
}
|
|
271
290
|
],
|
|
272
291
|
usage: null
|
|
@@ -286,7 +305,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
286
305
|
);
|
|
287
306
|
}
|
|
288
307
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
289
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
308
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
290
309
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
291
310
|
} catch (err) {
|
|
292
311
|
const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
|
|
@@ -302,7 +321,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
302
321
|
})
|
|
303
322
|
);
|
|
304
323
|
}
|
|
305
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
324
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
306
325
|
ctx.state.aiApiStreamResult = {
|
|
307
326
|
succeeded: false,
|
|
308
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") {
|