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.
Files changed (49) hide show
  1. package/README.md +1 -0
  2. package/client.d.ts +2 -0
  3. package/client.js +1 -0
  4. package/dist/client/824c2f7487ee05fd.js +1 -0
  5. package/dist/client/AiApiConfigPage.d.ts +3 -0
  6. package/dist/client/index.d.ts +1 -0
  7. package/dist/client/index.js +1 -0
  8. package/dist/client/locale.d.ts +10 -0
  9. package/dist/client/models/index.d.ts +10 -0
  10. package/dist/client/plugin.d.ts +6 -0
  11. package/dist/externalVersion.js +11 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +39 -0
  14. package/dist/locale/en-US.json +10 -0
  15. package/dist/locale/zh-CN.json +10 -0
  16. package/dist/server/collections/ai-api-config.d.ts +2 -0
  17. package/dist/server/collections/ai-api-config.js +63 -0
  18. package/dist/server/index.d.ts +1 -0
  19. package/dist/server/index.js +33 -0
  20. package/dist/server/middleware/rate-limit.d.ts +18 -0
  21. package/dist/server/middleware/rate-limit.js +61 -0
  22. package/dist/server/plugin.d.ts +18 -0
  23. package/dist/server/plugin.js +87 -0
  24. package/dist/server/resource/ai-api-config.d.ts +10 -0
  25. package/dist/server/resource/ai-api-config.js +73 -0
  26. package/dist/server/routes/agent-completions.d.ts +34 -0
  27. package/dist/server/routes/agent-completions.js +300 -0
  28. package/dist/server/routes/auth.d.ts +8 -0
  29. package/dist/server/routes/auth.js +110 -0
  30. package/dist/server/routes/chat-completions.d.ts +9 -0
  31. package/dist/server/routes/chat-completions.js +237 -0
  32. package/dist/server/routes/completions.d.ts +10 -0
  33. package/dist/server/routes/completions.js +245 -0
  34. package/dist/server/routes/embeddings.d.ts +22 -0
  35. package/dist/server/routes/embeddings.js +166 -0
  36. package/dist/server/routes/models.d.ts +21 -0
  37. package/dist/server/routes/models.js +154 -0
  38. package/dist/server/routes/router.d.ts +25 -0
  39. package/dist/server/routes/router.js +189 -0
  40. package/dist/server/utils/openai-format.d.ts +99 -0
  41. package/dist/server/utils/openai-format.js +140 -0
  42. package/dist/server/utils/rate-limiter.d.ts +35 -0
  43. package/dist/server/utils/rate-limiter.js +81 -0
  44. package/dist/server/utils/resolve-service.d.ts +19 -0
  45. package/dist/server/utils/resolve-service.js +71 -0
  46. package/nocobase-plugin-ai-api-2.0.20.tgz +0 -0
  47. package/package.json +13 -0
  48. package/server.d.ts +2 -0
  49. 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
+ });
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
@@ -0,0 +1,2 @@
1
+ export * from './dist/server';
2
+ export { default } from './dist/server';
package/server.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/server/index.js');