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,245 @@
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 completions_exports = {};
19
+ __export(completions_exports, {
20
+ handleCompletions: () => handleCompletions
21
+ });
22
+ module.exports = __toCommonJS(completions_exports);
23
+ var import_openai_format = require("../utils/openai-format");
24
+ var import_resolve_service = require("../utils/resolve-service");
25
+ async function handleCompletions(ctx, plugin) {
26
+ var _a;
27
+ const body = ctx.request.body;
28
+ if (!(body == null ? void 0 : body.model)) {
29
+ ctx.status = 400;
30
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'model' is required", "invalid_request_error", "missing_model");
31
+ return;
32
+ }
33
+ if (!(body == null ? void 0 : body.prompt) && body.prompt !== "") {
34
+ ctx.status = 400;
35
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'prompt' is required", "invalid_request_error", "missing_prompt");
36
+ return;
37
+ }
38
+ if (body.n !== void 0 && body.n !== null && body.n !== 1) {
39
+ ctx.status = 400;
40
+ ctx.body = (0, import_openai_format.toOpenAIError)(
41
+ 400,
42
+ `The 'n' parameter value ${body.n} is not supported. This API gateway always returns exactly one completion (n=1). Please omit 'n' or set it to 1.`,
43
+ "invalid_request_error",
44
+ "unsupported_parameter"
45
+ );
46
+ return;
47
+ }
48
+ const stream = body.stream === true;
49
+ const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
50
+ if (!resolved) {
51
+ ctx.status = 404;
52
+ ctx.body = (0, import_openai_format.toOpenAIError)(
53
+ 404,
54
+ `Could not resolve model '${body.model}'. Format: 'serviceName/modelId'. Use GET /v1/models to see available models.`,
55
+ "invalid_request_error",
56
+ "model_not_found"
57
+ );
58
+ return;
59
+ }
60
+ const { service, modelId } = resolved;
61
+ try {
62
+ const aiPlugin = ctx.app.pm.get("ai");
63
+ if (!aiPlugin) {
64
+ ctx.status = 500;
65
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "AI plugin not available", "server_error");
66
+ return;
67
+ }
68
+ if (service.enabled === false) {
69
+ ctx.status = 404;
70
+ ctx.body = (0, import_openai_format.toOpenAIError)(404, `LLM service '${service.title || service.name}' is disabled`, "invalid_request_error", "model_not_found");
71
+ return;
72
+ }
73
+ const config = await ctx.db.getRepository("aiApiConfig").findOne();
74
+ if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
75
+ const serviceName = service.name;
76
+ const serviceTitle = service.title;
77
+ const isAllowed = config.enabledLlmServices.some(
78
+ (s) => s === serviceName || s === serviceTitle
79
+ );
80
+ if (!isAllowed) {
81
+ ctx.status = 403;
82
+ ctx.body = (0, import_openai_format.toOpenAIError)(
83
+ 403,
84
+ `LLM service '${service.title || service.name}' is not enabled for API access`,
85
+ "invalid_request_error",
86
+ "model_not_available"
87
+ );
88
+ return;
89
+ }
90
+ }
91
+ const providerMeta = aiPlugin.aiManager.llmProviders.get(service.provider);
92
+ if (!providerMeta) {
93
+ ctx.status = 500;
94
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
95
+ return;
96
+ }
97
+ const modelOptions = {
98
+ model: modelId,
99
+ llmService: service.name
100
+ };
101
+ if (body.temperature !== void 0) modelOptions.temperature = body.temperature;
102
+ if (body.top_p !== void 0) modelOptions.topP = body.top_p;
103
+ if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
104
+ if (body.stop !== void 0) modelOptions.stop = body.stop;
105
+ const Provider = providerMeta.provider;
106
+ const provider = new Provider({
107
+ app: ctx.app,
108
+ serviceOptions: service.options,
109
+ modelOptions
110
+ });
111
+ const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
112
+ const langchainMessages = [];
113
+ if (config == null ? void 0 : config.defaultAiEmployee) {
114
+ const employee = await ctx.db.getRepository("aiEmployees").findOne({
115
+ filter: { username: config.defaultAiEmployee }
116
+ });
117
+ if (employee) {
118
+ const systemPrompt = employee.about || employee.defaultPrompt || "";
119
+ if (systemPrompt) {
120
+ langchainMessages.push(["system", systemPrompt]);
121
+ }
122
+ }
123
+ }
124
+ langchainMessages.push(["human", prompt]);
125
+ const completionId = (0, import_openai_format.generateCompletionId)().replace("chatcmpl-", "cmpl-");
126
+ const chatModel = provider.createModel();
127
+ if (stream) {
128
+ await handleStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
129
+ } else {
130
+ await handleNonStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
131
+ }
132
+ } catch (err) {
133
+ ctx.log.error("AI API completions error:", err);
134
+ if (!ctx.res.headersSent) {
135
+ ctx.status = 500;
136
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
137
+ }
138
+ }
139
+ }
140
+ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
141
+ const result = await chatModel.invoke(messages);
142
+ let text = "";
143
+ if (typeof result.content === "string") {
144
+ text = result.content;
145
+ } else if (Array.isArray(result.content)) {
146
+ const textPart = result.content.find((c) => c.type === "text");
147
+ text = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
148
+ }
149
+ const usage = result.usage_metadata ? {
150
+ prompt_tokens: result.usage_metadata.input_tokens || 0,
151
+ completion_tokens: result.usage_metadata.output_tokens || 0,
152
+ total_tokens: result.usage_metadata.total_tokens || 0
153
+ } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
154
+ ctx.status = 200;
155
+ ctx.body = {
156
+ id: completionId,
157
+ object: "text_completion",
158
+ created: Math.floor(Date.now() / 1e3),
159
+ model: modelName,
160
+ system_fingerprint: null,
161
+ choices: [
162
+ {
163
+ text,
164
+ index: 0,
165
+ logprobs: null,
166
+ finish_reason: "stop"
167
+ }
168
+ ],
169
+ usage
170
+ };
171
+ }
172
+ async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
173
+ ctx.set({
174
+ "Content-Type": "text/event-stream",
175
+ "Cache-Control": "no-cache",
176
+ Connection: "keep-alive",
177
+ "X-Accel-Buffering": "no"
178
+ });
179
+ ctx.status = 200;
180
+ try {
181
+ const stream = await chatModel.stream(messages);
182
+ for await (const chunk of stream) {
183
+ let text = "";
184
+ if (typeof chunk.content === "string") {
185
+ text = chunk.content;
186
+ } else if (Array.isArray(chunk.content)) {
187
+ const textPart = chunk.content.find((c) => c.type === "text");
188
+ text = (textPart == null ? void 0 : textPart.text) || "";
189
+ }
190
+ if (text) {
191
+ ctx.res.write(
192
+ (0, import_openai_format.formatSSE)({
193
+ id: completionId,
194
+ object: "text_completion",
195
+ created: Math.floor(Date.now() / 1e3),
196
+ model: modelName,
197
+ system_fingerprint: null,
198
+ choices: [
199
+ {
200
+ text,
201
+ index: 0,
202
+ logprobs: null,
203
+ finish_reason: null
204
+ }
205
+ ]
206
+ })
207
+ );
208
+ }
209
+ }
210
+ ctx.res.write(
211
+ (0, import_openai_format.formatSSE)({
212
+ id: completionId,
213
+ object: "text_completion",
214
+ created: Math.floor(Date.now() / 1e3),
215
+ model: modelName,
216
+ system_fingerprint: null,
217
+ choices: [
218
+ {
219
+ text: "",
220
+ index: 0,
221
+ logprobs: null,
222
+ finish_reason: "stop"
223
+ }
224
+ ]
225
+ })
226
+ );
227
+ ctx.res.write((0, import_openai_format.formatSSEDone)());
228
+ } catch (err) {
229
+ ctx.log.error("AI API completions streaming error:", err);
230
+ ctx.res.write(
231
+ (0, import_openai_format.formatSSE)({
232
+ error: {
233
+ message: err.message || "Streaming error",
234
+ type: "server_error"
235
+ }
236
+ })
237
+ );
238
+ } finally {
239
+ ctx.res.end();
240
+ }
241
+ }
242
+ // Annotate the CommonJS export names for ESM import in node:
243
+ 0 && (module.exports = {
244
+ handleCompletions
245
+ });
@@ -0,0 +1,22 @@
1
+ import { Context } from '@nocobase/actions';
2
+ import type PluginAiApiServer from '../plugin';
3
+ /**
4
+ * POST /api/ai-llm/v1/embeddings
5
+ *
6
+ * OpenAI-compatible embeddings endpoint.
7
+ *
8
+ * Supported providers (those with an `embedding` field in LLMProviderMeta):
9
+ * - openai → OpenAiEmbeddingProvider
10
+ * - openai-completions → OpenAiEmbeddingProvider
11
+ * - dashscope → DashscopeEmbeddingProvider
12
+ * - google-genai → GoogleGenAIEmbeddingProvider
13
+ * - ollama → OllamaEmbeddingProvider
14
+ *
15
+ * Not supported: anthropic, deepseek, kimi (no embedding provider registered).
16
+ *
17
+ * Limitations:
18
+ * - encoding_format 'base64' is not supported (always returns float arrays)
19
+ * - Token counts always return 0 (LangChain embeddings API doesn't expose this)
20
+ * - Token array input (integer[]) is not supported, only string input
21
+ */
22
+ export declare function handleEmbeddings(ctx: Context, plugin: PluginAiApiServer): Promise<void>;
@@ -0,0 +1,166 @@
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 embeddings_exports = {};
19
+ __export(embeddings_exports, {
20
+ handleEmbeddings: () => handleEmbeddings
21
+ });
22
+ module.exports = __toCommonJS(embeddings_exports);
23
+ var import_openai_format = require("../utils/openai-format");
24
+ var import_resolve_service = require("../utils/resolve-service");
25
+ async function handleEmbeddings(ctx, plugin) {
26
+ var _a;
27
+ const body = ctx.request.body;
28
+ if (!(body == null ? void 0 : body.model)) {
29
+ ctx.status = 400;
30
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'model' is required", "invalid_request_error", "missing_model");
31
+ return;
32
+ }
33
+ if (body.input === void 0 || body.input === null) {
34
+ ctx.status = 400;
35
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'input' is required", "invalid_request_error", "missing_input");
36
+ return;
37
+ }
38
+ if (body.encoding_format === "base64") {
39
+ ctx.status = 400;
40
+ ctx.body = (0, import_openai_format.toOpenAIError)(
41
+ 400,
42
+ "encoding_format 'base64' is not supported. Use 'float' (default) or omit the parameter.",
43
+ "invalid_request_error",
44
+ "unsupported_encoding_format"
45
+ );
46
+ return;
47
+ }
48
+ let inputs;
49
+ if (typeof body.input === "string") {
50
+ inputs = [body.input];
51
+ } else if (Array.isArray(body.input)) {
52
+ if (body.input.length === 0) {
53
+ ctx.status = 400;
54
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'input' array must not be empty", "invalid_request_error");
55
+ return;
56
+ }
57
+ if (typeof body.input[0] === "number") {
58
+ ctx.status = 400;
59
+ ctx.body = (0, import_openai_format.toOpenAIError)(
60
+ 400,
61
+ "Token array input is not supported. Please provide string input.",
62
+ "invalid_request_error",
63
+ "unsupported_input_type"
64
+ );
65
+ return;
66
+ }
67
+ inputs = body.input;
68
+ } else {
69
+ ctx.status = 400;
70
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'input' must be a string or array of strings", "invalid_request_error");
71
+ return;
72
+ }
73
+ const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
74
+ if (!resolved) {
75
+ ctx.status = 404;
76
+ ctx.body = (0, import_openai_format.toOpenAIError)(
77
+ 404,
78
+ `Could not resolve model '${body.model}'. Use GET /v1/models to list available models.`,
79
+ "invalid_request_error",
80
+ "model_not_found"
81
+ );
82
+ return;
83
+ }
84
+ const { service, modelId } = resolved;
85
+ if (service.enabled === false) {
86
+ ctx.status = 404;
87
+ ctx.body = (0, import_openai_format.toOpenAIError)(
88
+ 404,
89
+ `LLM service '${service.title || service.name}' is disabled`,
90
+ "invalid_request_error",
91
+ "model_not_found"
92
+ );
93
+ return;
94
+ }
95
+ try {
96
+ const config = await ctx.db.getRepository("aiApiConfig").findOne();
97
+ if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
98
+ const allowed = config.enabledLlmServices.some(
99
+ (s) => s === service.name || s === service.title
100
+ );
101
+ if (!allowed) {
102
+ ctx.status = 403;
103
+ ctx.body = (0, import_openai_format.toOpenAIError)(
104
+ 403,
105
+ `LLM service '${service.title || service.name}' is not enabled for API access`,
106
+ "invalid_request_error",
107
+ "model_not_available"
108
+ );
109
+ return;
110
+ }
111
+ }
112
+ } catch {
113
+ }
114
+ const aiPlugin = ctx.app.pm.get("ai");
115
+ if (!aiPlugin) {
116
+ ctx.status = 500;
117
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "AI plugin not available", "server_error");
118
+ return;
119
+ }
120
+ const providerMeta = aiPlugin.aiManager.llmProviders.get(service.provider);
121
+ if (!providerMeta) {
122
+ ctx.status = 500;
123
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
124
+ return;
125
+ }
126
+ if (!providerMeta.embedding) {
127
+ ctx.status = 400;
128
+ ctx.body = (0, import_openai_format.toOpenAIError)(
129
+ 400,
130
+ `Provider '${providerMeta.title || service.provider}' does not support embeddings. Embedding-capable providers: openai, openai-completions, dashscope, google-genai, ollama.`,
131
+ "invalid_request_error",
132
+ "model_not_supported"
133
+ );
134
+ return;
135
+ }
136
+ try {
137
+ const EmbeddingClass = providerMeta.embedding;
138
+ const embeddingProvider = new EmbeddingClass({
139
+ app: ctx.app,
140
+ serviceOptions: service.options,
141
+ // Contains apiKey, baseURL, etc.
142
+ modelOptions: { model: modelId }
143
+ // The specific embedding model
144
+ });
145
+ const embeddingModel = embeddingProvider.createEmbedding();
146
+ const vectors = await embeddingModel.embedDocuments(inputs);
147
+ ctx.status = 200;
148
+ ctx.set("Content-Type", "application/json");
149
+ ctx.body = (0, import_openai_format.toOpenAIEmbeddingsResponse)({
150
+ model: body.model,
151
+ embeddings: vectors,
152
+ // LangChain's EmbeddingsInterface does not expose token counts.
153
+ promptTokens: 0
154
+ });
155
+ } catch (err) {
156
+ ctx.log.error("AI API embeddings error:", err);
157
+ if (!ctx.res.headersSent) {
158
+ ctx.status = 500;
159
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Failed to generate embeddings", "server_error");
160
+ }
161
+ }
162
+ }
163
+ // Annotate the CommonJS export names for ESM import in node:
164
+ 0 && (module.exports = {
165
+ handleEmbeddings
166
+ });
@@ -0,0 +1,21 @@
1
+ import { Context } from '@nocobase/actions';
2
+ import type PluginAiApiServer from '../plugin';
3
+ /**
4
+ * GET /api/ai-llm/v1/models
5
+ *
6
+ * Lists all available models from enabled LLM services.
7
+ * Model IDs use the "serviceName/modelId" format (e.g. "my-openai/gpt-4o")
8
+ * so clients can copy-paste the ID directly into POST /v1/chat/completions
9
+ * without needing to configure a defaultLlmService.
10
+ *
11
+ * Backward compatibility: resolveModelString() in resolve-service.ts still
12
+ * accepts bare model IDs via its 3-tier fallback (defaultLlmService / single service).
13
+ */
14
+ export declare function handleListModels(ctx: Context, plugin: PluginAiApiServer): Promise<void>;
15
+ /**
16
+ * GET /api/ai-llm/v1/models/:model
17
+ *
18
+ * Retrieve a single model by ID.
19
+ * Accepts both "serviceName/modelId" format (new) and bare "modelId" (backward compat).
20
+ */
21
+ export declare function handleGetModel(ctx: Context, modelId: string, plugin: PluginAiApiServer): Promise<void>;
@@ -0,0 +1,154 @@
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 models_exports = {};
19
+ __export(models_exports, {
20
+ handleGetModel: () => handleGetModel,
21
+ handleListModels: () => handleListModels
22
+ });
23
+ module.exports = __toCommonJS(models_exports);
24
+ var import_openai_format = require("../utils/openai-format");
25
+ async function handleListModels(ctx, plugin) {
26
+ var _a;
27
+ try {
28
+ const aiPlugin = ctx.app.pm.get("ai");
29
+ if (!aiPlugin) {
30
+ ctx.status = 500;
31
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "AI plugin not available", "server_error");
32
+ return;
33
+ }
34
+ const config = await getPluginConfig(ctx);
35
+ const filter = {};
36
+ if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
37
+ filter.$or = [
38
+ { name: { $in: config.enabledLlmServices } },
39
+ { title: { $in: config.enabledLlmServices } }
40
+ ];
41
+ }
42
+ const services = await ctx.db.getRepository("llmServices").find({
43
+ filter,
44
+ sort: "sort"
45
+ });
46
+ const now = Math.floor(Date.now() / 1e3);
47
+ const models = [];
48
+ for (const service of services) {
49
+ if (service.enabled === false) continue;
50
+ const enabledModels = resolveEnabledModels(service);
51
+ const serviceLabel = service.title || service.name;
52
+ for (const model of enabledModels) {
53
+ models.push({
54
+ // Use "serviceName/modelId" format so the ID can be used directly in
55
+ // POST /v1/chat/completions without ambiguity in multi-service setups.
56
+ id: `${service.name}/${model.value}`,
57
+ object: "model",
58
+ created: now,
59
+ owned_by: serviceLabel
60
+ });
61
+ }
62
+ }
63
+ ctx.status = 200;
64
+ ctx.body = {
65
+ object: "list",
66
+ data: models
67
+ };
68
+ } catch (err) {
69
+ ctx.log.error("AI API list models error:", err);
70
+ ctx.status = 500;
71
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "Failed to list models", "server_error");
72
+ }
73
+ }
74
+ async function handleGetModel(ctx, modelId, plugin) {
75
+ var _a;
76
+ try {
77
+ const config = await getPluginConfig(ctx);
78
+ const filter = {};
79
+ if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
80
+ filter.$or = [
81
+ { name: { $in: config.enabledLlmServices } },
82
+ { title: { $in: config.enabledLlmServices } }
83
+ ];
84
+ }
85
+ const services = await ctx.db.getRepository("llmServices").find({
86
+ filter,
87
+ sort: "sort"
88
+ });
89
+ const now = Math.floor(Date.now() / 1e3);
90
+ let found = null;
91
+ for (const service of services) {
92
+ if (service.enabled === false) continue;
93
+ const enabledModels = resolveEnabledModels(service);
94
+ const serviceLabel = service.title || service.name;
95
+ for (const model of enabledModels) {
96
+ const fullId = `${service.name}/${model.value}`;
97
+ if (fullId === modelId || model.value === modelId) {
98
+ found = {
99
+ id: fullId,
100
+ object: "model",
101
+ created: now,
102
+ owned_by: serviceLabel
103
+ };
104
+ break;
105
+ }
106
+ }
107
+ if (found) break;
108
+ }
109
+ if (!found) {
110
+ ctx.status = 404;
111
+ ctx.body = (0, import_openai_format.toOpenAIError)(404, `Model '${modelId}' not found`, "invalid_request_error", "model_not_found");
112
+ return;
113
+ }
114
+ ctx.status = 200;
115
+ ctx.body = found;
116
+ } catch (err) {
117
+ ctx.log.error("AI API get model error:", err);
118
+ ctx.status = 500;
119
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "Failed to retrieve model", "server_error");
120
+ }
121
+ }
122
+ async function getPluginConfig(ctx) {
123
+ return ctx.db.getRepository("aiApiConfig").findOne();
124
+ }
125
+ function resolveEnabledModels(service) {
126
+ const raw = service.enabledModels;
127
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && raw.mode) {
128
+ if (raw.mode === "recommended") {
129
+ return getRecommendedModelsForProvider(service.provider);
130
+ }
131
+ return (raw.models || []).filter((m) => m.value).map((m) => ({ label: m.label || m.value, value: m.value }));
132
+ }
133
+ if (Array.isArray(raw)) {
134
+ if (raw.length === 0) {
135
+ return getRecommendedModelsForProvider(service.provider);
136
+ }
137
+ return raw.map((id) => ({ label: id, value: id }));
138
+ }
139
+ return getRecommendedModelsForProvider(service.provider);
140
+ }
141
+ function getRecommendedModelsForProvider(provider) {
142
+ try {
143
+ const { getRecommendedModels } = require("@nocobase/plugin-ai/src/common/recommended-models");
144
+ const models = getRecommendedModels(provider);
145
+ return Array.isArray(models) ? models : [];
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+ // Annotate the CommonJS export names for ESM import in node:
151
+ 0 && (module.exports = {
152
+ handleGetModel,
153
+ handleListModels
154
+ });
@@ -0,0 +1,25 @@
1
+ import type PluginAiApiServer from '../plugin';
2
+ /**
3
+ * Main Koa middleware router for OpenAI-compatible endpoints.
4
+ *
5
+ * Intercepts all requests to /api/ai-llm/v1/* and routes them
6
+ * to the appropriate handler. Runs before NocoBase's resourcer
7
+ * so the URL paths follow OpenAI convention.
8
+ *
9
+ * Features:
10
+ * - CORS support (Access-Control-Allow-Origin: *)
11
+ * - OPTIONS preflight handling (204)
12
+ * - X-Request-Id on every response
13
+ * - Bearer token authentication
14
+ * - Sliding window rate limiting (enforces rateLimitPerMinute from config)
15
+ * - Structured request logging via app.logger
16
+ *
17
+ * Supported endpoints:
18
+ * POST /v1/chat/completions — OpenAI chat completions (LLM or agent mode)
19
+ * POST /v1/completions — Legacy text completions (LiteLLM compat)
20
+ * POST /v1/embeddings — OpenAI embeddings
21
+ * GET /v1/models — List available models
22
+ * GET /v1/models/:id — Get a single model
23
+ * DELETE /v1/models/:id — Not implemented (501 stub)
24
+ */
25
+ export declare function createAiLlmRouter(plugin: PluginAiApiServer): (ctx: Context, next: Next) => Promise<any>;