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
|
@@ -31,6 +31,7 @@ __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_streaming = require("../utils/streaming");
|
|
34
35
|
async function handleCompletions(ctx, plugin) {
|
|
35
36
|
var _a;
|
|
36
37
|
const body = ctx.request.body;
|
|
@@ -54,7 +55,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
54
55
|
);
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
|
-
const stream = body.stream
|
|
58
|
+
const stream = (0, import_streaming.isStreamingRequested)(body.stream);
|
|
58
59
|
const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
|
|
59
60
|
if (!resolved) {
|
|
60
61
|
ctx.status = 404;
|
|
@@ -145,7 +146,7 @@ async function handleCompletions(ctx, plugin) {
|
|
|
145
146
|
ctx.log.error("AI API completions error:", err);
|
|
146
147
|
if (!ctx.res.headersSent) {
|
|
147
148
|
ctx.status = 500;
|
|
148
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(500, err
|
|
149
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(500, getErrorMessage(err, "Internal server error"), "server_error");
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
}
|
|
@@ -189,9 +190,12 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
189
190
|
"X-Accel-Buffering": "no"
|
|
190
191
|
});
|
|
191
192
|
ctx.status = 200;
|
|
193
|
+
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
194
|
+
let usage;
|
|
192
195
|
try {
|
|
193
|
-
const stream = await chatModel.stream(messages);
|
|
196
|
+
const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
|
|
194
197
|
for await (const chunk of stream) {
|
|
198
|
+
if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
|
|
195
199
|
let text = "";
|
|
196
200
|
if (typeof chunk.content === "string") {
|
|
197
201
|
text = chunk.content;
|
|
@@ -200,7 +204,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
200
204
|
text = (textPart == null ? void 0 : textPart.text) || "";
|
|
201
205
|
}
|
|
202
206
|
if (text) {
|
|
203
|
-
|
|
207
|
+
await (0, import_streaming.writeResponse)(
|
|
208
|
+
ctx,
|
|
204
209
|
(0, import_openai_format.formatSSE)({
|
|
205
210
|
id: completionId,
|
|
206
211
|
object: "text_completion",
|
|
@@ -218,8 +223,16 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
218
223
|
})
|
|
219
224
|
);
|
|
220
225
|
}
|
|
226
|
+
if (chunk.usage_metadata) {
|
|
227
|
+
usage = {
|
|
228
|
+
prompt_tokens: chunk.usage_metadata.input_tokens || 0,
|
|
229
|
+
completion_tokens: chunk.usage_metadata.output_tokens || 0,
|
|
230
|
+
total_tokens: chunk.usage_metadata.total_tokens || 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
221
233
|
}
|
|
222
|
-
|
|
234
|
+
await (0, import_streaming.writeResponse)(
|
|
235
|
+
ctx,
|
|
223
236
|
(0, import_openai_format.formatSSE)({
|
|
224
237
|
id: completionId,
|
|
225
238
|
object: "text_completion",
|
|
@@ -236,21 +249,30 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
236
249
|
]
|
|
237
250
|
})
|
|
238
251
|
);
|
|
239
|
-
|
|
252
|
+
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
253
|
+
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId, usage };
|
|
240
254
|
} catch (err) {
|
|
241
255
|
ctx.log.error("AI API completions streaming error:", err);
|
|
242
|
-
ctx.res.
|
|
243
|
-
(0,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
257
|
+
await (0, import_streaming.writeResponse)(
|
|
258
|
+
ctx,
|
|
259
|
+
(0, import_openai_format.formatSSE)({
|
|
260
|
+
error: {
|
|
261
|
+
message: getErrorMessage(err, "Streaming error"),
|
|
262
|
+
type: "server_error"
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, usage, errorCode: "stream_error" };
|
|
250
268
|
} finally {
|
|
251
|
-
|
|
269
|
+
requestAbort.dispose();
|
|
270
|
+
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
252
271
|
}
|
|
253
272
|
}
|
|
273
|
+
function getErrorMessage(error, fallback) {
|
|
274
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
275
|
+
}
|
|
254
276
|
// Annotate the CommonJS export names for ESM import in node:
|
|
255
277
|
0 && (module.exports = {
|
|
256
278
|
handleCompletions
|
|
@@ -49,11 +49,12 @@ var import_embeddings = require("./embeddings");
|
|
|
49
49
|
var import_openai_format = require("../utils/openai-format");
|
|
50
50
|
var import_rate_limit = require("../middleware/rate-limit");
|
|
51
51
|
var import_role_permission = require("../middleware/role-permission");
|
|
52
|
+
var import_usage = require("../usage");
|
|
52
53
|
const API_PREFIX = "/api/ai-llm/v1";
|
|
53
54
|
function createAiLlmRouter(plugin) {
|
|
54
55
|
const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
|
|
55
56
|
return async (ctx, next) => {
|
|
56
|
-
var _a;
|
|
57
|
+
var _a, _b, _c, _d;
|
|
57
58
|
const { path, method } = ctx;
|
|
58
59
|
if (!path.startsWith(API_PREFIX)) {
|
|
59
60
|
return next();
|
|
@@ -100,11 +101,29 @@ function createAiLlmRouter(plugin) {
|
|
|
100
101
|
}
|
|
101
102
|
const model = ((_a = ctx.request.body) == null ? void 0 : _a.model) ?? "-";
|
|
102
103
|
const t0 = Date.now();
|
|
104
|
+
let usageId;
|
|
105
|
+
try {
|
|
106
|
+
usageId = await (0, import_usage.startUsageRecord)(
|
|
107
|
+
ctx,
|
|
108
|
+
requestId,
|
|
109
|
+
subPath,
|
|
110
|
+
String(model),
|
|
111
|
+
Boolean((_b = ctx.request.body) == null ? void 0 : _b.stream)
|
|
112
|
+
);
|
|
113
|
+
} catch (usageError) {
|
|
114
|
+
ctx.log.error("AI API usage record could not be created:", usageError);
|
|
115
|
+
}
|
|
103
116
|
try {
|
|
104
117
|
if (method === "POST" && subPath === "/chat/completions") {
|
|
105
118
|
const mode = await resolveMode(ctx);
|
|
106
119
|
await (mode === "agent" ? (0, import_agent_completions.handleAgentCompletions)(ctx, plugin) : (0, import_chat_completions.handleChatCompletions)(ctx, plugin));
|
|
107
|
-
logRequest(
|
|
120
|
+
logRequest(
|
|
121
|
+
ctx,
|
|
122
|
+
requestId,
|
|
123
|
+
model,
|
|
124
|
+
((_c = ctx.state.aiApiStreamResult) == null ? void 0 : _c.succeeded) === false ? "error" : "ok",
|
|
125
|
+
Date.now() - t0
|
|
126
|
+
);
|
|
108
127
|
return;
|
|
109
128
|
}
|
|
110
129
|
if (method === "POST" && subPath === "/embeddings") {
|
|
@@ -124,7 +143,13 @@ function createAiLlmRouter(plugin) {
|
|
|
124
143
|
} else {
|
|
125
144
|
await (0, import_completions.handleCompletions)(ctx, plugin);
|
|
126
145
|
}
|
|
127
|
-
logRequest(
|
|
146
|
+
logRequest(
|
|
147
|
+
ctx,
|
|
148
|
+
requestId,
|
|
149
|
+
model,
|
|
150
|
+
((_d = ctx.state.aiApiStreamResult) == null ? void 0 : _d.succeeded) === false ? "error" : "ok",
|
|
151
|
+
Date.now() - t0
|
|
152
|
+
);
|
|
128
153
|
return;
|
|
129
154
|
}
|
|
130
155
|
if (method === "GET" && subPath === "/models") {
|
|
@@ -164,7 +189,19 @@ function createAiLlmRouter(plugin) {
|
|
|
164
189
|
logRequest(ctx, requestId, model, "error", Date.now() - t0);
|
|
165
190
|
if (!ctx.res.headersSent) {
|
|
166
191
|
ctx.status = 500;
|
|
167
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
192
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
193
|
+
500,
|
|
194
|
+
err instanceof Error && err.message ? err.message : "Internal server error",
|
|
195
|
+
"server_error"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
if (usageId !== void 0) {
|
|
200
|
+
try {
|
|
201
|
+
await (0, import_usage.finishUsageRecord)(ctx, usageId, t0, ctx.status >= 200 && ctx.status < 400 ? "succeeded" : "failed");
|
|
202
|
+
} catch (usageError) {
|
|
203
|
+
ctx.log.error("AI API usage record could not be finalized:", usageError);
|
|
204
|
+
}
|
|
168
205
|
}
|
|
169
206
|
}
|
|
170
207
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
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 usage_exports = {};
|
|
28
|
+
__export(usage_exports, {
|
|
29
|
+
finishUsageRecord: () => finishUsageRecord,
|
|
30
|
+
startUsageRecord: () => startUsageRecord
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(usage_exports);
|
|
33
|
+
async function startUsageRecord(ctx, requestId, endpoint, model, streaming) {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
const body = ctx.request.body || {};
|
|
36
|
+
const messages = Array.isArray(body.messages) ? body.messages : void 0;
|
|
37
|
+
const oauth = ctx.state.oauthPrincipal;
|
|
38
|
+
const record = await ctx.db.getRepository("aiApiUsageRecords").create({
|
|
39
|
+
values: {
|
|
40
|
+
requestId,
|
|
41
|
+
userId: String((_a = ctx.state.currentUser) == null ? void 0 : _a.id),
|
|
42
|
+
roleName: ctx.state.currentRole || ((_b = ctx.state.currentRoles) == null ? void 0 : _b[0]) || "unknown",
|
|
43
|
+
authType: ctx.state.aiApiAuthType || (oauth ? "oidc" : "session"),
|
|
44
|
+
oauthClientId: oauth == null ? void 0 : oauth.clientId,
|
|
45
|
+
oauthSubject: oauth == null ? void 0 : oauth.subject,
|
|
46
|
+
oauthScopes: oauth == null ? void 0 : oauth.scopes,
|
|
47
|
+
endpoint,
|
|
48
|
+
mode: ctx.get("X-AI-Mode") || void 0,
|
|
49
|
+
model: model === "-" ? void 0 : model,
|
|
50
|
+
status: "pending",
|
|
51
|
+
streaming,
|
|
52
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
53
|
+
requestMetadata: { messageCount: messages == null ? void 0 : messages.length, requestedMaxTokens: body.max_tokens }
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return record.id;
|
|
57
|
+
}
|
|
58
|
+
async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
59
|
+
var _a;
|
|
60
|
+
const response = ctx.body || {};
|
|
61
|
+
const streamResult = ctx.state.aiApiStreamResult;
|
|
62
|
+
const usage = response.usage || (streamResult == null ? void 0 : streamResult.usage);
|
|
63
|
+
const values = {
|
|
64
|
+
status: streamResult ? streamResult.succeeded ? "succeeded" : "failed" : status,
|
|
65
|
+
httpStatus: ctx.status,
|
|
66
|
+
errorCode: ((_a = response.error) == null ? void 0 : _a.code) || (streamResult == null ? void 0 : streamResult.errorCode),
|
|
67
|
+
inputTokens: usage == null ? void 0 : usage.prompt_tokens,
|
|
68
|
+
outputTokens: usage == null ? void 0 : usage.completion_tokens,
|
|
69
|
+
totalTokens: usage == null ? void 0 : usage.total_tokens,
|
|
70
|
+
providerRequestId: response.id || (streamResult == null ? void 0 : streamResult.id),
|
|
71
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
72
|
+
durationMs: Date.now() - startedAt,
|
|
73
|
+
responseMetadata: { usageSource: usage ? "response" : "unavailable" }
|
|
74
|
+
};
|
|
75
|
+
await ctx.db.getRepository("aiApiUsageRecords").update({ filterByTk: id, values });
|
|
76
|
+
}
|
|
77
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
78
|
+
0 && (module.exports = {
|
|
79
|
+
finishUsageRecord,
|
|
80
|
+
startUsageRecord
|
|
81
|
+
});
|
|
@@ -71,7 +71,15 @@ function toOpenAIError(statusCode, message, type = "invalid_request_error", code
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
function toOpenAIResponse(options) {
|
|
74
|
-
|
|
74
|
+
var _a;
|
|
75
|
+
const {
|
|
76
|
+
id,
|
|
77
|
+
model,
|
|
78
|
+
content,
|
|
79
|
+
finishReason = ((_a = options.toolCalls) == null ? void 0 : _a.length) ? "tool_calls" : "stop",
|
|
80
|
+
usage,
|
|
81
|
+
toolCalls
|
|
82
|
+
} = options;
|
|
75
83
|
return {
|
|
76
84
|
id,
|
|
77
85
|
object: "chat.completion",
|
|
@@ -83,7 +91,8 @@ function toOpenAIResponse(options) {
|
|
|
83
91
|
index: 0,
|
|
84
92
|
message: {
|
|
85
93
|
role: "assistant",
|
|
86
|
-
content
|
|
94
|
+
content,
|
|
95
|
+
...(toolCalls == null ? void 0 : toolCalls.length) ? { tool_calls: toolCalls } : {}
|
|
87
96
|
},
|
|
88
97
|
logprobs: null,
|
|
89
98
|
finish_reason: finishReason
|
|
@@ -0,0 +1,80 @@
|
|
|
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 streaming_exports = {};
|
|
28
|
+
__export(streaming_exports, {
|
|
29
|
+
createRequestAbortController: () => createRequestAbortController,
|
|
30
|
+
isStreamingRequested: () => isStreamingRequested,
|
|
31
|
+
writeResponse: () => writeResponse
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(streaming_exports);
|
|
34
|
+
function isStreamingRequested(value) {
|
|
35
|
+
return value !== false;
|
|
36
|
+
}
|
|
37
|
+
function createRequestAbortController(ctx) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const abort = () => {
|
|
40
|
+
if (!ctx.res.writableEnded) controller.abort(new Error("Client disconnected"));
|
|
41
|
+
};
|
|
42
|
+
ctx.req.once("aborted", abort);
|
|
43
|
+
ctx.res.once("close", abort);
|
|
44
|
+
return {
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
dispose() {
|
|
47
|
+
ctx.req.off("aborted", abort);
|
|
48
|
+
ctx.res.off("close", abort);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function writeResponse(ctx, data) {
|
|
53
|
+
if (ctx.res.writableEnded || ctx.res.destroyed) return false;
|
|
54
|
+
if (!ctx.res.write(data)) await waitForDrain(ctx);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function waitForDrain(ctx) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
ctx.res.off("drain", onDrain);
|
|
61
|
+
ctx.res.off("close", onClose);
|
|
62
|
+
};
|
|
63
|
+
const onDrain = () => {
|
|
64
|
+
cleanup();
|
|
65
|
+
resolve();
|
|
66
|
+
};
|
|
67
|
+
const onClose = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
reject(new Error("Client disconnected"));
|
|
70
|
+
};
|
|
71
|
+
ctx.res.once("drain", onDrain);
|
|
72
|
+
ctx.res.once("close", onClose);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
76
|
+
0 && (module.exports = {
|
|
77
|
+
createRequestAbortController,
|
|
78
|
+
isStreamingRequested,
|
|
79
|
+
writeResponse
|
|
80
|
+
});
|
package/dist/swagger.js
CHANGED
|
@@ -183,7 +183,7 @@ var swagger_default = {
|
|
|
183
183
|
model: { type: "string" },
|
|
184
184
|
prompt: { type: "string" },
|
|
185
185
|
max_tokens: { type: "integer" },
|
|
186
|
-
stream: { type: "boolean", default:
|
|
186
|
+
stream: { type: "boolean", default: true }
|
|
187
187
|
},
|
|
188
188
|
required: ["model", "prompt"]
|
|
189
189
|
}
|
|
@@ -294,7 +294,9 @@ var swagger_default = {
|
|
|
294
294
|
properties: {
|
|
295
295
|
role: { type: "string", enum: ["system", "user", "assistant", "tool"] },
|
|
296
296
|
content: { type: "string" },
|
|
297
|
-
name: { type: "string" }
|
|
297
|
+
name: { type: "string" },
|
|
298
|
+
tool_call_id: { type: "string" },
|
|
299
|
+
tool_calls: { type: "array", items: { $ref: "#/components/schemas/ToolCall" } }
|
|
298
300
|
},
|
|
299
301
|
required: ["role", "content"]
|
|
300
302
|
},
|
|
@@ -303,12 +305,17 @@ var swagger_default = {
|
|
|
303
305
|
properties: {
|
|
304
306
|
model: { type: "string", example: "openai/gpt-4o" },
|
|
305
307
|
messages: { type: "array", items: { $ref: "#/components/schemas/ChatMessage" } },
|
|
306
|
-
stream: { type: "boolean", default:
|
|
308
|
+
stream: { type: "boolean", default: true },
|
|
307
309
|
temperature: { type: "number", minimum: 0, maximum: 2 },
|
|
308
310
|
max_tokens: { type: "integer" },
|
|
309
311
|
top_p: { type: "number" },
|
|
310
312
|
frequency_penalty: { type: "number" },
|
|
311
|
-
presence_penalty: { type: "number" }
|
|
313
|
+
presence_penalty: { type: "number" },
|
|
314
|
+
tools: { type: "array", items: { $ref: "#/components/schemas/ToolDefinition" } },
|
|
315
|
+
tool_choice: {
|
|
316
|
+
description: "OpenAI-compatible tool choice: auto, none, required, or a named function choice.",
|
|
317
|
+
oneOf: [{ type: "string", enum: ["auto", "none", "required"] }, { type: "object" }]
|
|
318
|
+
}
|
|
312
319
|
},
|
|
313
320
|
required: ["model", "messages"]
|
|
314
321
|
},
|
|
@@ -339,6 +346,33 @@ var swagger_default = {
|
|
|
339
346
|
}
|
|
340
347
|
}
|
|
341
348
|
}
|
|
349
|
+
},
|
|
350
|
+
ToolDefinition: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
type: { type: "string", enum: ["function"] },
|
|
354
|
+
function: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
name: { type: "string" },
|
|
358
|
+
description: { type: "string" },
|
|
359
|
+
parameters: { type: "object" }
|
|
360
|
+
},
|
|
361
|
+
required: ["name", "parameters"]
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
required: ["type", "function"]
|
|
365
|
+
},
|
|
366
|
+
ToolCall: {
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: {
|
|
369
|
+
id: { type: "string" },
|
|
370
|
+
type: { type: "string", enum: ["function"] },
|
|
371
|
+
function: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: { name: { type: "string" }, arguments: { type: "string" } }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
342
376
|
}
|
|
343
377
|
}
|
|
344
378
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-ai-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"main": "dist/server/index.js",
|
|
5
5
|
"dependencies": {},
|
|
6
6
|
"peerDependencies": {
|
|
@@ -28,5 +28,6 @@
|
|
|
28
28
|
"client.d.ts",
|
|
29
29
|
"client-v2.d.ts",
|
|
30
30
|
"server.d.ts"
|
|
31
|
-
]
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0"
|
|
32
33
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { toOpenAIResponse, toOpenAIStreamChunk } from '../utils/openai-format';
|
|
2
|
+
import { isStreamingRequested } from '../utils/streaming';
|
|
3
|
+
|
|
4
|
+
describe('AI API OpenAI tool-call formatting', () => {
|
|
5
|
+
it('streams by default and only disables streaming for an explicit false value', () => {
|
|
6
|
+
expect(isStreamingRequested(undefined)).toBe(true);
|
|
7
|
+
expect(isStreamingRequested(true)).toBe(true);
|
|
8
|
+
expect(isStreamingRequested(false)).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('returns tool calls and the matching finish reason for a non-stream response', () => {
|
|
11
|
+
const response = toOpenAIResponse({
|
|
12
|
+
id: 'chatcmpl-1',
|
|
13
|
+
model: 'service/model',
|
|
14
|
+
content: '',
|
|
15
|
+
toolCalls: [
|
|
16
|
+
{
|
|
17
|
+
id: 'call-1',
|
|
18
|
+
type: 'function',
|
|
19
|
+
function: { name: 'get_weather', arguments: '{"city":"Hanoi"}' },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(response.choices[0].finish_reason).toBe('tool_calls');
|
|
25
|
+
expect(response.choices[0].message.tool_calls).toEqual([
|
|
26
|
+
{
|
|
27
|
+
id: 'call-1',
|
|
28
|
+
type: 'function',
|
|
29
|
+
function: { name: 'get_weather', arguments: '{"city":"Hanoi"}' },
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('formats streaming tool-call deltas', () => {
|
|
35
|
+
const chunk = toOpenAIStreamChunk({
|
|
36
|
+
id: 'chatcmpl-1',
|
|
37
|
+
model: 'service/model',
|
|
38
|
+
delta: {
|
|
39
|
+
tool_calls: [
|
|
40
|
+
{
|
|
41
|
+
index: 0,
|
|
42
|
+
id: 'call-1',
|
|
43
|
+
type: 'function',
|
|
44
|
+
function: { name: 'get_weather', arguments: '{"city"' },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(chunk.choices[0].delta.tool_calls?.[0].function?.name).toBe('get_weather');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineCollection } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default defineCollection({
|
|
4
|
+
name: 'aiApiUsageRecords',
|
|
5
|
+
autoGenId: true,
|
|
6
|
+
fields: [
|
|
7
|
+
{ name: 'requestId', type: 'string', unique: true, index: true },
|
|
8
|
+
{ name: 'userId', type: 'string', index: true },
|
|
9
|
+
{ name: 'roleName', type: 'string', index: true },
|
|
10
|
+
{ name: 'authType', type: 'string', index: true },
|
|
11
|
+
{ name: 'oauthClientId', type: 'string', allowNull: true, index: true },
|
|
12
|
+
{ name: 'oauthSubject', type: 'string', allowNull: true },
|
|
13
|
+
{ name: 'oauthScopes', type: 'json', allowNull: true },
|
|
14
|
+
{ name: 'endpoint', type: 'string' },
|
|
15
|
+
{ name: 'mode', type: 'string', allowNull: true },
|
|
16
|
+
{ name: 'model', type: 'string', allowNull: true, index: true },
|
|
17
|
+
{ name: 'status', type: 'string', index: true },
|
|
18
|
+
{ name: 'httpStatus', type: 'integer', allowNull: true },
|
|
19
|
+
{ name: 'errorCode', type: 'string', allowNull: true },
|
|
20
|
+
{ name: 'streaming', type: 'boolean', defaultValue: false },
|
|
21
|
+
{ name: 'inputTokens', type: 'integer', allowNull: true },
|
|
22
|
+
{ name: 'outputTokens', type: 'integer', allowNull: true },
|
|
23
|
+
{ name: 'totalTokens', type: 'integer', allowNull: true },
|
|
24
|
+
{ name: 'estimatedCost', type: 'decimal', allowNull: true, precision: 20, scale: 8 },
|
|
25
|
+
{ name: 'currency', type: 'string', allowNull: true },
|
|
26
|
+
{ name: 'providerRequestId', type: 'string', allowNull: true },
|
|
27
|
+
{ name: 'requestMetadata', type: 'jsonb', defaultValue: {} },
|
|
28
|
+
{ name: 'responseMetadata', type: 'jsonb', defaultValue: {} },
|
|
29
|
+
{ name: 'startedAt', type: 'datetimeTz', allowNull: true },
|
|
30
|
+
{ name: 'completedAt', type: 'datetimeTz', allowNull: true },
|
|
31
|
+
{ name: 'durationMs', type: 'integer', allowNull: true },
|
|
32
|
+
],
|
|
33
|
+
});
|