plugin-ai-api 1.0.0
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/README.md +1 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/824c2f7487ee05fd.js +1 -0
- package/dist/client/AiApiConfigPage.d.ts +3 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/locale.d.ts +10 -0
- package/dist/client/models/index.d.ts +10 -0
- package/dist/client/plugin.d.ts +6 -0
- package/dist/externalVersion.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/locale/en-US.json +10 -0
- package/dist/locale/zh-CN.json +10 -0
- package/dist/server/collections/ai-api-config.d.ts +2 -0
- package/dist/server/collections/ai-api-config.js +63 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/middleware/rate-limit.d.ts +18 -0
- package/dist/server/middleware/rate-limit.js +61 -0
- package/dist/server/plugin.d.ts +18 -0
- package/dist/server/plugin.js +87 -0
- package/dist/server/resource/ai-api-config.d.ts +10 -0
- package/dist/server/resource/ai-api-config.js +73 -0
- package/dist/server/routes/agent-completions.d.ts +34 -0
- package/dist/server/routes/agent-completions.js +300 -0
- package/dist/server/routes/auth.d.ts +8 -0
- package/dist/server/routes/auth.js +110 -0
- package/dist/server/routes/chat-completions.d.ts +9 -0
- package/dist/server/routes/chat-completions.js +237 -0
- package/dist/server/routes/completions.d.ts +10 -0
- package/dist/server/routes/completions.js +245 -0
- package/dist/server/routes/embeddings.d.ts +22 -0
- package/dist/server/routes/embeddings.js +166 -0
- package/dist/server/routes/models.d.ts +21 -0
- package/dist/server/routes/models.js +154 -0
- package/dist/server/routes/router.d.ts +25 -0
- package/dist/server/routes/router.js +189 -0
- package/dist/server/utils/openai-format.d.ts +99 -0
- package/dist/server/utils/openai-format.js +140 -0
- package/dist/server/utils/rate-limiter.d.ts +35 -0
- package/dist/server/utils/rate-limiter.js +81 -0
- package/dist/server/utils/resolve-service.d.ts +19 -0
- package/dist/server/utils/resolve-service.js +71 -0
- package/nocobase-plugin-ai-api-2.0.20.tgz +0 -0
- package/package.json +13 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var router_exports = {};
|
|
29
|
+
__export(router_exports, {
|
|
30
|
+
createAiLlmRouter: () => createAiLlmRouter
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(router_exports);
|
|
33
|
+
var import_crypto = __toESM(require("crypto"));
|
|
34
|
+
var import_auth = require("./auth");
|
|
35
|
+
var import_models = require("./models");
|
|
36
|
+
var import_chat_completions = require("./chat-completions");
|
|
37
|
+
var import_completions = require("./completions");
|
|
38
|
+
var import_agent_completions = require("./agent-completions");
|
|
39
|
+
var import_embeddings = require("./embeddings");
|
|
40
|
+
var import_openai_format = require("../utils/openai-format");
|
|
41
|
+
var import_rate_limit = require("../middleware/rate-limit");
|
|
42
|
+
const API_PREFIX = "/api/ai-llm/v1";
|
|
43
|
+
function createAiLlmRouter(plugin) {
|
|
44
|
+
const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
|
|
45
|
+
return async (ctx, next) => {
|
|
46
|
+
var _a;
|
|
47
|
+
const { path, method } = ctx;
|
|
48
|
+
if (!path.startsWith(API_PREFIX)) {
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
const subPath = path.substring(API_PREFIX.length);
|
|
52
|
+
ctx.set("Access-Control-Allow-Origin", "*");
|
|
53
|
+
ctx.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
54
|
+
ctx.set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-AI-Mode, X-Timezone, X-Locale");
|
|
55
|
+
ctx.set("Access-Control-Expose-Headers", "X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After");
|
|
56
|
+
ctx.set("Access-Control-Max-Age", "86400");
|
|
57
|
+
if (method === "OPTIONS") {
|
|
58
|
+
ctx.status = 204;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const requestId = `req-${import_crypto.default.randomBytes(12).toString("hex")}`;
|
|
62
|
+
ctx.set("X-Request-Id", requestId);
|
|
63
|
+
if (method === "POST" && !ctx.request.body) {
|
|
64
|
+
try {
|
|
65
|
+
const rawBody = await getRawBody(ctx);
|
|
66
|
+
ctx.request.body = JSON.parse(rawBody);
|
|
67
|
+
} catch (bodyErr) {
|
|
68
|
+
const status = (bodyErr == null ? void 0 : bodyErr.statusCode) === 413 ? 413 : 400;
|
|
69
|
+
const message = status === 413 ? "Request body too large (max 10 MB)" : "Invalid JSON in request body";
|
|
70
|
+
ctx.status = status;
|
|
71
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(status, message, "invalid_request_error");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const isAuth = await (0, import_auth.authenticateBearer)(ctx);
|
|
76
|
+
if (!isAuth) {
|
|
77
|
+
logRequest(ctx, requestId, "-", "auth_failed", 0);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const allowed = await checkRateLimit(ctx);
|
|
81
|
+
if (!allowed) {
|
|
82
|
+
logRequest(ctx, requestId, "-", "rate_limited", 0);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const model = ((_a = ctx.request.body) == null ? void 0 : _a.model) ?? "-";
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
try {
|
|
88
|
+
if (method === "POST" && subPath === "/chat/completions") {
|
|
89
|
+
const mode = await resolveMode(ctx);
|
|
90
|
+
await (mode === "agent" ? (0, import_agent_completions.handleAgentCompletions)(ctx, plugin) : (0, import_chat_completions.handleChatCompletions)(ctx, plugin));
|
|
91
|
+
logRequest(ctx, requestId, model, "ok", Date.now() - t0);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (method === "POST" && subPath === "/embeddings") {
|
|
95
|
+
await (0, import_embeddings.handleEmbeddings)(ctx, plugin);
|
|
96
|
+
logRequest(ctx, requestId, model, "ok", Date.now() - t0);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (method === "POST" && subPath === "/completions") {
|
|
100
|
+
await (0, import_completions.handleCompletions)(ctx, plugin);
|
|
101
|
+
logRequest(ctx, requestId, model, "ok", Date.now() - t0);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (method === "GET" && subPath === "/models") {
|
|
105
|
+
await (0, import_models.handleListModels)(ctx, plugin);
|
|
106
|
+
logRequest(ctx, requestId, "-", "ok", Date.now() - t0);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (method === "GET" && subPath.startsWith("/models/")) {
|
|
110
|
+
const modelId = subPath.substring("/models/".length);
|
|
111
|
+
if (modelId) {
|
|
112
|
+
await (0, import_models.handleGetModel)(ctx, decodeURIComponent(modelId), plugin);
|
|
113
|
+
logRequest(ctx, requestId, modelId, "ok", Date.now() - t0);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (method === "DELETE" && subPath.startsWith("/models/")) {
|
|
118
|
+
ctx.status = 501;
|
|
119
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
120
|
+
501,
|
|
121
|
+
"Model deletion is not supported by this API gateway. Use the NocoBase admin panel to manage LLM services.",
|
|
122
|
+
"invalid_request_error",
|
|
123
|
+
"not_implemented"
|
|
124
|
+
);
|
|
125
|
+
logRequest(ctx, requestId, "-", "not_implemented", Date.now() - t0);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
ctx.status = 404;
|
|
129
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
130
|
+
404,
|
|
131
|
+
`Unknown endpoint: ${method} ${path}. Supported: POST /v1/chat/completions, POST /v1/completions, POST /v1/embeddings, GET /v1/models`,
|
|
132
|
+
"invalid_request_error",
|
|
133
|
+
"unknown_url"
|
|
134
|
+
);
|
|
135
|
+
logRequest(ctx, requestId, "-", "not_found", Date.now() - t0);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
ctx.log.error("AI API router error:", err);
|
|
138
|
+
logRequest(ctx, requestId, model, "error", Date.now() - t0);
|
|
139
|
+
if (!ctx.res.headersSent) {
|
|
140
|
+
ctx.status = 500;
|
|
141
|
+
ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
147
|
+
function getRawBody(ctx) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
let body = "";
|
|
150
|
+
let byteCount = 0;
|
|
151
|
+
ctx.req.on("data", (chunk) => {
|
|
152
|
+
byteCount += chunk.length;
|
|
153
|
+
if (byteCount > MAX_BODY_BYTES) {
|
|
154
|
+
ctx.req.destroy();
|
|
155
|
+
reject(Object.assign(new Error("Request body too large (max 10 MB)"), { statusCode: 413 }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
body += chunk.toString();
|
|
159
|
+
});
|
|
160
|
+
ctx.req.on("end", () => resolve(body));
|
|
161
|
+
ctx.req.on("error", reject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function resolveMode(ctx) {
|
|
165
|
+
var _a;
|
|
166
|
+
const headerMode = (_a = ctx.get("X-AI-Mode")) == null ? void 0 : _a.toLowerCase();
|
|
167
|
+
if (headerMode === "agent" || headerMode === "llm") {
|
|
168
|
+
return headerMode;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
172
|
+
if ((config == null ? void 0 : config.mode) === "agent" || (config == null ? void 0 : config.mode) === "llm") {
|
|
173
|
+
return config.mode;
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
return "llm";
|
|
178
|
+
}
|
|
179
|
+
function logRequest(ctx, requestId, model, status, durationMs) {
|
|
180
|
+
var _a, _b;
|
|
181
|
+
const userId = ((_a = ctx.state.currentUser) == null ? void 0 : _a.id) ?? "anon";
|
|
182
|
+
(_b = ctx.app.logger) == null ? void 0 : _b.info(
|
|
183
|
+
`[ai-api] ${ctx.method} ${ctx.path} requestId=${requestId} userId=${userId} model=${model} status=${status} duration=${durationMs}ms`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
187
|
+
0 && (module.exports = {
|
|
188
|
+
createAiLlmRouter
|
|
189
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface ParsedModel {
|
|
2
|
+
llmService: string;
|
|
3
|
+
modelId: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse OpenAI-style model string into NocoBase llmService + modelId.
|
|
7
|
+
* Format: "llmServiceName/modelId" (e.g. "my-openai/gpt-4o")
|
|
8
|
+
* If no "/" is present, the entire string is treated as modelId and llmService is empty.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseModelString(model: string): ParsedModel;
|
|
11
|
+
export declare function generateCompletionId(): string;
|
|
12
|
+
export declare function toOpenAIError(statusCode: number, message: string, type?: string, code?: string): {
|
|
13
|
+
error: {
|
|
14
|
+
message: string;
|
|
15
|
+
type: string;
|
|
16
|
+
param: any;
|
|
17
|
+
code: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function toOpenAIResponse(options: {
|
|
21
|
+
id: string;
|
|
22
|
+
model: string;
|
|
23
|
+
content: string;
|
|
24
|
+
finishReason?: string;
|
|
25
|
+
usage?: {
|
|
26
|
+
prompt_tokens?: number;
|
|
27
|
+
completion_tokens?: number;
|
|
28
|
+
total_tokens?: number;
|
|
29
|
+
};
|
|
30
|
+
}): {
|
|
31
|
+
id: string;
|
|
32
|
+
object: string;
|
|
33
|
+
created: number;
|
|
34
|
+
model: string;
|
|
35
|
+
system_fingerprint: any;
|
|
36
|
+
choices: {
|
|
37
|
+
index: number;
|
|
38
|
+
message: {
|
|
39
|
+
role: string;
|
|
40
|
+
content: string;
|
|
41
|
+
};
|
|
42
|
+
logprobs: any;
|
|
43
|
+
finish_reason: string;
|
|
44
|
+
}[];
|
|
45
|
+
usage: {
|
|
46
|
+
prompt_tokens?: number;
|
|
47
|
+
completion_tokens?: number;
|
|
48
|
+
total_tokens?: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function toOpenAIStreamChunk(options: {
|
|
52
|
+
id: string;
|
|
53
|
+
model: string;
|
|
54
|
+
delta: {
|
|
55
|
+
role?: string;
|
|
56
|
+
content?: string;
|
|
57
|
+
};
|
|
58
|
+
finishReason?: string | null;
|
|
59
|
+
}): {
|
|
60
|
+
id: string;
|
|
61
|
+
object: string;
|
|
62
|
+
created: number;
|
|
63
|
+
model: string;
|
|
64
|
+
system_fingerprint: any;
|
|
65
|
+
choices: {
|
|
66
|
+
index: number;
|
|
67
|
+
delta: {
|
|
68
|
+
role?: string;
|
|
69
|
+
content?: string;
|
|
70
|
+
};
|
|
71
|
+
logprobs: any;
|
|
72
|
+
finish_reason: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
export declare function toOpenAIEmbeddingsResponse(options: {
|
|
76
|
+
model: string;
|
|
77
|
+
embeddings: number[][];
|
|
78
|
+
promptTokens?: number;
|
|
79
|
+
}): {
|
|
80
|
+
object: "list";
|
|
81
|
+
data: {
|
|
82
|
+
object: "embedding";
|
|
83
|
+
embedding: number[];
|
|
84
|
+
index: number;
|
|
85
|
+
}[];
|
|
86
|
+
model: string;
|
|
87
|
+
usage: {
|
|
88
|
+
prompt_tokens: number;
|
|
89
|
+
total_tokens: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Format a streaming chunk as an SSE data line.
|
|
94
|
+
*/
|
|
95
|
+
export declare function formatSSE(data: any): string;
|
|
96
|
+
/**
|
|
97
|
+
* Format the terminal SSE [DONE] signal.
|
|
98
|
+
*/
|
|
99
|
+
export declare function formatSSEDone(): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var openai_format_exports = {};
|
|
29
|
+
__export(openai_format_exports, {
|
|
30
|
+
formatSSE: () => formatSSE,
|
|
31
|
+
formatSSEDone: () => formatSSEDone,
|
|
32
|
+
generateCompletionId: () => generateCompletionId,
|
|
33
|
+
parseModelString: () => parseModelString,
|
|
34
|
+
toOpenAIEmbeddingsResponse: () => toOpenAIEmbeddingsResponse,
|
|
35
|
+
toOpenAIError: () => toOpenAIError,
|
|
36
|
+
toOpenAIResponse: () => toOpenAIResponse,
|
|
37
|
+
toOpenAIStreamChunk: () => toOpenAIStreamChunk
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(openai_format_exports);
|
|
40
|
+
var import_crypto = __toESM(require("crypto"));
|
|
41
|
+
function parseModelString(model) {
|
|
42
|
+
const slashIndex = model.indexOf("/");
|
|
43
|
+
if (slashIndex === -1) {
|
|
44
|
+
return { llmService: "", modelId: model };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
llmService: model.substring(0, slashIndex),
|
|
48
|
+
modelId: model.substring(slashIndex + 1)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function generateCompletionId() {
|
|
52
|
+
return `chatcmpl-${import_crypto.default.randomBytes(16).toString("hex").substring(0, 29)}`;
|
|
53
|
+
}
|
|
54
|
+
function toOpenAIError(statusCode, message, type = "invalid_request_error", code) {
|
|
55
|
+
return {
|
|
56
|
+
error: {
|
|
57
|
+
message,
|
|
58
|
+
type,
|
|
59
|
+
param: null,
|
|
60
|
+
code: code || null
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function toOpenAIResponse(options) {
|
|
65
|
+
const { id, model, content, finishReason = "stop", usage } = options;
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
object: "chat.completion",
|
|
69
|
+
created: Math.floor(Date.now() / 1e3),
|
|
70
|
+
model,
|
|
71
|
+
system_fingerprint: null,
|
|
72
|
+
choices: [
|
|
73
|
+
{
|
|
74
|
+
index: 0,
|
|
75
|
+
message: {
|
|
76
|
+
role: "assistant",
|
|
77
|
+
content
|
|
78
|
+
},
|
|
79
|
+
logprobs: null,
|
|
80
|
+
finish_reason: finishReason
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function toOpenAIStreamChunk(options) {
|
|
87
|
+
const { id, model, delta, finishReason = null } = options;
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
object: "chat.completion.chunk",
|
|
91
|
+
created: Math.floor(Date.now() / 1e3),
|
|
92
|
+
model,
|
|
93
|
+
system_fingerprint: null,
|
|
94
|
+
choices: [
|
|
95
|
+
{
|
|
96
|
+
index: 0,
|
|
97
|
+
delta,
|
|
98
|
+
logprobs: null,
|
|
99
|
+
finish_reason: finishReason
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function toOpenAIEmbeddingsResponse(options) {
|
|
105
|
+
const { model, embeddings, promptTokens = 0 } = options;
|
|
106
|
+
return {
|
|
107
|
+
object: "list",
|
|
108
|
+
data: embeddings.map((embedding, index) => ({
|
|
109
|
+
object: "embedding",
|
|
110
|
+
embedding,
|
|
111
|
+
index
|
|
112
|
+
})),
|
|
113
|
+
model,
|
|
114
|
+
usage: {
|
|
115
|
+
prompt_tokens: promptTokens,
|
|
116
|
+
total_tokens: promptTokens
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function formatSSE(data) {
|
|
121
|
+
return `data: ${JSON.stringify(data)}
|
|
122
|
+
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
function formatSSEDone() {
|
|
126
|
+
return `data: [DONE]
|
|
127
|
+
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
131
|
+
0 && (module.exports = {
|
|
132
|
+
formatSSE,
|
|
133
|
+
formatSSEDone,
|
|
134
|
+
generateCompletionId,
|
|
135
|
+
parseModelString,
|
|
136
|
+
toOpenAIEmbeddingsResponse,
|
|
137
|
+
toOpenAIError,
|
|
138
|
+
toOpenAIResponse,
|
|
139
|
+
toOpenAIStreamChunk
|
|
140
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory sliding window rate limiter.
|
|
3
|
+
*
|
|
4
|
+
* Stores per-user request timestamps. On each check: prunes timestamps
|
|
5
|
+
* older than the window, counts the remainder, and accepts/rejects.
|
|
6
|
+
*
|
|
7
|
+
* Single-process safe (Node.js event loop). Not distributed.
|
|
8
|
+
* For multi-process deployments, replace with a Redis-backed implementation.
|
|
9
|
+
*/
|
|
10
|
+
export declare class RateLimiter {
|
|
11
|
+
private readonly windowMs;
|
|
12
|
+
/** Map<userId, sorted array of request timestamps in ms> */
|
|
13
|
+
private readonly store;
|
|
14
|
+
constructor(windowMs?: number);
|
|
15
|
+
/**
|
|
16
|
+
* Check and record a request for a user.
|
|
17
|
+
*
|
|
18
|
+
* @param userId The user ID (string or numeric)
|
|
19
|
+
* @param limit Max allowed requests per window (from aiApiConfig.rateLimitPerMinute)
|
|
20
|
+
* @returns { allowed: true } or { allowed: false, retryAfterMs: number }
|
|
21
|
+
*/
|
|
22
|
+
check(userId: string | number, limit: number): {
|
|
23
|
+
allowed: true;
|
|
24
|
+
} | {
|
|
25
|
+
allowed: false;
|
|
26
|
+
retryAfterMs: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Garbage collect entries for inactive users.
|
|
30
|
+
* Call every ~5 minutes to prevent unbounded memory growth in long-running servers.
|
|
31
|
+
*/
|
|
32
|
+
gc(): void;
|
|
33
|
+
/** Clear all state (useful in tests). */
|
|
34
|
+
clear(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var rate_limiter_exports = {};
|
|
19
|
+
__export(rate_limiter_exports, {
|
|
20
|
+
RateLimiter: () => RateLimiter
|
|
21
|
+
});
|
|
22
|
+
module.exports = __toCommonJS(rate_limiter_exports);
|
|
23
|
+
class RateLimiter {
|
|
24
|
+
constructor(windowMs = 6e4) {
|
|
25
|
+
this.windowMs = windowMs;
|
|
26
|
+
}
|
|
27
|
+
/** Map<userId, sorted array of request timestamps in ms> */
|
|
28
|
+
store = /* @__PURE__ */ new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Check and record a request for a user.
|
|
31
|
+
*
|
|
32
|
+
* @param userId The user ID (string or numeric)
|
|
33
|
+
* @param limit Max allowed requests per window (from aiApiConfig.rateLimitPerMinute)
|
|
34
|
+
* @returns { allowed: true } or { allowed: false, retryAfterMs: number }
|
|
35
|
+
*/
|
|
36
|
+
check(userId, limit) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const windowStart = now - this.windowMs;
|
|
39
|
+
let timestamps = this.store.get(userId);
|
|
40
|
+
if (!timestamps) {
|
|
41
|
+
timestamps = [];
|
|
42
|
+
this.store.set(userId, timestamps);
|
|
43
|
+
}
|
|
44
|
+
let lo = 0, hi = timestamps.length;
|
|
45
|
+
while (lo < hi) {
|
|
46
|
+
const mid = lo + hi >>> 1;
|
|
47
|
+
if (timestamps[mid] < windowStart) {
|
|
48
|
+
lo = mid + 1;
|
|
49
|
+
} else {
|
|
50
|
+
hi = mid;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (lo > 0) timestamps.splice(0, lo);
|
|
54
|
+
if (timestamps.length >= limit) {
|
|
55
|
+
const retryAfterMs = Math.max(0, timestamps[0] + this.windowMs - now);
|
|
56
|
+
return { allowed: false, retryAfterMs };
|
|
57
|
+
}
|
|
58
|
+
timestamps.push(now);
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Garbage collect entries for inactive users.
|
|
63
|
+
* Call every ~5 minutes to prevent unbounded memory growth in long-running servers.
|
|
64
|
+
*/
|
|
65
|
+
gc() {
|
|
66
|
+
const cutoff = Date.now() - this.windowMs;
|
|
67
|
+
for (const [userId, timestamps] of this.store) {
|
|
68
|
+
if (!timestamps.length || timestamps[timestamps.length - 1] < cutoff) {
|
|
69
|
+
this.store.delete(userId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Clear all state (useful in tests). */
|
|
74
|
+
clear() {
|
|
75
|
+
this.store.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
79
|
+
0 && (module.exports = {
|
|
80
|
+
RateLimiter
|
|
81
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an LLM service by name or title.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveLlmService(ctx: Context, serviceKey: string): Promise<any>;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a model string to a service + modelId.
|
|
8
|
+
*
|
|
9
|
+
* Strategy (priority order):
|
|
10
|
+
* 1. Try splitting at each "/" position and match the left part against DB (name or title).
|
|
11
|
+
* This handles cases like "Custom LLM (OpenAI Compatible)/qwen/qwen3.6-plus-preview:free"
|
|
12
|
+
* 2. If no service match, use the defaultLlmService from config and treat the ENTIRE
|
|
13
|
+
* model string as the modelId. This allows clients to send just "qwen/qwen3.6-plus-preview:free"
|
|
14
|
+
* or "gpt-4o" without knowing the service name.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveModelString(ctx: Context, modelString: string): Promise<{
|
|
17
|
+
service: any;
|
|
18
|
+
modelId: string;
|
|
19
|
+
} | null>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var resolve_service_exports = {};
|
|
19
|
+
__export(resolve_service_exports, {
|
|
20
|
+
resolveLlmService: () => resolveLlmService,
|
|
21
|
+
resolveModelString: () => resolveModelString
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(resolve_service_exports);
|
|
24
|
+
async function resolveLlmService(ctx, serviceKey) {
|
|
25
|
+
const repo = ctx.db.getRepository("llmServices");
|
|
26
|
+
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
27
|
+
if (!service) {
|
|
28
|
+
service = await repo.findOne({ filter: { title: serviceKey } });
|
|
29
|
+
}
|
|
30
|
+
return service;
|
|
31
|
+
}
|
|
32
|
+
async function resolveModelString(ctx, modelString) {
|
|
33
|
+
const repo = ctx.db.getRepository("llmServices");
|
|
34
|
+
const slashPositions = [];
|
|
35
|
+
for (let i = 0; i < modelString.length; i++) {
|
|
36
|
+
if (modelString[i] === "/") {
|
|
37
|
+
slashPositions.push(i);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (slashPositions.length > 0) {
|
|
41
|
+
for (const pos of slashPositions) {
|
|
42
|
+
const serviceKey = modelString.substring(0, pos);
|
|
43
|
+
const modelId = modelString.substring(pos + 1);
|
|
44
|
+
if (!serviceKey || !modelId) continue;
|
|
45
|
+
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
46
|
+
if (!service) {
|
|
47
|
+
service = await repo.findOne({ filter: { title: serviceKey } });
|
|
48
|
+
}
|
|
49
|
+
if (service) {
|
|
50
|
+
return { service, modelId };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const config = await ctx.db.getRepository("aiApiConfig").findOne();
|
|
55
|
+
if (config == null ? void 0 : config.defaultLlmService) {
|
|
56
|
+
const service = await repo.findOne({ filter: { name: config.defaultLlmService } });
|
|
57
|
+
if (service) {
|
|
58
|
+
return { service, modelId: modelString };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const enabledServices = await repo.find({ filter: { enabled: true } });
|
|
62
|
+
if (enabledServices.length === 1) {
|
|
63
|
+
return { service: enabledServices[0], modelId: modelString };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
resolveLlmService,
|
|
70
|
+
resolveModelString
|
|
71
|
+
});
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plugin-ai-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/server/index.js",
|
|
5
|
+
"dependencies": {},
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"@nocobase/client": "2.x",
|
|
8
|
+
"@nocobase/server": "2.x",
|
|
9
|
+
"@nocobase/database": "2.x",
|
|
10
|
+
"@nocobase/plugin-ai": "2.x",
|
|
11
|
+
"@nocobase/plugin-api-keys": "2.x"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/server.d.ts
ADDED
package/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/server/index.js');
|