plugin-ai-api 1.0.20 → 1.0.23
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/123.e6fe04c856ce6417.js +10 -0
- package/dist/client/286.01c0e3c5fff3cccb.js +10 -0
- package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
- package/dist/client/562.17a0a299d2e5152c.js +10 -0
- package/dist/client/{757.71e30f2a1306562d.js → 757.a01403fb7a1bea01.js} +1 -1
- package/dist/client/{902.4238b04ac667c30a.js → 902.92e1daaf1ab16ebf.js} +1 -1
- package/dist/client/{97.37cda285d7da3a26.js → 97.72979a11a067a7c9.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/123.05f1f649923f93eb.js +10 -0
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
- package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
- package/dist/client-v2/{757.c377e2f2b054d89d.js → 757.a117ce1cf7119cea.js} +1 -1
- package/dist/client-v2/{902.d40d7bda106124c8.js → 902.9054d990ddc223ac.js} +1 -1
- package/dist/client-v2/952.94100128b7757f56.js +10 -0
- package/dist/client-v2/{97.fc922c37ced86831.js → 97.29c663318eebbd57.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +39 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +39 -9
- package/dist/locale/vi-VN.json +31 -1
- package/dist/locale/zh-CN.json +31 -1
- package/dist/server/collections/ai-api-config.js +6 -0
- package/dist/server/collections/ai-api-model-metadata.js +83 -0
- package/dist/server/collections/ai-api-user-permissions.js +67 -0
- package/dist/server/plugin.js +45 -1
- package/dist/server/resource/ai-api-config.js +17 -0
- package/dist/server/resource/ai-api-user-permissions.js +75 -0
- package/dist/server/routes/agent-completions.js +67 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +174 -20
- package/dist/server/routes/completions.js +41 -21
- package/dist/server/routes/embeddings.js +6 -14
- package/dist/server/routes/models.js +102 -20
- package/dist/server/routes/router.js +94 -22
- package/dist/server/usage.js +2 -0
- package/dist/server/utils/app-observability.js +110 -0
- package/dist/server/utils/openai-format.js +17 -3
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/utils/user-permissions.js +160 -0
- package/dist/server/validation.js +18 -0
- package/dist/swagger.js +36 -4
- package/package.json +2 -2
- package/src/client/__tests__/settings-registration.test.tsx +69 -0
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +28 -8
- package/src/client-v2/__tests__/settings-registration.test.tsx +87 -0
- package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
- package/src/client-v2/locale.ts +21 -1
- package/src/client-v2/pages/GeneralPage.tsx +13 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
- package/src/client-v2/pages/RolePermissionsTab.tsx +14 -0
- package/src/client-v2/pages/UserPermissionsPage.tsx +322 -0
- package/src/client-v2/plugin.tsx +50 -1
- package/src/constants.ts +28 -0
- package/src/locale/en-US.json +39 -9
- package/src/locale/vi-VN.json +31 -1
- package/src/locale/zh-CN.json +31 -1
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/models.test.ts +116 -0
- package/src/server/__tests__/openai-format.test.ts +52 -1
- package/src/server/__tests__/permission-sync.test.ts +109 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -0
- package/src/server/__tests__/usage-route.test.ts +213 -0
- package/src/server/__tests__/user-permissions-resource.test.ts +66 -0
- package/src/server/__tests__/user-permissions.test.ts +284 -0
- package/src/server/collections/ai-api-config.ts +6 -0
- package/src/server/collections/ai-api-model-metadata.ts +72 -0
- package/src/server/collections/ai-api-user-permissions.ts +46 -0
- package/src/server/plugin.ts +65 -4
- package/src/server/resource/ai-api-config.ts +23 -0
- package/src/server/resource/ai-api-user-permissions.ts +76 -0
- package/src/server/routes/agent-completions.ts +84 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +294 -20
- package/src/server/routes/completions.ts +54 -20
- package/src/server/routes/embeddings.ts +10 -15
- package/src/server/routes/models.ts +318 -195
- package/src/server/routes/router.ts +136 -26
- package/src/server/usage.ts +2 -0
- package/src/server/utils/app-observability.ts +105 -0
- package/src/server/utils/openai-format.ts +26 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/utils/user-permissions.ts +218 -0
- package/src/server/validation.ts +27 -0
- package/src/swagger.ts +47 -4
- package/dist/client/302.25edd5d75460acbf.js +0 -10
- package/dist/client/778.5c452944cb747975.js +0 -10
- package/dist/client-v2/302.9b27a263901d54d8.js +0 -10
- package/src/client/AiApiConfigPage.tsx +0 -309
|
@@ -31,11 +31,12 @@ __export(completions_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(completions_exports);
|
|
32
32
|
var import_openai_format = require("../utils/openai-format");
|
|
33
33
|
var import_resolve_service = require("../utils/resolve-service");
|
|
34
|
+
var import_user_permissions = require("../utils/user-permissions");
|
|
34
35
|
var import_streaming = require("../utils/streaming");
|
|
35
36
|
var import_usage = require("../usage");
|
|
36
37
|
var import_billing = require("../billing");
|
|
38
|
+
var import_app_observability = require("../utils/app-observability");
|
|
37
39
|
async function handleCompletions(ctx, plugin) {
|
|
38
|
-
var _a;
|
|
39
40
|
const body = ctx.request.body;
|
|
40
41
|
if (!(body == null ? void 0 : body.model)) {
|
|
41
42
|
ctx.status = 400;
|
|
@@ -88,20 +89,8 @@ async function handleCompletions(ctx, plugin) {
|
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
91
|
-
if ((
|
|
92
|
-
|
|
93
|
-
const serviceTitle = service.title;
|
|
94
|
-
const isAllowed = config.enabledLlmServices.some((s) => s === serviceName || s === serviceTitle);
|
|
95
|
-
if (!isAllowed) {
|
|
96
|
-
ctx.status = 403;
|
|
97
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
98
|
-
403,
|
|
99
|
-
`LLM service '${service.title || service.name}' is not enabled for API access`,
|
|
100
|
-
"invalid_request_error",
|
|
101
|
-
"model_not_available"
|
|
102
|
-
);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
92
|
+
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
|
|
93
|
+
return;
|
|
105
94
|
}
|
|
106
95
|
const providerMeta = aiPlugin.aiManager.llmProviders.get(service.provider);
|
|
107
96
|
if (!providerMeta) {
|
|
@@ -142,7 +131,14 @@ async function handleCompletions(ctx, plugin) {
|
|
|
142
131
|
const chatModel = provider.createModel();
|
|
143
132
|
(0, import_billing.markLlmProviderAttempted)(ctx);
|
|
144
133
|
if (stream) {
|
|
145
|
-
await handleStreamingTextCompletion(
|
|
134
|
+
await handleStreamingTextCompletion(
|
|
135
|
+
ctx,
|
|
136
|
+
chatModel,
|
|
137
|
+
langchainMessages,
|
|
138
|
+
completionId,
|
|
139
|
+
body.model,
|
|
140
|
+
body.stream_options
|
|
141
|
+
);
|
|
146
142
|
} else {
|
|
147
143
|
await handleNonStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
|
|
148
144
|
}
|
|
@@ -192,7 +188,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
192
188
|
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
193
189
|
};
|
|
194
190
|
}
|
|
195
|
-
async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
|
|
191
|
+
async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions) {
|
|
196
192
|
ctx.set({
|
|
197
193
|
"Content-Type": "text/event-stream",
|
|
198
194
|
"Cache-Control": "no-cache",
|
|
@@ -204,7 +200,10 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
204
200
|
let usage;
|
|
205
201
|
let providerRequestId;
|
|
206
202
|
try {
|
|
207
|
-
const stream = await chatModel.stream(messages, {
|
|
203
|
+
const stream = await chatModel.stream(messages, {
|
|
204
|
+
stream_options: { ...streamOptions, include_usage: true },
|
|
205
|
+
signal: requestAbort.signal
|
|
206
|
+
});
|
|
208
207
|
for await (const chunk of stream) {
|
|
209
208
|
if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
|
|
210
209
|
let text = "";
|
|
@@ -215,6 +214,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
215
214
|
text = (textPart == null ? void 0 : textPart.text) || "";
|
|
216
215
|
}
|
|
217
216
|
if (text) {
|
|
217
|
+
(0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
|
|
218
218
|
await (0, import_streaming.writeResponse)(
|
|
219
219
|
ctx,
|
|
220
220
|
(0, import_openai_format.formatSSE)({
|
|
@@ -230,7 +230,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
230
230
|
logprobs: null,
|
|
231
231
|
finish_reason: null
|
|
232
232
|
}
|
|
233
|
-
]
|
|
233
|
+
],
|
|
234
|
+
usage: null
|
|
234
235
|
})
|
|
235
236
|
);
|
|
236
237
|
}
|
|
@@ -254,13 +255,28 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
254
255
|
logprobs: null,
|
|
255
256
|
finish_reason: "stop"
|
|
256
257
|
}
|
|
257
|
-
]
|
|
258
|
+
],
|
|
259
|
+
usage: null
|
|
258
260
|
})
|
|
259
261
|
);
|
|
262
|
+
if (usage) {
|
|
263
|
+
await (0, import_streaming.writeResponse)(
|
|
264
|
+
ctx,
|
|
265
|
+
(0, import_openai_format.formatSSE)(
|
|
266
|
+
(0, import_openai_format.toOpenAIUsageChunk)({
|
|
267
|
+
id: completionId,
|
|
268
|
+
model: modelName,
|
|
269
|
+
object: "text_completion",
|
|
270
|
+
usage
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
260
275
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
261
276
|
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
262
277
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
263
278
|
} catch (err) {
|
|
279
|
+
const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
|
|
264
280
|
ctx.log.error("AI API completions streaming error:", err);
|
|
265
281
|
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
266
282
|
await (0, import_streaming.writeResponse)(
|
|
@@ -274,7 +290,11 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
274
290
|
);
|
|
275
291
|
}
|
|
276
292
|
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
277
|
-
ctx.state.aiApiStreamResult = {
|
|
293
|
+
ctx.state.aiApiStreamResult = {
|
|
294
|
+
succeeded: false,
|
|
295
|
+
id: completionId,
|
|
296
|
+
errorCode: cancelled ? "client_disconnected" : "stream_error"
|
|
297
|
+
};
|
|
278
298
|
} finally {
|
|
279
299
|
requestAbort.dispose();
|
|
280
300
|
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
@@ -31,9 +31,9 @@ __export(embeddings_exports, {
|
|
|
31
31
|
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
|
+
var import_user_permissions = require("../utils/user-permissions");
|
|
34
35
|
var import_usage = require("../usage");
|
|
35
36
|
async function handleEmbeddings(ctx, plugin) {
|
|
36
|
-
var _a;
|
|
37
37
|
const body = ctx.request.body;
|
|
38
38
|
if (!(body == null ? void 0 : body.model)) {
|
|
39
39
|
ctx.status = 400;
|
|
@@ -102,23 +102,15 @@ async function handleEmbeddings(ctx, plugin) {
|
|
|
102
102
|
);
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
|
+
let globalEnabledServices = [];
|
|
105
106
|
try {
|
|
106
107
|
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
107
|
-
|
|
108
|
-
const allowed = config.enabledLlmServices.some((s) => s === service.name || s === service.title);
|
|
109
|
-
if (!allowed) {
|
|
110
|
-
ctx.status = 403;
|
|
111
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
112
|
-
403,
|
|
113
|
-
`LLM service '${service.title || service.name}' is not enabled for API access`,
|
|
114
|
-
"invalid_request_error",
|
|
115
|
-
"model_not_available"
|
|
116
|
-
);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
108
|
+
globalEnabledServices = (config == null ? void 0 : config.enabledLlmServices) ?? [];
|
|
120
109
|
} catch {
|
|
121
110
|
}
|
|
111
|
+
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, globalEnabledServices, service, modelId)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
122
114
|
const aiPlugin = ctx.app.pm.get("ai");
|
|
123
115
|
if (!aiPlugin) {
|
|
124
116
|
ctx.status = 500;
|
|
@@ -26,11 +26,13 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
27
|
var models_exports = {};
|
|
28
28
|
__export(models_exports, {
|
|
29
|
+
buildModelObject: () => buildModelObject,
|
|
29
30
|
handleGetModel: () => handleGetModel,
|
|
30
31
|
handleListModels: () => handleListModels
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(models_exports);
|
|
33
34
|
var import_openai_format = require("../utils/openai-format");
|
|
35
|
+
var import_user_permissions = require("../utils/user-permissions");
|
|
34
36
|
async function handleListModels(ctx, plugin) {
|
|
35
37
|
var _a;
|
|
36
38
|
try {
|
|
@@ -49,21 +51,25 @@ async function handleListModels(ctx, plugin) {
|
|
|
49
51
|
filter,
|
|
50
52
|
sort: "sort"
|
|
51
53
|
});
|
|
54
|
+
const scope = await (0, import_user_permissions.resolveUserAccessScope)(ctx);
|
|
55
|
+
if (scope.lookupFailed) {
|
|
56
|
+
respondPermissionCheckFailed(ctx);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const metadataMap = await loadModelMetadata(ctx);
|
|
52
60
|
const now = Math.floor(Date.now() / 1e3);
|
|
53
61
|
const models = [];
|
|
54
62
|
for (const service of services) {
|
|
55
63
|
if (service.enabled === false) continue;
|
|
64
|
+
if (!(0, import_user_permissions.isServiceAllowed)(scope, config == null ? void 0 : config.enabledLlmServices, service)) continue;
|
|
56
65
|
const enabledModels = resolveEnabledModels(service);
|
|
57
66
|
const serviceLabel = service.title || service.name;
|
|
58
67
|
for (const model of enabledModels) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
created: now,
|
|
65
|
-
owned_by: serviceLabel
|
|
66
|
-
});
|
|
68
|
+
const fullId = `${service.name}/${model.value}`;
|
|
69
|
+
if (!(0, import_user_permissions.isModelAllowed)(scope, fullId)) continue;
|
|
70
|
+
const meta = metadataMap.get(fullId);
|
|
71
|
+
if (meta && meta.enabled === false) continue;
|
|
72
|
+
models.push(buildModelObject(fullId, now, serviceLabel, meta));
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
ctx.status = 200;
|
|
@@ -89,21 +95,26 @@ async function handleGetModel(ctx, modelId, plugin) {
|
|
|
89
95
|
filter,
|
|
90
96
|
sort: "sort"
|
|
91
97
|
});
|
|
98
|
+
const scope = await (0, import_user_permissions.resolveUserAccessScope)(ctx);
|
|
99
|
+
if (scope.lookupFailed) {
|
|
100
|
+
respondPermissionCheckFailed(ctx);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const metadataMap = await loadModelMetadata(ctx);
|
|
92
104
|
const now = Math.floor(Date.now() / 1e3);
|
|
93
105
|
let found = null;
|
|
94
106
|
for (const service of services) {
|
|
95
107
|
if (service.enabled === false) continue;
|
|
108
|
+
if (!(0, import_user_permissions.isServiceAllowed)(scope, config == null ? void 0 : config.enabledLlmServices, service)) continue;
|
|
96
109
|
const enabledModels = resolveEnabledModels(service);
|
|
97
110
|
const serviceLabel = service.title || service.name;
|
|
98
111
|
for (const model of enabledModels) {
|
|
99
112
|
const fullId = `${service.name}/${model.value}`;
|
|
100
113
|
if (fullId === modelId || model.value === modelId) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
owned_by: serviceLabel
|
|
106
|
-
};
|
|
114
|
+
if (!(0, import_user_permissions.isModelAllowed)(scope, fullId)) continue;
|
|
115
|
+
const meta = metadataMap.get(fullId);
|
|
116
|
+
if (meta && meta.enabled === false) continue;
|
|
117
|
+
found = buildModelObject(fullId, now, serviceLabel, meta);
|
|
107
118
|
break;
|
|
108
119
|
}
|
|
109
120
|
}
|
|
@@ -122,6 +133,70 @@ async function handleGetModel(ctx, modelId, plugin) {
|
|
|
122
133
|
ctx.body = (0, import_openai_format.toOpenAIError)(500, "Failed to retrieve model", "server_error");
|
|
123
134
|
}
|
|
124
135
|
}
|
|
136
|
+
function respondPermissionCheckFailed(ctx) {
|
|
137
|
+
ctx.status = 503;
|
|
138
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
139
|
+
503,
|
|
140
|
+
"Unable to verify LLM permissions for this user. Please retry shortly.",
|
|
141
|
+
"service_unavailable",
|
|
142
|
+
"permission_check_failed"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function loadModelMetadata(ctx) {
|
|
146
|
+
var _a, _b;
|
|
147
|
+
const map = /* @__PURE__ */ new Map();
|
|
148
|
+
try {
|
|
149
|
+
const rows = await ctx.db.getRepository("aiApiModelMetadata").find();
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
const service = row.get("llmService");
|
|
152
|
+
const model = row.get("model");
|
|
153
|
+
if (!service || !model) continue;
|
|
154
|
+
map.set(`${service}/${model}`, {
|
|
155
|
+
contextWindow: row.get("contextWindow"),
|
|
156
|
+
maxCompletionTokens: row.get("maxCompletionTokens"),
|
|
157
|
+
ownedByOverride: row.get("ownedByOverride"),
|
|
158
|
+
displayName: row.get("displayName"),
|
|
159
|
+
description: row.get("description"),
|
|
160
|
+
enabled: row.get("enabled")
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
(_b = (_a = ctx.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, "AI API model metadata unavailable, skipping overrides:", err);
|
|
165
|
+
}
|
|
166
|
+
return map;
|
|
167
|
+
}
|
|
168
|
+
function buildModelObject(fullId, created, serviceLabel, meta) {
|
|
169
|
+
const model = {
|
|
170
|
+
id: fullId,
|
|
171
|
+
object: "model",
|
|
172
|
+
created,
|
|
173
|
+
owned_by: (meta == null ? void 0 : meta.ownedByOverride) || serviceLabel
|
|
174
|
+
};
|
|
175
|
+
const contextWindow = toPositiveInt(meta == null ? void 0 : meta.contextWindow);
|
|
176
|
+
if (contextWindow !== null) {
|
|
177
|
+
model.context_window = contextWindow;
|
|
178
|
+
model.context_length = contextWindow;
|
|
179
|
+
}
|
|
180
|
+
const maxCompletionTokens = toPositiveInt(meta == null ? void 0 : meta.maxCompletionTokens);
|
|
181
|
+
if (maxCompletionTokens !== null) {
|
|
182
|
+
model.max_completion_tokens = maxCompletionTokens;
|
|
183
|
+
}
|
|
184
|
+
if (meta == null ? void 0 : meta.displayName) {
|
|
185
|
+
model.display_name = meta.displayName;
|
|
186
|
+
model.name = meta.displayName;
|
|
187
|
+
}
|
|
188
|
+
if (meta == null ? void 0 : meta.description) {
|
|
189
|
+
model.description = meta.description;
|
|
190
|
+
}
|
|
191
|
+
if (meta) {
|
|
192
|
+
model.active = meta.enabled !== false;
|
|
193
|
+
}
|
|
194
|
+
return model;
|
|
195
|
+
}
|
|
196
|
+
function toPositiveInt(value) {
|
|
197
|
+
const n = Number(value);
|
|
198
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
199
|
+
}
|
|
125
200
|
async function getPluginConfig(ctx) {
|
|
126
201
|
return ctx.db.getRepository("aiApiConfig").findOne();
|
|
127
202
|
}
|
|
@@ -142,16 +217,23 @@ function resolveEnabledModels(service) {
|
|
|
142
217
|
return getRecommendedModelsForProvider(service.provider);
|
|
143
218
|
}
|
|
144
219
|
function getRecommendedModelsForProvider(provider) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
220
|
+
const modulePaths = [
|
|
221
|
+
"@nocobase/plugin-ai/dist/common/recommended-models",
|
|
222
|
+
"@nocobase/plugin-ai/src/common/recommended-models"
|
|
223
|
+
];
|
|
224
|
+
for (const modulePath of modulePaths) {
|
|
225
|
+
try {
|
|
226
|
+
const { getRecommendedModels } = require(modulePath);
|
|
227
|
+
const models = getRecommendedModels(provider);
|
|
228
|
+
return Array.isArray(models) ? models : [];
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
151
231
|
}
|
|
232
|
+
return [];
|
|
152
233
|
}
|
|
153
234
|
// Annotate the CommonJS export names for ESM import in node:
|
|
154
235
|
0 && (module.exports = {
|
|
236
|
+
buildModelObject,
|
|
155
237
|
handleGetModel,
|
|
156
238
|
handleListModels
|
|
157
239
|
});
|
|
@@ -36,7 +36,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
var router_exports = {};
|
|
38
38
|
__export(router_exports, {
|
|
39
|
-
|
|
39
|
+
AI_LLM_PREFIX: () => AI_LLM_PREFIX,
|
|
40
|
+
MAX_REQUEST_BODY_MB_LIMIT: () => MAX_REQUEST_BODY_MB_LIMIT,
|
|
41
|
+
createAiLlmRouter: () => createAiLlmRouter,
|
|
42
|
+
getRawBody: () => getRawBody,
|
|
43
|
+
normalizeMaxRequestBodyMb: () => normalizeMaxRequestBodyMb
|
|
40
44
|
});
|
|
41
45
|
module.exports = __toCommonJS(router_exports);
|
|
42
46
|
var import_crypto = __toESM(require("crypto"));
|
|
@@ -52,7 +56,9 @@ var import_role_permission = require("../middleware/role-permission");
|
|
|
52
56
|
var import_usage = require("../usage");
|
|
53
57
|
var import_streaming = require("../utils/streaming");
|
|
54
58
|
var import_billing = require("../billing");
|
|
59
|
+
var import_app_observability = require("../utils/app-observability");
|
|
55
60
|
const API_PREFIX = "/api/ai-llm/v1";
|
|
61
|
+
const AI_LLM_PREFIX = API_PREFIX;
|
|
56
62
|
function createAiLlmRouter(plugin) {
|
|
57
63
|
const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
|
|
58
64
|
return async (ctx, next) => {
|
|
@@ -77,18 +83,6 @@ function createAiLlmRouter(plugin) {
|
|
|
77
83
|
}
|
|
78
84
|
const requestId = `req-${import_crypto.default.randomBytes(12).toString("hex")}`;
|
|
79
85
|
ctx.set("X-Request-Id", requestId);
|
|
80
|
-
if (method === "POST" && !ctx.request.body) {
|
|
81
|
-
try {
|
|
82
|
-
const rawBody = await getRawBody(ctx);
|
|
83
|
-
ctx.request.body = JSON.parse(rawBody);
|
|
84
|
-
} catch (bodyErr) {
|
|
85
|
-
const status = bodyErr && typeof bodyErr === "object" && "statusCode" in bodyErr && bodyErr.statusCode === 413 ? 413 : 400;
|
|
86
|
-
const message = status === 413 ? "Request body too large (max 10 MB)" : "Invalid JSON in request body";
|
|
87
|
-
ctx.status = status;
|
|
88
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(status, message, "invalid_request_error");
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
86
|
const isAuth = await (0, import_auth.authenticateBearer)(ctx);
|
|
93
87
|
if (!isAuth) {
|
|
94
88
|
logRequest(ctx, requestId, "-", "auth_failed", 0);
|
|
@@ -104,6 +98,29 @@ function createAiLlmRouter(plugin) {
|
|
|
104
98
|
logRequest(ctx, requestId, "-", "rate_limited", 0);
|
|
105
99
|
return;
|
|
106
100
|
}
|
|
101
|
+
if (method === "POST" && !ctx.request.body) {
|
|
102
|
+
const maxBodyBytes = await resolveMaxBodyBytes(ctx);
|
|
103
|
+
const declaredLength = Number(ctx.get("Content-Length"));
|
|
104
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
105
|
+
respondBodyTooLarge(ctx, maxBodyBytes);
|
|
106
|
+
logRequest(ctx, requestId, "-", "body_too_large", 0);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const rawBody = await getRawBody(ctx, maxBodyBytes);
|
|
111
|
+
ctx.request.body = JSON.parse(rawBody);
|
|
112
|
+
} catch (bodyErr) {
|
|
113
|
+
const tooLarge = bodyErr && typeof bodyErr === "object" && "statusCode" in bodyErr && bodyErr.statusCode === 413;
|
|
114
|
+
if (tooLarge) {
|
|
115
|
+
respondBodyTooLarge(ctx, maxBodyBytes);
|
|
116
|
+
logRequest(ctx, requestId, "-", "body_too_large", 0);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.status = 400;
|
|
120
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(400, "Invalid JSON in request body", "invalid_request_error");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
107
124
|
const requestBody = ctx.request.body || {};
|
|
108
125
|
const model = requestBody.model === void 0 || requestBody.model === null ? "-" : String(requestBody.model);
|
|
109
126
|
const isUsageEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/embeddings");
|
|
@@ -121,6 +138,16 @@ function createAiLlmRouter(plugin) {
|
|
|
121
138
|
};
|
|
122
139
|
}
|
|
123
140
|
const t0 = Date.now();
|
|
141
|
+
if (isUsageEndpoint) {
|
|
142
|
+
const service = subPath === "/embeddings" ? "llm.embedding" : resolvedMode === "agent" ? "llm.agent" : subPath === "/completions" ? "llm.completion" : "llm.chat";
|
|
143
|
+
(0, import_app_observability.startAiApiObservation)(ctx, {
|
|
144
|
+
service,
|
|
145
|
+
operation: subPath,
|
|
146
|
+
streaming,
|
|
147
|
+
model: model === "-" ? void 0 : model,
|
|
148
|
+
mode: resolvedMode
|
|
149
|
+
});
|
|
150
|
+
}
|
|
124
151
|
let usageId;
|
|
125
152
|
try {
|
|
126
153
|
usageId = isUsageEndpoint ? await (0, import_usage.startUsageRecord)(ctx, requestId, subPath, model, streaming, resolvedMode) : void 0;
|
|
@@ -225,24 +252,65 @@ function createAiLlmRouter(plugin) {
|
|
|
225
252
|
ctx.log.error("AI API quota reservation could not be finalized:", billingError);
|
|
226
253
|
}
|
|
227
254
|
}
|
|
255
|
+
if (isUsageEndpoint) {
|
|
256
|
+
const streamResult = ctx.state.aiApiStreamResult;
|
|
257
|
+
(0, import_app_observability.finishAiApiObservation)(ctx, {
|
|
258
|
+
status: (streamResult == null ? void 0 : streamResult.errorCode) === "client_disconnected" ? "cancelled" : streamResult ? streamResult.succeeded ? "succeeded" : "failed" : ctx.status >= 200 && ctx.status < 400 ? "succeeded" : ctx.status >= 500 ? "failed" : "rejected",
|
|
259
|
+
errorCode: streamResult == null ? void 0 : streamResult.errorCode
|
|
260
|
+
});
|
|
261
|
+
}
|
|
228
262
|
}
|
|
229
263
|
};
|
|
230
264
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
265
|
+
const DEFAULT_MAX_BODY_MB = 10;
|
|
266
|
+
const MAX_REQUEST_BODY_MB_LIMIT = 100;
|
|
267
|
+
function normalizeMaxRequestBodyMb(value) {
|
|
268
|
+
const mb = Number(value);
|
|
269
|
+
if (!Number.isSafeInteger(mb) || mb <= 0) return DEFAULT_MAX_BODY_MB;
|
|
270
|
+
return Math.min(mb, MAX_REQUEST_BODY_MB_LIMIT);
|
|
271
|
+
}
|
|
272
|
+
async function resolveMaxBodyBytes(ctx) {
|
|
273
|
+
var _a, _b;
|
|
274
|
+
let configuredMb;
|
|
275
|
+
try {
|
|
276
|
+
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
277
|
+
configuredMb = config == null ? void 0 : config.get("maxRequestBodyMb");
|
|
278
|
+
} catch (err) {
|
|
279
|
+
(_b = (_a = ctx.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, "AI API: could not read maxRequestBodyMb, using default:", err);
|
|
280
|
+
}
|
|
281
|
+
return normalizeMaxRequestBodyMb(configuredMb) * 1024 * 1024;
|
|
282
|
+
}
|
|
283
|
+
function formatMb(bytes) {
|
|
284
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
285
|
+
}
|
|
286
|
+
function respondBodyTooLarge(ctx, maxBodyBytes) {
|
|
287
|
+
ctx.status = 413;
|
|
288
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
289
|
+
413,
|
|
290
|
+
`Request body too large (max ${formatMb(maxBodyBytes)}). Inline base64 images inflate payloads by ~33%; raise "Max request body size" in Settings \u2192 AI API Gateway if needed.`,
|
|
291
|
+
"invalid_request_error"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
function getRawBody(ctx, maxBodyBytes) {
|
|
233
295
|
return new Promise((resolve, reject) => {
|
|
234
|
-
|
|
296
|
+
const chunks = [];
|
|
235
297
|
let byteCount = 0;
|
|
298
|
+
let aborted = false;
|
|
236
299
|
ctx.req.on("data", (chunk) => {
|
|
300
|
+
if (aborted) return;
|
|
237
301
|
byteCount += chunk.length;
|
|
238
|
-
if (byteCount >
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
if (byteCount > maxBodyBytes) {
|
|
303
|
+
aborted = true;
|
|
304
|
+
chunks.length = 0;
|
|
305
|
+
ctx.req.resume();
|
|
306
|
+
reject(Object.assign(new Error(`Request body too large (max ${formatMb(maxBodyBytes)})`), { statusCode: 413 }));
|
|
241
307
|
return;
|
|
242
308
|
}
|
|
243
|
-
|
|
309
|
+
chunks.push(chunk);
|
|
310
|
+
});
|
|
311
|
+
ctx.req.on("end", () => {
|
|
312
|
+
if (!aborted) resolve(Buffer.concat(chunks).toString("utf8"));
|
|
244
313
|
});
|
|
245
|
-
ctx.req.on("end", () => resolve(body));
|
|
246
314
|
ctx.req.on("error", reject);
|
|
247
315
|
});
|
|
248
316
|
}
|
|
@@ -277,5 +345,9 @@ function logRequest(ctx, requestId, model, status, durationMs) {
|
|
|
277
345
|
}
|
|
278
346
|
// Annotate the CommonJS export names for ESM import in node:
|
|
279
347
|
0 && (module.exports = {
|
|
280
|
-
|
|
348
|
+
AI_LLM_PREFIX,
|
|
349
|
+
MAX_REQUEST_BODY_MB_LIMIT,
|
|
350
|
+
createAiLlmRouter,
|
|
351
|
+
getRawBody,
|
|
352
|
+
normalizeMaxRequestBodyMb
|
|
281
353
|
});
|
package/dist/server/usage.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(usage_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(usage_exports);
|
|
37
37
|
var import_billing = require("./billing");
|
|
38
|
+
var import_app_observability = require("./utils/app-observability");
|
|
38
39
|
function getAiApiState(ctx) {
|
|
39
40
|
return ctx.state;
|
|
40
41
|
}
|
|
@@ -60,6 +61,7 @@ function normalizeUsage(value) {
|
|
|
60
61
|
function setAiApiUsageResult(ctx, value, metadata = {}) {
|
|
61
62
|
const usage = normalizeUsage(value);
|
|
62
63
|
getAiApiState(ctx).aiApiUsageResult = usage ? { source: "provider", usage, ...metadata } : { source: "unavailable", ...metadata };
|
|
64
|
+
(0, import_app_observability.addAiApiUsage)(ctx, usage);
|
|
63
65
|
return usage;
|
|
64
66
|
}
|
|
65
67
|
function setAiApiUsageUnavailable(ctx, gatewayResponseId) {
|
|
@@ -0,0 +1,110 @@
|
|
|
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 app_observability_exports = {};
|
|
28
|
+
__export(app_observability_exports, {
|
|
29
|
+
addAiApiUsage: () => addAiApiUsage,
|
|
30
|
+
finishAiApiObservation: () => finishAiApiObservation,
|
|
31
|
+
markAiApiFirstProviderOutput: () => markAiApiFirstProviderOutput,
|
|
32
|
+
setAiApiObservationOutcome: () => setAiApiObservationOutcome,
|
|
33
|
+
startAiApiObservation: () => startAiApiObservation
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(app_observability_exports);
|
|
36
|
+
const CONTRACT_SYMBOL = Symbol.for("nocobase.app-observability.contract");
|
|
37
|
+
const NOOP_HANDLE = {
|
|
38
|
+
markFirstByte() {
|
|
39
|
+
},
|
|
40
|
+
addInputTokens() {
|
|
41
|
+
},
|
|
42
|
+
addOutputTokens() {
|
|
43
|
+
},
|
|
44
|
+
finish() {
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function state(ctx) {
|
|
48
|
+
return ctx.state;
|
|
49
|
+
}
|
|
50
|
+
function safely(ctx, callback) {
|
|
51
|
+
var _a, _b, _c;
|
|
52
|
+
try {
|
|
53
|
+
callback();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
(_c = (_b = (_a = ctx.app) == null ? void 0 : _a.logger) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "[ai-api] App observability callback failed", { error });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function startAiApiObservation(ctx, input) {
|
|
59
|
+
let handle = NOOP_HANDLE;
|
|
60
|
+
safely(ctx, () => {
|
|
61
|
+
const contract = ctx.app[CONTRACT_SYMBOL];
|
|
62
|
+
if (!contract || typeof contract.start !== "function") return;
|
|
63
|
+
const candidate = contract.start({
|
|
64
|
+
service: input.service,
|
|
65
|
+
operation: input.operation,
|
|
66
|
+
streaming: input.streaming,
|
|
67
|
+
attributes: {
|
|
68
|
+
mode: input.mode,
|
|
69
|
+
endpoint: input.operation,
|
|
70
|
+
...input.model ? { model: input.model } : {}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (candidate && typeof candidate.finish === "function") handle = candidate;
|
|
74
|
+
});
|
|
75
|
+
state(ctx).aiApiObservabilityHandle = handle;
|
|
76
|
+
}
|
|
77
|
+
function markAiApiFirstProviderOutput(ctx) {
|
|
78
|
+
safely(ctx, () => {
|
|
79
|
+
var _a;
|
|
80
|
+
return (_a = state(ctx).aiApiObservabilityHandle) == null ? void 0 : _a.markFirstByte();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function addAiApiUsage(ctx, usage) {
|
|
84
|
+
if (!usage) return;
|
|
85
|
+
safely(ctx, () => {
|
|
86
|
+
const handle = state(ctx).aiApiObservabilityHandle;
|
|
87
|
+
if (usage.prompt_tokens !== null) handle == null ? void 0 : handle.addInputTokens(usage.prompt_tokens);
|
|
88
|
+
if (usage.completion_tokens !== null) handle == null ? void 0 : handle.addOutputTokens(usage.completion_tokens);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function setAiApiObservationOutcome(ctx, outcome) {
|
|
92
|
+
state(ctx).aiApiObservabilityOutcome = outcome;
|
|
93
|
+
}
|
|
94
|
+
function finishAiApiObservation(ctx, fallback) {
|
|
95
|
+
const current = state(ctx);
|
|
96
|
+
const handle = current.aiApiObservabilityHandle;
|
|
97
|
+
if (!handle) return;
|
|
98
|
+
current.aiApiObservabilityHandle = void 0;
|
|
99
|
+
const outcome = current.aiApiObservabilityOutcome ?? fallback;
|
|
100
|
+
current.aiApiObservabilityOutcome = void 0;
|
|
101
|
+
safely(ctx, () => handle.finish(outcome));
|
|
102
|
+
}
|
|
103
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
104
|
+
0 && (module.exports = {
|
|
105
|
+
addAiApiUsage,
|
|
106
|
+
finishAiApiObservation,
|
|
107
|
+
markAiApiFirstProviderOutput,
|
|
108
|
+
setAiApiObservationOutcome,
|
|
109
|
+
startAiApiObservation
|
|
110
|
+
});
|