plugin-ai-api 1.0.8 → 1.0.10
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/externalVersion.js +9 -9
- package/dist/server/collections/ai-api-usage-records.js +63 -0
- package/dist/server/middleware/role-permission.js +15 -5
- package/dist/server/plugin.js +8 -1
- package/dist/server/routes/agent-completions.js +108 -42
- package/dist/server/routes/auth.js +37 -7
- package/dist/server/routes/chat-completions.js +103 -19
- package/dist/server/routes/completions.js +37 -15
- package/dist/server/routes/router.js +41 -4
- package/dist/server/usage.js +81 -0
- package/dist/server/utils/openai-format.js +11 -2
- package/dist/server/utils/streaming.js +80 -0
- package/dist/swagger.js +38 -4
- package/package.json +3 -2
- package/src/server/__tests__/openai-format.test.ts +52 -0
- package/src/server/collections/ai-api-usage-records.ts +33 -0
- package/src/server/middleware/role-permission.ts +79 -66
- package/src/server/plugin.ts +99 -89
- package/src/server/routes/agent-completions.ts +121 -54
- package/src/server/routes/auth.ts +142 -111
- package/src/server/routes/chat-completions.ts +406 -318
- package/src/server/routes/completions.ts +322 -299
- package/src/server/routes/router.ts +320 -283
- package/src/server/usage.ts +55 -0
- package/src/server/utils/ai-employee-runtime.ts +1 -1
- package/src/server/utils/openai-format.ts +164 -142
- package/src/server/utils/streaming.ts +46 -0
- package/src/swagger.ts +359 -325
package/dist/externalVersion.js
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
module.exports = {
|
|
11
11
|
"react": "18.2.0",
|
|
12
12
|
"antd": "5.24.2",
|
|
13
|
-
"@nocobase/client-v2": "2.1.
|
|
14
|
-
"@nocobase/flow-engine": "2.1.
|
|
15
|
-
"@nocobase/client": "2.1.
|
|
16
|
-
"@nocobase/plugin-acl": "2.1.
|
|
17
|
-
"@nocobase/server": "2.1.
|
|
13
|
+
"@nocobase/client-v2": "2.1.27",
|
|
14
|
+
"@nocobase/flow-engine": "2.1.27",
|
|
15
|
+
"@nocobase/client": "2.1.27",
|
|
16
|
+
"@nocobase/plugin-acl": "2.1.27",
|
|
17
|
+
"@nocobase/server": "2.1.27",
|
|
18
18
|
"dayjs": "1.11.13",
|
|
19
|
-
"@nocobase/
|
|
20
|
-
"@nocobase/
|
|
21
|
-
"@nocobase/resourcer": "2.1.
|
|
22
|
-
"@nocobase/plugin-ai": "2.1.
|
|
19
|
+
"@nocobase/actions": "2.1.27",
|
|
20
|
+
"@nocobase/database": "2.1.27",
|
|
21
|
+
"@nocobase/resourcer": "2.1.27",
|
|
22
|
+
"@nocobase/plugin-ai": "2.1.27"
|
|
23
23
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var ai_api_usage_records_exports = {};
|
|
28
|
+
__export(ai_api_usage_records_exports, {
|
|
29
|
+
default: () => ai_api_usage_records_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_usage_records_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var ai_api_usage_records_default = (0, import_database.defineCollection)({
|
|
34
|
+
name: "aiApiUsageRecords",
|
|
35
|
+
autoGenId: true,
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: "requestId", type: "string", unique: true, index: true },
|
|
38
|
+
{ name: "userId", type: "string", index: true },
|
|
39
|
+
{ name: "roleName", type: "string", index: true },
|
|
40
|
+
{ name: "authType", type: "string", index: true },
|
|
41
|
+
{ name: "oauthClientId", type: "string", allowNull: true, index: true },
|
|
42
|
+
{ name: "oauthSubject", type: "string", allowNull: true },
|
|
43
|
+
{ name: "oauthScopes", type: "json", allowNull: true },
|
|
44
|
+
{ name: "endpoint", type: "string" },
|
|
45
|
+
{ name: "mode", type: "string", allowNull: true },
|
|
46
|
+
{ name: "model", type: "string", allowNull: true, index: true },
|
|
47
|
+
{ name: "status", type: "string", index: true },
|
|
48
|
+
{ name: "httpStatus", type: "integer", allowNull: true },
|
|
49
|
+
{ name: "errorCode", type: "string", allowNull: true },
|
|
50
|
+
{ name: "streaming", type: "boolean", defaultValue: false },
|
|
51
|
+
{ name: "inputTokens", type: "integer", allowNull: true },
|
|
52
|
+
{ name: "outputTokens", type: "integer", allowNull: true },
|
|
53
|
+
{ name: "totalTokens", type: "integer", allowNull: true },
|
|
54
|
+
{ name: "estimatedCost", type: "decimal", allowNull: true, precision: 20, scale: 8 },
|
|
55
|
+
{ name: "currency", type: "string", allowNull: true },
|
|
56
|
+
{ name: "providerRequestId", type: "string", allowNull: true },
|
|
57
|
+
{ name: "requestMetadata", type: "jsonb", defaultValue: {} },
|
|
58
|
+
{ name: "responseMetadata", type: "jsonb", defaultValue: {} },
|
|
59
|
+
{ name: "startedAt", type: "datetimeTz", allowNull: true },
|
|
60
|
+
{ name: "completedAt", type: "datetimeTz", allowNull: true },
|
|
61
|
+
{ name: "durationMs", type: "integer", allowNull: true }
|
|
62
|
+
]
|
|
63
|
+
});
|
|
@@ -27,19 +27,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var role_permission_exports = {};
|
|
28
28
|
__export(role_permission_exports, {
|
|
29
29
|
checkEmployeeAccess: () => checkEmployeeAccess,
|
|
30
|
-
checkRolePermission: () => checkRolePermission
|
|
30
|
+
checkRolePermission: () => checkRolePermission,
|
|
31
|
+
invalidateRolePermissionCache: () => invalidateRolePermissionCache
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(role_permission_exports);
|
|
33
34
|
var import_openai_format = require("../utils/openai-format");
|
|
35
|
+
const PERMISSION_TTL_MS = 15e3;
|
|
36
|
+
const permissionCache = /* @__PURE__ */ new Map();
|
|
37
|
+
function invalidateRolePermissionCache(roleName) {
|
|
38
|
+
if (roleName) permissionCache.delete(roleName);
|
|
39
|
+
else permissionCache.clear();
|
|
40
|
+
}
|
|
34
41
|
async function checkRolePermission(ctx) {
|
|
35
42
|
var _a;
|
|
36
43
|
const roleName = ((_a = ctx.state.currentRoles) == null ? void 0 : _a[0]) || "member";
|
|
37
44
|
if (roleName === "root" || roleName === "admin") {
|
|
38
45
|
return true;
|
|
39
46
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
const cached = permissionCache.get(roleName);
|
|
48
|
+
const record = cached && cached.expiresAt > Date.now() ? cached.record : await ctx.db.getRepository("aiApiRolePermissions").findOne({ filter: { roleName } });
|
|
49
|
+
if (!cached || cached.expiresAt <= Date.now()) {
|
|
50
|
+
permissionCache.set(roleName, { record, expiresAt: Date.now() + PERMISSION_TTL_MS });
|
|
51
|
+
}
|
|
43
52
|
if (!(record == null ? void 0 : record.enabled)) {
|
|
44
53
|
ctx.status = 403;
|
|
45
54
|
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
@@ -62,5 +71,6 @@ function checkEmployeeAccess(ctx, employeeUsername) {
|
|
|
62
71
|
// Annotate the CommonJS export names for ESM import in node:
|
|
63
72
|
0 && (module.exports = {
|
|
64
73
|
checkEmployeeAccess,
|
|
65
|
-
checkRolePermission
|
|
74
|
+
checkRolePermission,
|
|
75
|
+
invalidateRolePermissionCache
|
|
66
76
|
});
|
package/dist/server/plugin.js
CHANGED
|
@@ -44,6 +44,7 @@ var import_server = require("@nocobase/server");
|
|
|
44
44
|
var import_router = require("./routes/router");
|
|
45
45
|
var import_ai_api_config = __toESM(require("./resource/ai-api-config"));
|
|
46
46
|
var import_rate_limiter = require("./utils/rate-limiter");
|
|
47
|
+
var import_role_permission = require("./middleware/role-permission");
|
|
47
48
|
var import_dayjs = __toESM(require("dayjs"));
|
|
48
49
|
var import_utc = __toESM(require("dayjs/plugin/utc"));
|
|
49
50
|
var import_timezone = __toESM(require("dayjs/plugin/timezone"));
|
|
@@ -61,8 +62,14 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
61
62
|
async beforeLoad() {
|
|
62
63
|
}
|
|
63
64
|
async load() {
|
|
64
|
-
this.app.use((0, import_router.createAiLlmRouter)(this), { before: "resourcer" });
|
|
65
|
+
this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
|
|
65
66
|
this.app.resourceManager.define(import_ai_api_config.default);
|
|
67
|
+
this.app.db.on("aiApiRolePermissions.afterSave", (model) => {
|
|
68
|
+
(0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
|
|
69
|
+
});
|
|
70
|
+
this.app.db.on("aiApiRolePermissions.afterDestroy", (model) => {
|
|
71
|
+
(0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
|
|
72
|
+
});
|
|
66
73
|
this.app.acl.registerSnippet({
|
|
67
74
|
name: `pm.${this.name}.configuration`,
|
|
68
75
|
actions: ["aiApiConfig:*", "aiApiRolePermissions:*"]
|
|
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(agent_completions_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_role_permission = require("../middleware/role-permission");
|
|
35
|
+
var import_streaming = require("../utils/streaming");
|
|
35
36
|
var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
|
|
36
37
|
async function handleAgentCompletions(ctx, plugin) {
|
|
37
38
|
var _a;
|
|
@@ -96,7 +97,7 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
96
97
|
);
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
|
-
const wantStream = body.stream
|
|
100
|
+
const wantStream = (0, import_streaming.isStreamingRequested)(body.stream);
|
|
100
101
|
const lifecycle = (0, import_ai_employee_runtime.getAgentRuntimeLifecycle)(ctx);
|
|
101
102
|
let runtimeContext;
|
|
102
103
|
let lifecycleCompleted = false;
|
|
@@ -182,42 +183,87 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
182
183
|
);
|
|
183
184
|
const originalWrite = ctx.res.write.bind(ctx.res);
|
|
184
185
|
const originalEnd = ctx.res.end.bind(ctx.res);
|
|
186
|
+
const aiPlugin = ctx.app.pm.get("ai");
|
|
187
|
+
const abortAgent = () => {
|
|
188
|
+
var _a2, _b, _c;
|
|
189
|
+
if (!ctx.res.writableEnded) {
|
|
190
|
+
(_c = (_b = (_a2 = aiPlugin == null ? void 0 : aiPlugin.aiEmployeesManager) == null ? void 0 : _a2.conversationController) == null ? void 0 : _b.get(String(sessionId))) == null ? void 0 : _c.abort();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
ctx.req.once("aborted", abortAgent);
|
|
194
|
+
ctx.res.once("close", abortAgent);
|
|
195
|
+
let streamSucceeded = false;
|
|
196
|
+
let sawToolCalls = false;
|
|
197
|
+
let pendingSse = "";
|
|
185
198
|
ctx.res.end = (...args) => {
|
|
186
199
|
if (args[0]) {
|
|
187
200
|
ctx.res.write(args[0]);
|
|
188
201
|
}
|
|
189
202
|
};
|
|
190
203
|
ctx.res.write = (data) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
var _a2;
|
|
205
|
+
pendingSse += typeof data === "string" ? data : data.toString("utf8");
|
|
206
|
+
const frames = pendingSse.split("\n\n");
|
|
207
|
+
pendingSse = frames.pop() || "";
|
|
208
|
+
for (const frame of frames) {
|
|
209
|
+
for (const line of frame.split("\n")) {
|
|
210
|
+
const trimmed = line.trim();
|
|
211
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
212
|
+
const jsonStr = trimmed.substring(6);
|
|
213
|
+
if (!jsonStr) continue;
|
|
214
|
+
try {
|
|
215
|
+
const event = JSON.parse(jsonStr);
|
|
216
|
+
if (event.type === "content" && event.body) {
|
|
217
|
+
originalWrite(
|
|
218
|
+
(0, import_openai_format.formatSSE)(
|
|
219
|
+
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
220
|
+
id: completionId,
|
|
221
|
+
model: body.model,
|
|
222
|
+
delta: { content: String(event.body) }
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
} else if (event.type === "tool_call_chunks" && Array.isArray(event.body)) {
|
|
227
|
+
const chunks = toOpenAIToolCallChunks(event.body);
|
|
228
|
+
if (chunks.length) {
|
|
229
|
+
sawToolCalls = true;
|
|
230
|
+
originalWrite(
|
|
231
|
+
(0, import_openai_format.formatSSE)(
|
|
232
|
+
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
233
|
+
id: completionId,
|
|
234
|
+
model: body.model,
|
|
235
|
+
delta: { tool_calls: chunks }
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
} else if (!sawToolCalls && event.type === "tool_calls" && Array.isArray((_a2 = event.body) == null ? void 0 : _a2.toolCalls)) {
|
|
241
|
+
const chunks = toOpenAIToolCallChunks(event.body.toolCalls);
|
|
242
|
+
if (chunks.length) {
|
|
243
|
+
sawToolCalls = true;
|
|
244
|
+
originalWrite(
|
|
245
|
+
(0, import_openai_format.formatSSE)(
|
|
246
|
+
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
247
|
+
id: completionId,
|
|
248
|
+
model: body.model,
|
|
249
|
+
delta: { tool_calls: chunks }
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
} else if (event.type === "error" && event.body) {
|
|
255
|
+
originalWrite(
|
|
256
|
+
(0, import_openai_format.formatSSE)({
|
|
257
|
+
error: {
|
|
258
|
+
message: String(event.body),
|
|
259
|
+
type: "server_error",
|
|
260
|
+
code: "agent_error"
|
|
261
|
+
}
|
|
206
262
|
})
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
originalWrite(
|
|
211
|
-
(0, import_openai_format.formatSSE)({
|
|
212
|
-
error: {
|
|
213
|
-
message: String(event.body),
|
|
214
|
-
type: "server_error",
|
|
215
|
-
code: "agent_error"
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
);
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
219
266
|
}
|
|
220
|
-
} catch {
|
|
221
267
|
}
|
|
222
268
|
}
|
|
223
269
|
return true;
|
|
@@ -229,7 +275,10 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
229
275
|
model: modelId
|
|
230
276
|
})
|
|
231
277
|
);
|
|
232
|
-
await aiEmployee.stream({ userMessages });
|
|
278
|
+
streamSucceeded = await aiEmployee.stream({ userMessages });
|
|
279
|
+
if (!streamSucceeded) {
|
|
280
|
+
throw new Error("AI Employee stream failed");
|
|
281
|
+
}
|
|
233
282
|
try {
|
|
234
283
|
await (lifecycle == null ? void 0 : lifecycle.runAfterHooks(runtimeContext, { succeeded: true }));
|
|
235
284
|
} finally {
|
|
@@ -238,18 +287,25 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
238
287
|
} finally {
|
|
239
288
|
ctx.res.write = originalWrite;
|
|
240
289
|
ctx.res.end = originalEnd;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
290
|
+
ctx.req.off("aborted", abortAgent);
|
|
291
|
+
ctx.res.off("close", abortAgent);
|
|
292
|
+
if (streamSucceeded && !ctx.res.destroyed) {
|
|
293
|
+
originalWrite(
|
|
294
|
+
(0, import_openai_format.formatSSE)(
|
|
295
|
+
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
296
|
+
id: completionId,
|
|
297
|
+
model: body.model,
|
|
298
|
+
delta: {},
|
|
299
|
+
finishReason: "stop"
|
|
300
|
+
})
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
originalWrite((0, import_openai_format.formatSSEDone)());
|
|
304
|
+
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
305
|
+
} else {
|
|
306
|
+
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "agent_error" };
|
|
307
|
+
}
|
|
308
|
+
if (!ctx.res.writableEnded && !ctx.res.destroyed) originalEnd();
|
|
253
309
|
}
|
|
254
310
|
} else {
|
|
255
311
|
const aiEmployee = new AIEmployee(
|
|
@@ -312,6 +368,16 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
312
368
|
}
|
|
313
369
|
}
|
|
314
370
|
}
|
|
371
|
+
function toOpenAIToolCallChunks(value) {
|
|
372
|
+
return value.map((call, fallbackIndex) => ({
|
|
373
|
+
index: typeof call.index === "number" ? call.index : fallbackIndex,
|
|
374
|
+
...call.id ? { id: String(call.id), type: "function" } : {},
|
|
375
|
+
function: {
|
|
376
|
+
...call.name ? { name: String(call.name) } : {},
|
|
377
|
+
...call.args !== void 0 ? { arguments: typeof call.args === "string" ? call.args : JSON.stringify(call.args) } : {}
|
|
378
|
+
}
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
315
381
|
function extractLastAiMessageContent(result) {
|
|
316
382
|
var _a;
|
|
317
383
|
if (!(result == null ? void 0 : result.messages) || !Array.isArray(result.messages)) return "";
|
|
@@ -37,7 +37,7 @@ async function authenticateBearer(ctx) {
|
|
|
37
37
|
ctx.status = 401;
|
|
38
38
|
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
39
39
|
401,
|
|
40
|
-
"Missing or invalid Authorization header. Expected: Bearer <
|
|
40
|
+
"Missing or invalid Authorization header. Expected: Bearer <access-token>",
|
|
41
41
|
"invalid_request_error",
|
|
42
42
|
"invalid_api_key"
|
|
43
43
|
);
|
|
@@ -50,11 +50,16 @@ async function authenticateBearer(ctx) {
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
52
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (ctx.state.currentUser) {
|
|
54
|
+
if (!ctx.state.currentRole) {
|
|
55
|
+
const requestedRole = ctx.get("X-Role");
|
|
56
|
+
const rolesRepository2 = ctx.db.getRepository("users.roles", ctx.state.currentUser.id);
|
|
57
|
+
const roles2 = await rolesRepository2.find({ fields: ["name"] });
|
|
58
|
+
const roleNames2 = roles2.map((role) => role.name);
|
|
59
|
+
ctx.state.currentRole = roleNames2.includes(requestedRole) ? requestedRole : roleNames2[0];
|
|
60
|
+
ctx.state.currentRoles = ctx.state.currentRole ? [ctx.state.currentRole] : roleNames2;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
58
63
|
}
|
|
59
64
|
const jwt = (_a = ctx.app["authManager"]) == null ? void 0 : _a.jwt;
|
|
60
65
|
if (!jwt) {
|
|
@@ -75,6 +80,16 @@ async function authenticateBearer(ctx) {
|
|
|
75
80
|
ctx.body = (0, import_openai_format.toOpenAIError)(401, "Invalid or expired API key", "invalid_request_error", "invalid_api_key");
|
|
76
81
|
return false;
|
|
77
82
|
}
|
|
83
|
+
if (!decoded.roleName) {
|
|
84
|
+
ctx.status = 401;
|
|
85
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
86
|
+
401,
|
|
87
|
+
"Token was not resolved by the NocoBase auth middleware",
|
|
88
|
+
"invalid_request_error",
|
|
89
|
+
"invalid_api_key"
|
|
90
|
+
);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
78
93
|
const user = await ctx.db.getRepository("users").findOne({
|
|
79
94
|
filterByTk: decoded.userId
|
|
80
95
|
});
|
|
@@ -84,7 +99,22 @@ async function authenticateBearer(ctx) {
|
|
|
84
99
|
return false;
|
|
85
100
|
}
|
|
86
101
|
ctx.state.currentUser = user;
|
|
87
|
-
|
|
102
|
+
const rolesRepository = ctx.db.getRepository("users.roles", user.id);
|
|
103
|
+
const roles = await rolesRepository.find({ fields: ["name"] });
|
|
104
|
+
const roleNames = roles.map((role) => role.name);
|
|
105
|
+
if (!roleNames.includes(decoded.roleName)) {
|
|
106
|
+
ctx.status = 403;
|
|
107
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
108
|
+
403,
|
|
109
|
+
"The API key role is no longer assigned to this user",
|
|
110
|
+
"permission_denied",
|
|
111
|
+
"role_not_permitted"
|
|
112
|
+
);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
ctx.state.currentRole = decoded.roleName;
|
|
116
|
+
ctx.state.currentRoles = [decoded.roleName];
|
|
117
|
+
ctx.state.aiApiAuthType = "apiKey";
|
|
88
118
|
if (!ctx.auth) {
|
|
89
119
|
ctx.auth = {};
|
|
90
120
|
}
|
|
@@ -31,6 +31,7 @@ __export(chat_completions_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(chat_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_streaming = require("../utils/streaming");
|
|
34
35
|
var import_role_permission = require("../middleware/role-permission");
|
|
35
36
|
async function handleChatCompletions(ctx, plugin) {
|
|
36
37
|
var _a;
|
|
@@ -55,7 +56,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
55
56
|
);
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
|
-
const stream = body.stream
|
|
59
|
+
const stream = (0, import_streaming.isStreamingRequested)(body.stream);
|
|
59
60
|
const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
|
|
60
61
|
if (!resolved) {
|
|
61
62
|
ctx.status = 404;
|
|
@@ -150,10 +151,22 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
150
151
|
const langchainMessages = messages.map((msg) => {
|
|
151
152
|
const role = msg.role === "assistant" ? "ai" : msg.role;
|
|
152
153
|
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
154
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
155
|
+
return {
|
|
156
|
+
role,
|
|
157
|
+
content,
|
|
158
|
+
tool_calls: msg.tool_calls,
|
|
159
|
+
additional_kwargs: { tool_calls: msg.tool_calls }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (msg.role === "tool") {
|
|
163
|
+
return { role: "tool", content, tool_call_id: msg.tool_call_id, name: msg.name };
|
|
164
|
+
}
|
|
153
165
|
return [role, content];
|
|
154
166
|
});
|
|
155
167
|
const completionId = (0, import_openai_format.generateCompletionId)();
|
|
156
|
-
const
|
|
168
|
+
const baseModel = provider.createModel();
|
|
169
|
+
const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice);
|
|
157
170
|
if (stream) {
|
|
158
171
|
await handleStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
|
|
159
172
|
} else {
|
|
@@ -163,7 +176,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
163
176
|
ctx.log.error("AI API chat completions error:", err);
|
|
164
177
|
if (!ctx.res.headersSent) {
|
|
165
178
|
ctx.status = 500;
|
|
166
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(500, err
|
|
179
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
}
|
|
@@ -182,11 +195,13 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
|
|
|
182
195
|
total_tokens: result.usage_metadata.total_tokens || 0
|
|
183
196
|
} : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
184
197
|
ctx.status = 200;
|
|
198
|
+
const toolCalls = normalizeToolCalls(result.tool_calls);
|
|
185
199
|
ctx.body = (0, import_openai_format.toOpenAIResponse)({
|
|
186
200
|
id: completionId,
|
|
187
201
|
model: modelName,
|
|
188
202
|
content,
|
|
189
|
-
usage
|
|
203
|
+
usage,
|
|
204
|
+
toolCalls
|
|
190
205
|
});
|
|
191
206
|
}
|
|
192
207
|
async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
|
|
@@ -198,7 +213,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
198
213
|
// Disable nginx buffering
|
|
199
214
|
});
|
|
200
215
|
ctx.status = 200;
|
|
201
|
-
|
|
216
|
+
await (0, import_streaming.writeResponse)(
|
|
217
|
+
ctx,
|
|
202
218
|
(0, import_openai_format.formatSSE)(
|
|
203
219
|
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
204
220
|
id: completionId,
|
|
@@ -207,9 +223,13 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
207
223
|
})
|
|
208
224
|
)
|
|
209
225
|
);
|
|
226
|
+
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
227
|
+
let usage;
|
|
228
|
+
let finishReason = "stop";
|
|
210
229
|
try {
|
|
211
|
-
const stream = await chatModel.stream(messages);
|
|
230
|
+
const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
|
|
212
231
|
for await (const chunk of stream) {
|
|
232
|
+
if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
|
|
213
233
|
let content = "";
|
|
214
234
|
if (typeof chunk.content === "string") {
|
|
215
235
|
content = chunk.content;
|
|
@@ -218,7 +238,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
218
238
|
content = (textPart == null ? void 0 : textPart.text) || "";
|
|
219
239
|
}
|
|
220
240
|
if (content) {
|
|
221
|
-
|
|
241
|
+
await (0, import_streaming.writeResponse)(
|
|
242
|
+
ctx,
|
|
222
243
|
(0, import_openai_format.formatSSE)(
|
|
223
244
|
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
224
245
|
id: completionId,
|
|
@@ -228,31 +249,94 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
228
249
|
)
|
|
229
250
|
);
|
|
230
251
|
}
|
|
252
|
+
const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
|
|
253
|
+
if (toolCallChunks.length) {
|
|
254
|
+
finishReason = "tool_calls";
|
|
255
|
+
await (0, import_streaming.writeResponse)(
|
|
256
|
+
ctx,
|
|
257
|
+
(0, import_openai_format.formatSSE)((0, import_openai_format.toOpenAIStreamChunk)({ id: completionId, model: modelName, delta: { tool_calls: toolCallChunks } }))
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (chunk.usage_metadata) {
|
|
261
|
+
usage = {
|
|
262
|
+
prompt_tokens: chunk.usage_metadata.input_tokens || 0,
|
|
263
|
+
completion_tokens: chunk.usage_metadata.output_tokens || 0,
|
|
264
|
+
total_tokens: chunk.usage_metadata.total_tokens || 0
|
|
265
|
+
};
|
|
266
|
+
}
|
|
231
267
|
}
|
|
232
|
-
|
|
268
|
+
await (0, import_streaming.writeResponse)(
|
|
269
|
+
ctx,
|
|
233
270
|
(0, import_openai_format.formatSSE)(
|
|
234
271
|
(0, import_openai_format.toOpenAIStreamChunk)({
|
|
235
272
|
id: completionId,
|
|
236
273
|
model: modelName,
|
|
237
274
|
delta: {},
|
|
238
|
-
finishReason
|
|
275
|
+
finishReason
|
|
239
276
|
})
|
|
240
277
|
)
|
|
241
278
|
);
|
|
242
|
-
|
|
279
|
+
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
280
|
+
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
|
|
243
281
|
} catch (err) {
|
|
244
282
|
ctx.log.error("AI API streaming error:", err);
|
|
245
|
-
ctx.res.
|
|
246
|
-
(0,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
283
|
+
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
284
|
+
await (0, import_streaming.writeResponse)(
|
|
285
|
+
ctx,
|
|
286
|
+
(0, import_openai_format.formatSSE)({
|
|
287
|
+
error: {
|
|
288
|
+
message: getErrorMessage(err, "Streaming error"),
|
|
289
|
+
type: "server_error"
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
|
|
253
295
|
} finally {
|
|
254
|
-
|
|
296
|
+
requestAbort.dispose();
|
|
297
|
+
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function getErrorMessage(error, fallback) {
|
|
301
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
302
|
+
}
|
|
303
|
+
function bindRequestTools(chatModel, tools, toolChoice) {
|
|
304
|
+
if (!Array.isArray(tools) || tools.length === 0) return chatModel;
|
|
305
|
+
if (typeof chatModel.bindTools !== "function") {
|
|
306
|
+
throw new Error("The selected LLM provider does not support tool calling");
|
|
255
307
|
}
|
|
308
|
+
return chatModel.bindTools(tools, toolChoice === void 0 ? void 0 : { tool_choice: toolChoice });
|
|
309
|
+
}
|
|
310
|
+
function normalizeToolCalls(value) {
|
|
311
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
312
|
+
return value.map((call) => {
|
|
313
|
+
var _a, _b;
|
|
314
|
+
return {
|
|
315
|
+
id: String(call.id || ""),
|
|
316
|
+
type: "function",
|
|
317
|
+
function: {
|
|
318
|
+
name: String(call.name || ((_a = call.function) == null ? void 0 : _a.name) || ""),
|
|
319
|
+
arguments: serializeToolArguments(call.args ?? ((_b = call.function) == null ? void 0 : _b.arguments))
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function normalizeToolCallChunks(value) {
|
|
325
|
+
if (!Array.isArray(value)) return [];
|
|
326
|
+
return value.map((call, fallbackIndex) => {
|
|
327
|
+
var _a, _b;
|
|
328
|
+
return {
|
|
329
|
+
index: typeof call.index === "number" ? call.index : fallbackIndex,
|
|
330
|
+
...call.id ? { id: String(call.id), type: "function" } : {},
|
|
331
|
+
function: {
|
|
332
|
+
...call.name ? { name: String(call.name) } : {},
|
|
333
|
+
...call.args !== void 0 || ((_a = call.function) == null ? void 0 : _a.arguments) !== void 0 ? { arguments: serializeToolArguments(call.args ?? ((_b = call.function) == null ? void 0 : _b.arguments)) } : {}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function serializeToolArguments(value) {
|
|
339
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? {});
|
|
256
340
|
}
|
|
257
341
|
// Annotate the CommonJS export names for ESM import in node:
|
|
258
342
|
0 && (module.exports = {
|