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,300 @@
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 agent_completions_exports = {};
29
+ __export(agent_completions_exports, {
30
+ handleAgentCompletions: () => handleAgentCompletions
31
+ });
32
+ module.exports = __toCommonJS(agent_completions_exports);
33
+ var import_openai_format = require("../utils/openai-format");
34
+ var import_resolve_service = require("../utils/resolve-service");
35
+ async function handleAgentCompletions(ctx, plugin) {
36
+ var _a;
37
+ const body = ctx.request.body;
38
+ if (!(body == null ? void 0 : body.model)) {
39
+ ctx.status = 400;
40
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'model' is required", "invalid_request_error", "missing_model");
41
+ return;
42
+ }
43
+ if (!(body == null ? void 0 : body.messages) || !Array.isArray(body.messages) || body.messages.length === 0) {
44
+ ctx.status = 400;
45
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'messages' must be a non-empty array", "invalid_request_error", "missing_messages");
46
+ return;
47
+ }
48
+ if (body.n !== void 0 && body.n !== null && body.n !== 1) {
49
+ ctx.status = 400;
50
+ ctx.body = (0, import_openai_format.toOpenAIError)(
51
+ 400,
52
+ `The 'n' parameter value ${body.n} is not supported. This gateway always returns n=1.`,
53
+ "invalid_request_error",
54
+ "unsupported_parameter"
55
+ );
56
+ return;
57
+ }
58
+ const config = await ctx.db.getRepository("aiApiConfig").findOne();
59
+ if (!(config == null ? void 0 : config.defaultAiEmployee)) {
60
+ ctx.status = 400;
61
+ ctx.body = (0, import_openai_format.toOpenAIError)(
62
+ 400,
63
+ "Agent mode requires a Default AI Employee. Configure one in Settings > AI API Gateway.",
64
+ "invalid_request_error",
65
+ "missing_config"
66
+ );
67
+ return;
68
+ }
69
+ const resolved = await (0, import_resolve_service.resolveModelString)(ctx, body.model);
70
+ if (!resolved) {
71
+ ctx.status = 404;
72
+ ctx.body = (0, import_openai_format.toOpenAIError)(
73
+ 404,
74
+ `Could not resolve model '${body.model}'. Use GET /v1/models to see available models.`,
75
+ "invalid_request_error",
76
+ "model_not_found"
77
+ );
78
+ return;
79
+ }
80
+ const { service, modelId } = resolved;
81
+ if (service.enabled === false) {
82
+ ctx.status = 404;
83
+ ctx.body = (0, import_openai_format.toOpenAIError)(404, "LLM service is disabled", "invalid_request_error", "model_not_found");
84
+ return;
85
+ }
86
+ const employeeUsername = config.defaultAiEmployee;
87
+ const wantStream = body.stream === true;
88
+ try {
89
+ const employeeRecord = await ctx.db.getRepository("aiEmployees").findOne({
90
+ filter: { username: employeeUsername }
91
+ });
92
+ if (!employeeRecord) {
93
+ ctx.status = 400;
94
+ ctx.body = (0, import_openai_format.toOpenAIError)(
95
+ 400,
96
+ `AI employee '${employeeUsername}' not found. Check Settings > AI API Gateway.`,
97
+ "invalid_request_error",
98
+ "missing_config"
99
+ );
100
+ return;
101
+ }
102
+ const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
103
+ const conversation = await ctx.db.getRepository("aiConversations").create({
104
+ values: {
105
+ userId,
106
+ aiEmployee: { username: employeeUsername },
107
+ options: {},
108
+ thread: 1
109
+ }
110
+ });
111
+ const sessionId = conversation.sessionId ?? conversation.id ?? String(conversation.get("id"));
112
+ const userMessages = body.messages.map((msg) => ({
113
+ // 'assistant' role maps to the AI employee's username in NocoBase's system
114
+ role: msg.role === "assistant" ? employeeUsername : msg.role,
115
+ content: {
116
+ type: "text",
117
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
118
+ }
119
+ }));
120
+ const completionId = (0, import_openai_format.generateCompletionId)();
121
+ let AIEmployee;
122
+ try {
123
+ const mod = await import(
124
+ /* webpackIgnore: true */
125
+ "@nocobase/plugin-ai/src/server/ai-employees/ai-employee"
126
+ );
127
+ AIEmployee = mod.AIEmployee;
128
+ } catch (importErr) {
129
+ ctx.log.error("AI API: Failed to import AIEmployee from plugin-ai. Ensure plugin-ai is installed and built.", importErr);
130
+ ctx.status = 500;
131
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "Agent mode unavailable: plugin-ai not found or not built", "server_error");
132
+ return;
133
+ }
134
+ if (!AIEmployee) {
135
+ ctx.status = 500;
136
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "Agent mode unavailable: AIEmployee class not exported by plugin-ai", "server_error");
137
+ return;
138
+ }
139
+ if (wantStream) {
140
+ ctx.set({
141
+ "Content-Type": "text/event-stream",
142
+ "Cache-Control": "no-cache",
143
+ Connection: "keep-alive",
144
+ "X-Accel-Buffering": "no"
145
+ });
146
+ ctx.status = 200;
147
+ ctx.res.write(
148
+ (0, import_openai_format.formatSSE)(
149
+ (0, import_openai_format.toOpenAIStreamChunk)({
150
+ id: completionId,
151
+ model: body.model,
152
+ delta: { role: "assistant", content: "" }
153
+ })
154
+ )
155
+ );
156
+ const originalWrite = ctx.res.write.bind(ctx.res);
157
+ const originalEnd = ctx.res.end.bind(ctx.res);
158
+ ctx.res.end = (...args) => {
159
+ if (args[0]) {
160
+ ctx.res.write(args[0]);
161
+ }
162
+ };
163
+ ctx.res.write = (data) => {
164
+ const text = typeof data === "string" ? data : data.toString("utf8");
165
+ for (const line of text.split("\n")) {
166
+ const trimmed = line.trim();
167
+ if (!trimmed.startsWith("data: ")) continue;
168
+ const jsonStr = trimmed.substring(6);
169
+ if (!jsonStr) continue;
170
+ try {
171
+ const event = JSON.parse(jsonStr);
172
+ if (event.type === "content" && event.body) {
173
+ originalWrite(
174
+ (0, import_openai_format.formatSSE)(
175
+ (0, import_openai_format.toOpenAIStreamChunk)({
176
+ id: completionId,
177
+ model: body.model,
178
+ delta: { content: String(event.body) }
179
+ })
180
+ )
181
+ );
182
+ } else if (event.type === "error" && event.body) {
183
+ originalWrite(
184
+ (0, import_openai_format.formatSSE)({
185
+ error: {
186
+ message: String(event.body),
187
+ type: "server_error",
188
+ code: "agent_error"
189
+ }
190
+ })
191
+ );
192
+ }
193
+ } catch {
194
+ }
195
+ }
196
+ return true;
197
+ };
198
+ try {
199
+ const aiEmployee = new AIEmployee(
200
+ ctx,
201
+ employeeRecord,
202
+ sessionId,
203
+ void 0,
204
+ // systemMessage — use AI employee's default
205
+ void 0,
206
+ // skillSettings
207
+ false,
208
+ // webSearch
209
+ { llmService: service.name, model: modelId },
210
+ false
211
+ // legacy — use LangGraph (thread: 1)
212
+ );
213
+ await aiEmployee.stream({ userMessages });
214
+ } finally {
215
+ ctx.res.write = originalWrite;
216
+ ctx.res.end = originalEnd;
217
+ originalWrite(
218
+ (0, import_openai_format.formatSSE)(
219
+ (0, import_openai_format.toOpenAIStreamChunk)({
220
+ id: completionId,
221
+ model: body.model,
222
+ delta: {},
223
+ finishReason: "stop"
224
+ })
225
+ )
226
+ );
227
+ originalWrite((0, import_openai_format.formatSSEDone)());
228
+ originalEnd();
229
+ }
230
+ } else {
231
+ const aiEmployee = new AIEmployee(
232
+ ctx,
233
+ employeeRecord,
234
+ sessionId,
235
+ void 0,
236
+ void 0,
237
+ false,
238
+ { llmService: service.name, model: modelId },
239
+ false
240
+ );
241
+ const result = await aiEmployee.invoke({ userMessages });
242
+ const content = extractLastAiMessageContent(result);
243
+ ctx.status = 200;
244
+ ctx.set("Content-Type", "application/json");
245
+ ctx.body = {
246
+ id: completionId,
247
+ object: "chat.completion",
248
+ created: Math.floor(Date.now() / 1e3),
249
+ model: body.model,
250
+ system_fingerprint: null,
251
+ choices: [
252
+ {
253
+ index: 0,
254
+ message: { role: "assistant", content },
255
+ logprobs: null,
256
+ finish_reason: "stop"
257
+ }
258
+ ],
259
+ // NOTE: Token usage is unavailable in agent mode.
260
+ // ResponseMetadataCollector is private inside AIEmployee and not exposed publicly.
261
+ // Clients can detect agent mode by checking usage.total_tokens === 0.
262
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
263
+ };
264
+ }
265
+ setImmediate(async () => {
266
+ try {
267
+ await ctx.db.getRepository("aiConversations").destroy({
268
+ filterByTk: sessionId
269
+ });
270
+ } catch {
271
+ }
272
+ });
273
+ } catch (err) {
274
+ ctx.log.error("AI API agent completions error:", err);
275
+ if (!ctx.res.headersSent) {
276
+ ctx.status = 500;
277
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
278
+ }
279
+ }
280
+ }
281
+ function extractLastAiMessageContent(result) {
282
+ var _a;
283
+ if (!(result == null ? void 0 : result.messages) || !Array.isArray(result.messages)) return "";
284
+ for (let i = result.messages.length - 1; i >= 0; i--) {
285
+ const msg = result.messages[i];
286
+ if (!msg) continue;
287
+ const className = (_a = msg == null ? void 0 : msg.constructor) == null ? void 0 : _a.name;
288
+ if (className === "HumanMessage" || className === "ToolMessage") continue;
289
+ if (typeof msg.content === "string") return msg.content;
290
+ if (Array.isArray(msg.content)) {
291
+ const textBlock = msg.content.find((c) => c.type === "text");
292
+ return (textBlock == null ? void 0 : textBlock.text) || "";
293
+ }
294
+ }
295
+ return "";
296
+ }
297
+ // Annotate the CommonJS export names for ESM import in node:
298
+ 0 && (module.exports = {
299
+ handleAgentCompletions
300
+ });
@@ -0,0 +1,8 @@
1
+ import { Context } from '@nocobase/actions';
2
+ /**
3
+ * Authentication middleware for /api/ai-llm/v1/* routes.
4
+ *
5
+ * Validates Bearer token against NocoBase's apiKeys collection.
6
+ * Sets ctx.state.currentUser for downstream handlers.
7
+ */
8
+ export declare function authenticateBearer(ctx: Context): Promise<boolean>;
@@ -0,0 +1,110 @@
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 auth_exports = {};
19
+ __export(auth_exports, {
20
+ authenticateBearer: () => authenticateBearer
21
+ });
22
+ module.exports = __toCommonJS(auth_exports);
23
+ var import_openai_format = require("../utils/openai-format");
24
+ async function authenticateBearer(ctx) {
25
+ var _a, _b;
26
+ const authHeader = ctx.get("Authorization") || "";
27
+ if (!authHeader.startsWith("Bearer ")) {
28
+ ctx.status = 401;
29
+ ctx.body = (0, import_openai_format.toOpenAIError)(
30
+ 401,
31
+ "Missing or invalid Authorization header. Expected: Bearer <api-key>",
32
+ "invalid_request_error",
33
+ "invalid_api_key"
34
+ );
35
+ return false;
36
+ }
37
+ const token = authHeader.substring(7).trim();
38
+ if (!token) {
39
+ ctx.status = 401;
40
+ ctx.body = (0, import_openai_format.toOpenAIError)(401, "API key is empty", "invalid_request_error", "invalid_api_key");
41
+ return false;
42
+ }
43
+ try {
44
+ const auth = ctx.app["authManager"];
45
+ if (!auth) {
46
+ ctx.status = 500;
47
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "Auth system not available", "server_error");
48
+ return false;
49
+ }
50
+ const jwt = (_a = ctx.app["authManager"]) == null ? void 0 : _a.jwt;
51
+ if (!jwt) {
52
+ ctx.status = 500;
53
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "JWT system not available", "server_error");
54
+ return false;
55
+ }
56
+ let decoded;
57
+ try {
58
+ decoded = await jwt.decode(token);
59
+ } catch (e) {
60
+ ctx.status = 401;
61
+ ctx.body = (0, import_openai_format.toOpenAIError)(
62
+ 401,
63
+ "Incorrect API key provided",
64
+ "invalid_request_error",
65
+ "invalid_api_key"
66
+ );
67
+ return false;
68
+ }
69
+ if (!decoded || !decoded.userId) {
70
+ ctx.status = 401;
71
+ ctx.body = (0, import_openai_format.toOpenAIError)(
72
+ 401,
73
+ "Invalid or expired API key",
74
+ "invalid_request_error",
75
+ "invalid_api_key"
76
+ );
77
+ return false;
78
+ }
79
+ const user = await ctx.db.getRepository("users").findOne({
80
+ filterByTk: decoded.userId
81
+ });
82
+ if (!user) {
83
+ ctx.status = 401;
84
+ ctx.body = (0, import_openai_format.toOpenAIError)(401, "User not found for this API key", "invalid_request_error", "invalid_api_key");
85
+ return false;
86
+ }
87
+ ctx.state.currentUser = user;
88
+ ctx.state.currentRoles = decoded.roleName ? [decoded.roleName] : ["member"];
89
+ if (!ctx.auth) {
90
+ ctx.auth = {};
91
+ }
92
+ const SAFE_USER_FIELDS = /* @__PURE__ */ new Set(["id", "username", "nickname", "email", "createdAt", "updatedAt"]);
93
+ const rawUser = ((_b = user.toJSON) == null ? void 0 : _b.call(user)) ?? {};
94
+ const safeUser = { id: user.id };
95
+ for (const field of SAFE_USER_FIELDS) {
96
+ if (rawUser[field] !== void 0) safeUser[field] = rawUser[field];
97
+ }
98
+ ctx.auth.user = safeUser;
99
+ return true;
100
+ } catch (err) {
101
+ ctx.log.error("AI API auth error:", err);
102
+ ctx.status = 401;
103
+ ctx.body = (0, import_openai_format.toOpenAIError)(401, "Authentication failed", "invalid_request_error", "invalid_api_key");
104
+ return false;
105
+ }
106
+ }
107
+ // Annotate the CommonJS export names for ESM import in node:
108
+ 0 && (module.exports = {
109
+ authenticateBearer
110
+ });
@@ -0,0 +1,9 @@
1
+ import { Context } from '@nocobase/actions';
2
+ import type PluginAiApiServer from '../plugin';
3
+ /**
4
+ * POST /api/ai-llm/v1/chat/completions
5
+ *
6
+ * Handles OpenAI-compatible chat completion requests.
7
+ * Supports both streaming (SSE) and non-streaming modes.
8
+ */
9
+ export declare function handleChatCompletions(ctx: Context, plugin: PluginAiApiServer): Promise<void>;
@@ -0,0 +1,237 @@
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 chat_completions_exports = {};
19
+ __export(chat_completions_exports, {
20
+ handleChatCompletions: () => handleChatCompletions
21
+ });
22
+ module.exports = __toCommonJS(chat_completions_exports);
23
+ var import_openai_format = require("../utils/openai-format");
24
+ var import_resolve_service = require("../utils/resolve-service");
25
+ async function handleChatCompletions(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.messages) || !Array.isArray(body.messages) || body.messages.length === 0) {
34
+ ctx.status = 400;
35
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'messages' must be a non-empty array", "invalid_request_error", "missing_messages");
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.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
105
+ if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
106
+ if (body.stop !== void 0) modelOptions.stop = body.stop;
107
+ const Provider = providerMeta.provider;
108
+ const provider = new Provider({
109
+ app: ctx.app,
110
+ serviceOptions: service.options,
111
+ modelOptions
112
+ });
113
+ let systemPrompt = "";
114
+ if (config == null ? void 0 : config.defaultAiEmployee) {
115
+ const employee = await ctx.db.getRepository("aiEmployees").findOne({
116
+ filter: { username: config.defaultAiEmployee }
117
+ });
118
+ if (employee) {
119
+ systemPrompt = employee.about || employee.defaultPrompt || "";
120
+ }
121
+ }
122
+ const messages = [...body.messages];
123
+ const hasSystemMessage = messages.some((m) => m.role === "system");
124
+ if (systemPrompt && !hasSystemMessage) {
125
+ messages.unshift({ role: "system", content: systemPrompt });
126
+ }
127
+ const langchainMessages = messages.map((msg) => {
128
+ const role = msg.role === "assistant" ? "ai" : msg.role;
129
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
130
+ return [role, content];
131
+ });
132
+ const completionId = (0, import_openai_format.generateCompletionId)();
133
+ const chatModel = provider.createModel();
134
+ if (stream) {
135
+ await handleStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
136
+ } else {
137
+ await handleNonStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
138
+ }
139
+ } catch (err) {
140
+ ctx.log.error("AI API chat completions error:", err);
141
+ if (!ctx.res.headersSent) {
142
+ ctx.status = 500;
143
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, err.message || "Internal server error", "server_error");
144
+ }
145
+ }
146
+ }
147
+ async function handleNonStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
148
+ const result = await chatModel.invoke(messages);
149
+ let content = "";
150
+ if (typeof result.content === "string") {
151
+ content = result.content;
152
+ } else if (Array.isArray(result.content)) {
153
+ const textPart = result.content.find((c) => c.type === "text");
154
+ content = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
155
+ }
156
+ const usage = result.usage_metadata ? {
157
+ prompt_tokens: result.usage_metadata.input_tokens || 0,
158
+ completion_tokens: result.usage_metadata.output_tokens || 0,
159
+ total_tokens: result.usage_metadata.total_tokens || 0
160
+ } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
161
+ ctx.status = 200;
162
+ ctx.body = (0, import_openai_format.toOpenAIResponse)({
163
+ id: completionId,
164
+ model: modelName,
165
+ content,
166
+ usage
167
+ });
168
+ }
169
+ async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
170
+ ctx.set({
171
+ "Content-Type": "text/event-stream",
172
+ "Cache-Control": "no-cache",
173
+ Connection: "keep-alive",
174
+ "X-Accel-Buffering": "no"
175
+ // Disable nginx buffering
176
+ });
177
+ ctx.status = 200;
178
+ ctx.res.write(
179
+ (0, import_openai_format.formatSSE)(
180
+ (0, import_openai_format.toOpenAIStreamChunk)({
181
+ id: completionId,
182
+ model: modelName,
183
+ delta: { role: "assistant", content: "" }
184
+ })
185
+ )
186
+ );
187
+ try {
188
+ const stream = await chatModel.stream(messages);
189
+ for await (const chunk of stream) {
190
+ let content = "";
191
+ if (typeof chunk.content === "string") {
192
+ content = chunk.content;
193
+ } else if (Array.isArray(chunk.content)) {
194
+ const textPart = chunk.content.find((c) => c.type === "text");
195
+ content = (textPart == null ? void 0 : textPart.text) || "";
196
+ }
197
+ if (content) {
198
+ ctx.res.write(
199
+ (0, import_openai_format.formatSSE)(
200
+ (0, import_openai_format.toOpenAIStreamChunk)({
201
+ id: completionId,
202
+ model: modelName,
203
+ delta: { content }
204
+ })
205
+ )
206
+ );
207
+ }
208
+ }
209
+ ctx.res.write(
210
+ (0, import_openai_format.formatSSE)(
211
+ (0, import_openai_format.toOpenAIStreamChunk)({
212
+ id: completionId,
213
+ model: modelName,
214
+ delta: {},
215
+ finishReason: "stop"
216
+ })
217
+ )
218
+ );
219
+ ctx.res.write((0, import_openai_format.formatSSEDone)());
220
+ } catch (err) {
221
+ ctx.log.error("AI API streaming error:", err);
222
+ ctx.res.write(
223
+ (0, import_openai_format.formatSSE)({
224
+ error: {
225
+ message: err.message || "Streaming error",
226
+ type: "server_error"
227
+ }
228
+ })
229
+ );
230
+ } finally {
231
+ ctx.res.end();
232
+ }
233
+ }
234
+ // Annotate the CommonJS export names for ESM import in node:
235
+ 0 && (module.exports = {
236
+ handleChatCompletions
237
+ });
@@ -0,0 +1,10 @@
1
+ import { Context } from '@nocobase/actions';
2
+ import type PluginAiApiServer from '../plugin';
3
+ /**
4
+ * POST /api/ai-llm/v1/completions
5
+ *
6
+ * Handles legacy OpenAI text completions API.
7
+ * Converts prompt to messages and delegates to the chat model.
8
+ * Required by LiteLLM and other tools that test via legacy completions endpoint.
9
+ */
10
+ export declare function handleCompletions(ctx: Context, plugin: PluginAiApiServer): Promise<void>;