plugin-ai-api 1.0.2 → 1.0.4

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 (88) hide show
  1. package/README.md +15 -1
  2. package/client-v2.d.ts +2 -0
  3. package/client-v2.js +1 -0
  4. package/dist/client/778.180aac1b13a38702.js +10 -0
  5. package/dist/client/950.2797862732563625.js +10 -0
  6. package/dist/client/index.js +10 -1
  7. package/dist/client-v2/950.8991b849cd7c1351.js +10 -0
  8. package/dist/client-v2/index.js +10 -0
  9. package/dist/externalVersion.js +19 -8
  10. package/dist/index.js +9 -0
  11. package/dist/locale/en-US.json +10 -10
  12. package/dist/locale/zh-CN.json +10 -10
  13. package/dist/server/collections/ai-api-config.js +9 -0
  14. package/dist/server/collections/ai-api-role-permissions.js +9 -0
  15. package/dist/server/index.js +9 -0
  16. package/dist/server/middleware/rate-limit.js +9 -0
  17. package/dist/server/middleware/role-permission.js +9 -0
  18. package/dist/server/plugin.js +14 -0
  19. package/dist/server/resource/ai-api-config.js +9 -0
  20. package/dist/server/routes/agent-completions.js +21 -4
  21. package/dist/server/routes/auth.js +11 -12
  22. package/dist/server/routes/chat-completions.js +16 -4
  23. package/dist/server/routes/completions.js +16 -4
  24. package/dist/server/routes/embeddings.js +10 -3
  25. package/dist/server/routes/models.js +11 -8
  26. package/dist/server/routes/router.js +20 -4
  27. package/dist/server/utils/openai-format.js +9 -0
  28. package/dist/server/utils/rate-limiter.js +9 -0
  29. package/dist/server/utils/resolve-service.js +9 -0
  30. package/dist/swagger.js +10 -4
  31. package/package.json +21 -3
  32. package/src/client/AiApiConfigPage.tsx +309 -0
  33. package/src/client/client.d.ts +258 -0
  34. package/src/client/components/AiApiRolePermissions.tsx +169 -0
  35. package/{dist/client/models/index.d.ts → src/client/index.tsx} +10 -10
  36. package/src/client/locale.ts +21 -0
  37. package/src/client/models/index.ts +12 -0
  38. package/src/client/plugin.tsx +48 -0
  39. package/src/client-v2/plugin.tsx +24 -0
  40. package/{dist/client/locale.d.ts → src/index.ts} +11 -10
  41. package/src/locale/en-US.json +10 -0
  42. package/src/locale/zh-CN.json +10 -0
  43. package/src/server/collections/.gitkeep +0 -0
  44. package/src/server/collections/ai-api-config.ts +51 -0
  45. package/src/server/collections/ai-api-role-permissions.ts +41 -0
  46. package/src/server/index.ts +10 -0
  47. package/src/server/middleware/rate-limit.ts +70 -0
  48. package/src/server/middleware/role-permission.ts +66 -0
  49. package/src/server/plugin.ts +89 -0
  50. package/src/server/resource/ai-api-config.ts +74 -0
  51. package/src/server/routes/agent-completions.ts +428 -0
  52. package/src/server/routes/auth.ts +111 -0
  53. package/src/server/routes/chat-completions.ts +318 -0
  54. package/src/server/routes/completions.ts +299 -0
  55. package/src/server/routes/embeddings.ts +191 -0
  56. package/src/server/routes/models.ts +195 -0
  57. package/src/server/routes/router.ts +283 -0
  58. package/src/server/utils/openai-format.ts +142 -0
  59. package/src/server/utils/rate-limiter.ts +83 -0
  60. package/src/server/utils/resolve-service.ts +82 -0
  61. package/src/swagger.ts +325 -0
  62. package/dist/client/2c126424a58bcb27.js +0 -1
  63. package/dist/client/824c2f7487ee05fd.js +0 -1
  64. package/dist/client/AiApiConfigPage.d.ts +0 -3
  65. package/dist/client/components/AiApiRolePermissions.d.ts +0 -8
  66. package/dist/client/plugin.d.ts +0 -6
  67. package/dist/index.d.ts +0 -2
  68. package/dist/server/collections/ai-api-config.d.ts +0 -2
  69. package/dist/server/collections/ai-api-role-permissions.d.ts +0 -2
  70. package/dist/server/index.d.ts +0 -1
  71. package/dist/server/middleware/rate-limit.d.ts +0 -18
  72. package/dist/server/middleware/role-permission.d.ts +0 -21
  73. package/dist/server/plugin.d.ts +0 -18
  74. package/dist/server/resource/ai-api-config.d.ts +0 -10
  75. package/dist/server/routes/agent-completions.d.ts +0 -34
  76. package/dist/server/routes/auth.d.ts +0 -8
  77. package/dist/server/routes/chat-completions.d.ts +0 -9
  78. package/dist/server/routes/completions.d.ts +0 -10
  79. package/dist/server/routes/embeddings.d.ts +0 -22
  80. package/dist/server/routes/models.d.ts +0 -21
  81. package/dist/server/routes/router.d.ts +0 -25
  82. package/dist/server/utils/openai-format.d.ts +0 -99
  83. package/dist/server/utils/rate-limiter.d.ts +0 -35
  84. package/dist/server/utils/resolve-service.d.ts +0 -19
  85. package/dist/swagger.d.ts +0 -472
  86. package/plugin-ai-api-1.0.1.tgz +0 -0
  87. package/plugin-ai-api-1.0.2.tgz +0 -0
  88. /package/{dist/client/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import crypto from 'crypto';
11
+
12
+ // ─── Model string parsing ───
13
+
14
+ export interface ParsedModel {
15
+ llmService: string;
16
+ modelId: string;
17
+ }
18
+
19
+ /**
20
+ * Parse OpenAI-style model string into NocoBase llmService + modelId.
21
+ * Format: "llmServiceName/modelId" (e.g. "my-openai/gpt-4o")
22
+ * If no "/" is present, the entire string is treated as modelId and llmService is empty.
23
+ */
24
+ export function parseModelString(model: string): ParsedModel {
25
+ const slashIndex = model.indexOf('/');
26
+ if (slashIndex === -1) {
27
+ return { llmService: '', modelId: model };
28
+ }
29
+ return {
30
+ llmService: model.substring(0, slashIndex),
31
+ modelId: model.substring(slashIndex + 1),
32
+ };
33
+ }
34
+
35
+ // ─── ID generation ───
36
+
37
+ export function generateCompletionId(): string {
38
+ return `chatcmpl-${crypto.randomBytes(16).toString('hex').substring(0, 29)}`;
39
+ }
40
+
41
+ // ─── OpenAI error format ───
42
+
43
+ export function toOpenAIError(statusCode: number, message: string, type = 'invalid_request_error', code?: string) {
44
+ return {
45
+ error: {
46
+ message,
47
+ type,
48
+ param: null,
49
+ code: code || null,
50
+ },
51
+ };
52
+ }
53
+
54
+ // ─── OpenAI Chat Completion response (non-streaming) ───
55
+
56
+ export function toOpenAIResponse(options: {
57
+ id: string;
58
+ model: string;
59
+ content: string;
60
+ finishReason?: string;
61
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number };
62
+ }) {
63
+ const { id, model, content, finishReason = 'stop', usage } = options;
64
+ return {
65
+ id,
66
+ object: 'chat.completion',
67
+ created: Math.floor(Date.now() / 1000),
68
+ model,
69
+ system_fingerprint: null,
70
+ choices: [
71
+ {
72
+ index: 0,
73
+ message: {
74
+ role: 'assistant',
75
+ content,
76
+ },
77
+ logprobs: null,
78
+ finish_reason: finishReason,
79
+ },
80
+ ],
81
+ usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
82
+ };
83
+ }
84
+
85
+ // ─── OpenAI Streaming chunk format ───
86
+
87
+ export function toOpenAIStreamChunk(options: {
88
+ id: string;
89
+ model: string;
90
+ delta: { role?: string; content?: string };
91
+ finishReason?: string | null;
92
+ }) {
93
+ const { id, model, delta, finishReason = null } = options;
94
+ return {
95
+ id,
96
+ object: 'chat.completion.chunk',
97
+ created: Math.floor(Date.now() / 1000),
98
+ model,
99
+ system_fingerprint: null,
100
+ choices: [
101
+ {
102
+ index: 0,
103
+ delta,
104
+ logprobs: null,
105
+ finish_reason: finishReason,
106
+ },
107
+ ],
108
+ };
109
+ }
110
+
111
+ // ─── OpenAI Embeddings response ───
112
+
113
+ export function toOpenAIEmbeddingsResponse(options: { model: string; embeddings: number[][]; promptTokens?: number }) {
114
+ const { model, embeddings, promptTokens = 0 } = options;
115
+ return {
116
+ object: 'list' as const,
117
+ data: embeddings.map((embedding, index) => ({
118
+ object: 'embedding' as const,
119
+ embedding,
120
+ index,
121
+ })),
122
+ model,
123
+ usage: {
124
+ prompt_tokens: promptTokens,
125
+ total_tokens: promptTokens,
126
+ },
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Format a streaming chunk as an SSE data line.
132
+ */
133
+ export function formatSSE(data: any): string {
134
+ return `data: ${JSON.stringify(data)}\n\n`;
135
+ }
136
+
137
+ /**
138
+ * Format the terminal SSE [DONE] signal.
139
+ */
140
+ export function formatSSEDone(): string {
141
+ return `data: [DONE]\n\n`;
142
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ /**
11
+ * In-memory sliding window rate limiter.
12
+ *
13
+ * Stores per-user request timestamps. On each check: prunes timestamps
14
+ * older than the window, counts the remainder, and accepts/rejects.
15
+ *
16
+ * Single-process safe (Node.js event loop). Not distributed.
17
+ * For multi-process deployments, replace with a Redis-backed implementation.
18
+ */
19
+ export class RateLimiter {
20
+ /** Map<userId, sorted array of request timestamps in ms> */
21
+ private readonly store = new Map<string | number, number[]>();
22
+
23
+ constructor(private readonly windowMs = 60_000) {}
24
+
25
+ /**
26
+ * Check and record a request for a user.
27
+ *
28
+ * @param userId The user ID (string or numeric)
29
+ * @param limit Max allowed requests per window (from aiApiConfig.rateLimitPerMinute)
30
+ * @returns { allowed: true } or { allowed: false, retryAfterMs: number }
31
+ */
32
+ check(userId: string | number, limit: number): { allowed: true } | { allowed: false; retryAfterMs: number } {
33
+ const now = Date.now();
34
+ const windowStart = now - this.windowMs;
35
+
36
+ let timestamps = this.store.get(userId);
37
+ if (!timestamps) {
38
+ timestamps = [];
39
+ this.store.set(userId, timestamps);
40
+ }
41
+
42
+ // Binary-search prune: drop all timestamps before the window start.
43
+ // O(log n + k) vs O(n) for a simple filter.
44
+ let lo = 0,
45
+ hi = timestamps.length;
46
+ while (lo < hi) {
47
+ const mid = (lo + hi) >>> 1;
48
+ if (timestamps[mid] < windowStart) {
49
+ lo = mid + 1;
50
+ } else {
51
+ hi = mid;
52
+ }
53
+ }
54
+ if (lo > 0) timestamps.splice(0, lo);
55
+
56
+ if (timestamps.length >= limit) {
57
+ // retryAfterMs is the time until the oldest request falls out of the window
58
+ const retryAfterMs = Math.max(0, timestamps[0] + this.windowMs - now);
59
+ return { allowed: false, retryAfterMs };
60
+ }
61
+
62
+ timestamps.push(now);
63
+ return { allowed: true };
64
+ }
65
+
66
+ /**
67
+ * Garbage collect entries for inactive users.
68
+ * Call every ~5 minutes to prevent unbounded memory growth in long-running servers.
69
+ */
70
+ gc(): void {
71
+ const cutoff = Date.now() - this.windowMs;
72
+ for (const [userId, timestamps] of this.store) {
73
+ if (!timestamps.length || timestamps[timestamps.length - 1] < cutoff) {
74
+ this.store.delete(userId);
75
+ }
76
+ }
77
+ }
78
+
79
+ /** Clear all state (useful in tests). */
80
+ clear(): void {
81
+ this.store.clear();
82
+ }
83
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { Context } from '@nocobase/actions';
11
+
12
+ /**
13
+ * Resolve an LLM service by name or title.
14
+ */
15
+ export async function resolveLlmService(ctx: Context, serviceKey: string) {
16
+ const repo = ctx.db.getRepository('llmServices');
17
+
18
+ let service = await repo.findOne({ filter: { name: serviceKey } });
19
+ if (!service) {
20
+ service = await repo.findOne({ filter: { title: serviceKey } });
21
+ }
22
+ return service;
23
+ }
24
+
25
+ /**
26
+ * Resolve a model string to a service + modelId.
27
+ *
28
+ * Strategy (priority order):
29
+ * 1. Try splitting at each "/" position and match the left part against DB (name or title).
30
+ * This handles cases like "Custom LLM (OpenAI Compatible)/qwen/qwen3.6-plus-preview:free"
31
+ * 2. If no service match, use the defaultLlmService from config and treat the ENTIRE
32
+ * model string as the modelId. This allows clients to send just "qwen/qwen3.6-plus-preview:free"
33
+ * or "gpt-4o" without knowing the service name.
34
+ */
35
+ export async function resolveModelString(
36
+ ctx: Context,
37
+ modelString: string,
38
+ ): Promise<{ service: any; modelId: string } | null> {
39
+ const repo = ctx.db.getRepository('llmServices');
40
+
41
+ // ─── Strategy 1: Try splitting at "/" positions ───
42
+ const slashPositions: number[] = [];
43
+ for (let i = 0; i < modelString.length; i++) {
44
+ if (modelString[i] === '/') {
45
+ slashPositions.push(i);
46
+ }
47
+ }
48
+
49
+ if (slashPositions.length > 0) {
50
+ for (const pos of slashPositions) {
51
+ const serviceKey = modelString.substring(0, pos);
52
+ const modelId = modelString.substring(pos + 1);
53
+ if (!serviceKey || !modelId) continue;
54
+
55
+ let service = await repo.findOne({ filter: { name: serviceKey } });
56
+ if (!service) {
57
+ service = await repo.findOne({ filter: { title: serviceKey } });
58
+ }
59
+
60
+ if (service) {
61
+ return { service, modelId };
62
+ }
63
+ }
64
+ }
65
+
66
+ // ─── Strategy 2: Use default LLM service from config ───
67
+ const config = await ctx.db.getRepository('aiApiConfig').findOne();
68
+ if (config?.defaultLlmService) {
69
+ const service = await repo.findOne({ filter: { name: config.defaultLlmService } });
70
+ if (service) {
71
+ return { service, modelId: modelString };
72
+ }
73
+ }
74
+
75
+ // ─── Strategy 3: If only one service enabled, use it ───
76
+ const enabledServices = await repo.find({ filter: { enabled: true } });
77
+ if (enabledServices.length === 1) {
78
+ return { service: enabledServices[0], modelId: modelString };
79
+ }
80
+
81
+ return null;
82
+ }
package/src/swagger.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export default {
11
+ info: {
12
+ title: 'NocoBase API - AI API Plugin',
13
+ description: 'OpenAI-compatible AI gateway and configuration API',
14
+ },
15
+ tags: [
16
+ { name: 'aiApiConfig', description: 'AI API configuration management' },
17
+ { name: 'ai-llm', description: 'OpenAI-compatible LLM gateway (prefix: /api/ai-llm/v1)' },
18
+ ],
19
+ paths: {
20
+ '/aiApiConfig:get': {
21
+ get: {
22
+ tags: ['aiApiConfig'],
23
+ summary: 'Get AI API configuration',
24
+ security: [{ BearerAuth: [] }],
25
+ responses: {
26
+ 200: {
27
+ description: 'Current AI API configuration',
28
+ content: {
29
+ 'application/json': {
30
+ schema: { $ref: '#/components/schemas/AiApiConfig' },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ },
37
+ '/aiApiConfig:save': {
38
+ post: {
39
+ tags: ['aiApiConfig'],
40
+ summary: 'Save AI API configuration',
41
+ security: [{ BearerAuth: [] }],
42
+ requestBody: {
43
+ required: true,
44
+ content: {
45
+ 'application/json': {
46
+ schema: { $ref: '#/components/schemas/AiApiConfig' },
47
+ },
48
+ },
49
+ },
50
+ responses: {
51
+ 200: { description: 'Configuration saved successfully' },
52
+ },
53
+ },
54
+ },
55
+ '/ai-llm/v1/models': {
56
+ get: {
57
+ tags: ['ai-llm'],
58
+ summary: 'List available models',
59
+ description:
60
+ 'Returns all LLM models available across registered services. Model IDs are formatted as `serviceName/modelId`.',
61
+ security: [{ BearerAuth: [] }],
62
+ responses: {
63
+ 200: {
64
+ description: 'List of available models',
65
+ content: {
66
+ 'application/json': {
67
+ schema: {
68
+ type: 'object',
69
+ properties: {
70
+ object: { type: 'string', example: 'list' },
71
+ data: {
72
+ type: 'array',
73
+ items: { $ref: '#/components/schemas/ModelObject' },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ '/ai-llm/v1/models/{id}': {
84
+ get: {
85
+ tags: ['ai-llm'],
86
+ summary: 'Get model details',
87
+ security: [{ BearerAuth: [] }],
88
+ parameters: [
89
+ {
90
+ name: 'id',
91
+ in: 'path',
92
+ required: true,
93
+ description: 'Model ID in format `serviceName/modelId`',
94
+ schema: { type: 'string', example: 'openai/gpt-4o' },
95
+ },
96
+ ],
97
+ responses: {
98
+ 200: {
99
+ description: 'Model details',
100
+ content: {
101
+ 'application/json': {
102
+ schema: { $ref: '#/components/schemas/ModelObject' },
103
+ },
104
+ },
105
+ },
106
+ 404: { description: 'Model not found' },
107
+ },
108
+ },
109
+ },
110
+ '/ai-llm/v1/chat/completions': {
111
+ post: {
112
+ tags: ['ai-llm'],
113
+ summary: 'Chat completions',
114
+ description:
115
+ 'OpenAI-compatible chat completions endpoint. Supports streaming (`stream: true`). Use header `X-AI-Mode: llm` (default) or `X-AI-Mode: agent` to switch between direct LLM and agent mode.',
116
+ security: [{ BearerAuth: [] }],
117
+ parameters: [
118
+ {
119
+ name: 'X-AI-Mode',
120
+ in: 'header',
121
+ required: false,
122
+ description: 'AI mode: `llm` (default) or `agent`',
123
+ schema: { type: 'string', enum: ['llm', 'agent'], default: 'llm' },
124
+ },
125
+ ],
126
+ requestBody: {
127
+ required: true,
128
+ content: {
129
+ 'application/json': {
130
+ schema: { $ref: '#/components/schemas/ChatCompletionRequest' },
131
+ },
132
+ },
133
+ },
134
+ responses: {
135
+ 200: {
136
+ description: 'Chat completion response (or SSE stream when `stream: true`)',
137
+ content: {
138
+ 'application/json': {
139
+ schema: { $ref: '#/components/schemas/ChatCompletionResponse' },
140
+ },
141
+ 'text/event-stream': {
142
+ schema: { type: 'string', description: 'SSE stream of completion chunks' },
143
+ },
144
+ },
145
+ },
146
+ 429: { description: 'Rate limit exceeded' },
147
+ },
148
+ },
149
+ },
150
+ '/ai-llm/v1/completions': {
151
+ post: {
152
+ tags: ['ai-llm'],
153
+ summary: 'Text completions (legacy)',
154
+ description: 'Legacy text completions for LiteLLM compatibility.',
155
+ security: [{ BearerAuth: [] }],
156
+ requestBody: {
157
+ required: true,
158
+ content: {
159
+ 'application/json': {
160
+ schema: {
161
+ type: 'object',
162
+ properties: {
163
+ model: { type: 'string' },
164
+ prompt: { type: 'string' },
165
+ max_tokens: { type: 'integer' },
166
+ stream: { type: 'boolean', default: false },
167
+ },
168
+ required: ['model', 'prompt'],
169
+ },
170
+ },
171
+ },
172
+ },
173
+ responses: {
174
+ 200: { description: 'Completion response' },
175
+ 429: { description: 'Rate limit exceeded' },
176
+ },
177
+ },
178
+ },
179
+ '/ai-llm/v1/embeddings': {
180
+ post: {
181
+ tags: ['ai-llm'],
182
+ summary: 'Generate embeddings',
183
+ description: 'OpenAI-compatible embeddings endpoint.',
184
+ security: [{ BearerAuth: [] }],
185
+ requestBody: {
186
+ required: true,
187
+ content: {
188
+ 'application/json': {
189
+ schema: {
190
+ type: 'object',
191
+ properties: {
192
+ model: { type: 'string', example: 'openai/text-embedding-ada-002' },
193
+ input: {
194
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
195
+ },
196
+ },
197
+ required: ['model', 'input'],
198
+ },
199
+ },
200
+ },
201
+ },
202
+ responses: {
203
+ 200: {
204
+ description: 'Embedding vectors',
205
+ content: {
206
+ 'application/json': {
207
+ schema: {
208
+ type: 'object',
209
+ properties: {
210
+ object: { type: 'string', example: 'list' },
211
+ data: {
212
+ type: 'array',
213
+ items: {
214
+ type: 'object',
215
+ properties: {
216
+ object: { type: 'string', example: 'embedding' },
217
+ embedding: { type: 'array', items: { type: 'number' } },
218
+ index: { type: 'integer' },
219
+ },
220
+ },
221
+ },
222
+ model: { type: 'string' },
223
+ usage: {
224
+ type: 'object',
225
+ properties: {
226
+ prompt_tokens: { type: 'integer' },
227
+ total_tokens: { type: 'integer' },
228
+ },
229
+ },
230
+ },
231
+ },
232
+ },
233
+ },
234
+ },
235
+ 429: { description: 'Rate limit exceeded' },
236
+ },
237
+ },
238
+ },
239
+ },
240
+ components: {
241
+ schemas: {
242
+ AiApiConfig: {
243
+ type: 'object',
244
+ properties: {
245
+ mode: {
246
+ type: 'string',
247
+ enum: ['llm', 'agent'],
248
+ description: 'Default AI mode',
249
+ },
250
+ defaultAiEmployee: { type: 'string', description: 'Default AI employee name' },
251
+ defaultLlmService: { type: 'string', description: 'Default LLM service name' },
252
+ enabledLlmServices: {
253
+ type: 'array',
254
+ items: { type: 'string' },
255
+ description: 'List of enabled LLM service names',
256
+ },
257
+ rateLimitPerMinute: {
258
+ type: 'integer',
259
+ description: 'Max requests per minute per user (0 = unlimited)',
260
+ },
261
+ },
262
+ },
263
+ ModelObject: {
264
+ type: 'object',
265
+ properties: {
266
+ id: { type: 'string', example: 'openai/gpt-4o', description: 'Model ID (serviceName/modelId)' },
267
+ object: { type: 'string', example: 'model' },
268
+ created: { type: 'integer' },
269
+ owned_by: { type: 'string' },
270
+ },
271
+ },
272
+ ChatMessage: {
273
+ type: 'object',
274
+ properties: {
275
+ role: { type: 'string', enum: ['system', 'user', 'assistant', 'tool'] },
276
+ content: { type: 'string' },
277
+ name: { type: 'string' },
278
+ },
279
+ required: ['role', 'content'],
280
+ },
281
+ ChatCompletionRequest: {
282
+ type: 'object',
283
+ properties: {
284
+ model: { type: 'string', example: 'openai/gpt-4o' },
285
+ messages: { type: 'array', items: { $ref: '#/components/schemas/ChatMessage' } },
286
+ stream: { type: 'boolean', default: false },
287
+ temperature: { type: 'number', minimum: 0, maximum: 2 },
288
+ max_tokens: { type: 'integer' },
289
+ top_p: { type: 'number' },
290
+ frequency_penalty: { type: 'number' },
291
+ presence_penalty: { type: 'number' },
292
+ },
293
+ required: ['model', 'messages'],
294
+ },
295
+ ChatCompletionResponse: {
296
+ type: 'object',
297
+ properties: {
298
+ id: { type: 'string' },
299
+ object: { type: 'string', example: 'chat.completion' },
300
+ created: { type: 'integer' },
301
+ model: { type: 'string' },
302
+ choices: {
303
+ type: 'array',
304
+ items: {
305
+ type: 'object',
306
+ properties: {
307
+ index: { type: 'integer' },
308
+ message: { $ref: '#/components/schemas/ChatMessage' },
309
+ finish_reason: { type: 'string', enum: ['stop', 'length', 'tool_calls'] },
310
+ },
311
+ },
312
+ },
313
+ usage: {
314
+ type: 'object',
315
+ properties: {
316
+ prompt_tokens: { type: 'integer' },
317
+ completion_tokens: { type: 'integer' },
318
+ total_tokens: { type: 'integer' },
319
+ },
320
+ },
321
+ },
322
+ },
323
+ },
324
+ },
325
+ };
@@ -1 +0,0 @@
1
- "use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["23"],{928:function(e,t,r){r.r(t),r.d(t,{AiApiRolePermissions:function(){return f}});var n=r(156),l=r.n(n),a=r(772),o=r(721);function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function u(e,t,r,n,l,a,o){try{var i=e[a](o),u=i.value}catch(e){r(e);return}i.done?t(u):Promise.resolve(u).then(n,l)}function c(e){return function(){var t=this,r=arguments;return new Promise(function(n,l){var a=e.apply(t,r);function o(e){u(a,n,l,o,i,"next",e)}function i(e){u(a,n,l,o,i,"throw",e)}o(void 0)})}}function s(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{},n=Object.keys(r);"function"==typeof Object.getOwnPropertySymbols&&(n=n.concat(Object.getOwnPropertySymbols(r).filter(function(e){return Object.getOwnPropertyDescriptor(r,e).enumerable}))),n.forEach(function(t){var n;n=r[t],t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n})}return e}function p(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,l=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=l){var a=[],o=!0,i=!1;try{for(l=l.call(e);!(o=(r=l.next()).done)&&(a.push(r.value),!t||a.length!==t);o=!0);}catch(e){i=!0,n=e}finally{try{o||null==l.return||l.return()}finally{if(i)throw n}}return a}}(e,t)||function(e,t){if(e){if("string"==typeof e)return i(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if("Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r)return Array.from(r);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return i(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){var r,n,l,a,o={label:0,sent:function(){if(1&l[0])throw l[1];return l[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(a){return function(i){var u=[a,i];if(r)throw TypeError("Generator is already executing.");for(;o;)try{if(r=1,n&&(l=2&u[0]?n.return:u[0]?n.throw||((l=n.return)&&l.call(n),0):n.next)&&!(l=l.call(n,u[1])).done)return l;switch(n=0,l&&(u=[2&u[0],l.value]),u[0]){case 0:case 1:l=u;break;case 4:return o.label++,{value:u[1],done:!1};case 5:o.label++,n=u[1],u=[0];continue;case 7:u=o.ops.pop(),o.trys.pop();continue;default:if(!(l=(l=o.trys).length>0&&l[l.length-1])&&(6===u[0]||2===u[0])){o=0;continue}if(3===u[0]&&(!l||u[1]>l[0]&&u[1]<l[3])){o.label=u[1];break}if(6===u[0]&&o.label<l[1]){o.label=l[1],l=u;break}if(l&&o.label<l[2]){o.label=l[2],o.ops.push(u);break}l[2]&&o.ops.pop(),o.trys.pop();continue}u=t.call(e,o)}catch(e){u=[6,e],n=0}finally{r=l=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}}}var m=o.Typography.Text;function f(e){var t,r,i=e.role,u=(0,a.useAPIClient)(),f=p((0,n.useState)(!0),2),y=f[0],b=f[1],h=p((0,n.useState)(!1),2),v=h[0],w=h[1],E=p((0,n.useState)([]),2),g=E[0],A=E[1],O=p((0,n.useState)(null),2),S=O[0],P=O[1],j=null==i?void 0:i.name;(0,n.useEffect)(function(){j&&k()},[j]);var k=(t=c(function(){var e,t,r,n,l,a,o;return d(this,function(i){switch(i.label){case 0:b(!0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([u.request({url:"aiApiRolePermissions",params:{filter:{roleName:j},paginate:!1}}),u.request({url:"aiEmployees:list",params:{paginate:!1}})])];case 2:return l=(n=p.apply(void 0,[i.sent(),2]))[0],a=n[1],P((o=null==l||null==(t=l.data)||null==(e=t.data)?void 0:e[0])?{id:o.id,roleName:o.roleName,enabled:!!o.enabled,allowAllEmployees:!1!==o.allowAllEmployees,allowedEmployees:o.allowedEmployees||[]}:{roleName:j,enabled:!1,allowAllEmployees:!0,allowedEmployees:[]}),A(((null==a||null==(r=a.data)?void 0:r.data)||[]).map(function(e){return{username:e.username,nickname:e.nickname||e.username}})),[3,5];case 3:return console.error("Failed to load AI API role permissions:",i.sent()),[3,5];case 4:return b(!1),[7];case 5:return[2]}})}),function(){return t.apply(this,arguments)}),I=(r=c(function(e){var t,r,n,l;return d(this,function(a){switch(a.label){case 0:if(!S)return[2];P(t=s({},S,e)),w(!0),a.label=1;case 1:if(a.trys.push([1,6,7,8]),!t.id)return[3,3];return[4,u.request({url:"aiApiRolePermissions/".concat(t.id),method:"PUT",data:t})];case 2:return a.sent(),[3,5];case 3:return[4,u.request({url:"aiApiRolePermissions",method:"POST",data:t})];case 4:var o,i;(null==(l=null==(n=a.sent())||null==(r=n.data)?void 0:r.data)?void 0:l.id)&&P((o=s({},t),i=i={id:l.id},Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):(function(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r.push.apply(r,n)}return r})(Object(i)).forEach(function(e){Object.defineProperty(o,e,Object.getOwnPropertyDescriptor(i,e))}),o)),a.label=5;case 5:return[3,8];case 6:return console.error("Failed to save AI API role permissions:",a.sent()),P(S),[3,8];case 7:return w(!1),[7];case 8:return[2]}})}),function(e){return r.apply(this,arguments)});return y?l().createElement(o.Spin,null):l().createElement(o.Card,{bordered:!1},l().createElement(o.Space,{direction:"vertical",style:{width:"100%"},size:"middle"},l().createElement(o.Space,null,l().createElement(o.Switch,{checked:!!(null==S?void 0:S.enabled),loading:v,onChange:function(e){return I({enabled:e})}}),l().createElement(m,{strong:!0},"Allow this role to use the AI API")),(null==S?void 0:S.enabled)&&l().createElement(l().Fragment,null,l().createElement(o.Divider,{style:{margin:"8px 0"}}),l().createElement(o.Space,null,l().createElement(o.Switch,{checked:!!(null==S?void 0:S.allowAllEmployees),loading:v,onChange:function(e){return I({allowAllEmployees:e})}}),l().createElement(m,null,"Allow all AI Employees")),!(null==S?void 0:S.allowAllEmployees)&&l().createElement("div",null,l().createElement(m,{type:"secondary",style:{display:"block",marginBottom:8}},"Select which AI Employees this role may use:"),l().createElement(o.Select,{mode:"multiple",allowClear:!0,style:{width:"100%",maxWidth:480},placeholder:"Select allowed AI Employees",value:(null==S?void 0:S.allowedEmployees)||[],options:g.map(function(e){return{label:"".concat(e.nickname," (").concat(e.username,")"),value:e.username}}),onChange:function(e){return I({allowedEmployees:e})},disabled:v})))))}}}]);
@@ -1 +0,0 @@
1
- "use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["503"],{968:function(e,t,n){n.r(t),n.d(t,{AiApiConfigPage:function(){return y}});var a=n(156),r=n.n(a),l=n(721),o=n(772);function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,a=Array(t);n<t;n++)a[n]=e[n];return a}function c(e,t,n,a,r,l,o){try{var i=e[l](o),c=i.value}catch(e){n(e);return}i.done?t(c):Promise.resolve(c).then(a,r)}function u(e){return function(){var t=this,n=arguments;return new Promise(function(a,r){var l=e.apply(t,n);function o(e){c(l,a,r,o,i,"next",e)}function i(e){c(l,a,r,o,i,"throw",e)}o(void 0)})}}function s(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n,a,r=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=r){var l=[],o=!0,i=!1;try{for(r=r.call(e);!(o=(n=r.next()).done)&&(l.push(n.value),!t||l.length!==t);o=!0);}catch(e){i=!0,a=e}finally{try{o||null==r.return||r.return()}finally{if(i)throw a}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return i(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if("Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n)return Array.from(n);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return i(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function m(e,t){var n,a,r,l,o={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return l={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(l[Symbol.iterator]=function(){return this}),l;function i(l){return function(i){var c=[l,i];if(n)throw TypeError("Generator is already executing.");for(;o;)try{if(n=1,a&&(r=2&c[0]?a.return:c[0]?a.throw||((r=a.return)&&r.call(a),0):a.next)&&!(r=r.call(a,c[1])).done)return r;switch(a=0,r&&(c=[2&c[0],r.value]),c[0]){case 0:case 1:r=c;break;case 4:return o.label++,{value:c[1],done:!1};case 5:o.label++,a=c[1],c=[0];continue;case 7:c=o.ops.pop(),o.trys.pop();continue;default:if(!(r=(r=o.trys).length>0&&r[r.length-1])&&(6===c[0]||2===c[0])){o=0;continue}if(3===c[0]&&(!r||c[1]>r[0]&&c[1]<r[3])){o.label=c[1];break}if(6===c[0]&&o.label<r[1]){o.label=r[1],r=c;break}if(r&&o.label<r[2]){o.label=r[2],o.ops.push(c);break}r[2]&&o.ops.pop(),o.trys.pop();continue}c=t.call(e,o)}catch(e){c=[6,e],a=0}finally{n=r=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}var p=l.Typography.Title,d=l.Typography.Text,f=l.Typography.Paragraph;function y(){var e,t,n=(0,o.useAPIClient)(),i=s(l.Form.useForm(),1)[0],c=s((0,a.useState)(!0),2),y=c[0],v=c[1],h=s((0,a.useState)(!1),2),b=h[0],g=h[1],E=s((0,a.useState)([]),2),A=E[0],S=E[1],w=s((0,a.useState)([]),2),I=w[0],L=w[1],k=s((0,a.useState)(""),2),C=k[0],P=k[1];(0,a.useEffect)(function(){x(),P("".concat(window.location.origin,"/api/ai-llm/v1"))},[]);var x=(e=u(function(){var e,t,a,r,l,o,c;return m(this,function(u){switch(u.label){case 0:v(!0),u.label=1;case 1:return u.trys.push([1,5,6,7]),[4,n.request({url:"aiApiConfig:get"})];case 2:return(l=null==(r=u.sent())||null==(e=r.data)?void 0:e.data)&&i.setFieldsValue({mode:l.mode||"llm",defaultAiEmployee:l.defaultAiEmployee||void 0,defaultLlmService:l.defaultLlmService||void 0,enabledLlmServices:l.enabledLlmServices||[],rateLimitPerMinute:l.rateLimitPerMinute||60}),[4,n.request({url:"aiEmployees:list",params:{paginate:!1}})];case 3:return S(((null==(o=u.sent())||null==(t=o.data)?void 0:t.data)||[]).map(function(e){return{username:e.username,nickname:e.nickname||e.username}})),[4,n.request({url:"ai:listLLMServices"})];case 4:return L(((null==(c=u.sent())||null==(a=c.data)?void 0:a.data)||[]).map(function(e){return{name:e.name,title:e.title||e.name,provider:e.provider,enabled:!1!==e.enabled}})),[3,7];case 5:return console.error("Failed to load config:",u.sent()),[3,7];case 6:return v(!1),[7];case 7:return[2]}})}),function(){return e.apply(this,arguments)}),M=(t=u(function(){var e;return m(this,function(t){switch(t.label){case 0:g(!0),t.label=1;case 1:return t.trys.push([1,3,4,5]),e=i.getFieldsValue(),[4,n.request({url:"aiApiConfig:save",method:"post",data:e})];case 2:return t.sent(),l.message.success("Configuration saved"),[3,5];case 3:return t.sent(),l.message.error("Failed to save configuration"),[3,5];case 4:return g(!1),[7];case 5:return[2]}})}),function(){return t.apply(this,arguments)});return y?r().createElement("div",{style:{display:"flex",justifyContent:"center",padding:60}},r().createElement(l.Spin,{size:"large"})):r().createElement("div",{style:{maxWidth:800,margin:"0 auto",padding:"24px 0"}},r().createElement(p,{level:3},"AI API Gateway Configuration"),r().createElement(f,{type:"secondary"},"Configure the OpenAI-compatible API endpoint. External applications can connect using the base URL and a NocoBase API key."),r().createElement(l.Card,{style:{marginBottom:24}},r().createElement(l.Alert,{type:"info",showIcon:!0,message:"API Endpoint",description:r().createElement(l.Space,{direction:"vertical",size:4},r().createElement(d,null,"Base URL: ",r().createElement(d,{code:!0,copyable:!0},C)),r().createElement(d,{type:"secondary"},"Use this as the base URL in any OpenAI-compatible client (Cursor, Continue.dev, n8n, etc.)")),style:{marginBottom:16}}),r().createElement(l.Alert,{type:"warning",showIcon:!0,message:"Authentication",description:r().createElement(d,null,"Clients must include a NocoBase API key as a Bearer token:",r().createElement("br",null),r().createElement(d,{code:!0},"Authorization: Bearer ","<your-nocobase-api-key>"),r().createElement("br",null),r().createElement(d,{type:"secondary"},"API keys can be created in Settings → API keys."))})),r().createElement(l.Card,{title:"Configuration"},r().createElement(l.Form,{form:i,layout:"vertical",onFinish:M,initialValues:{mode:"llm"}},r().createElement(l.Form.Item,{name:"mode",label:"API Mode",tooltip:"LLM Proxy: Direct access to the LLM model. Agent: Full AI Employee with tools, knowledge base, and agent capabilities."},r().createElement(l.Radio.Group,null,r().createElement(l.Radio.Button,{value:"llm"},"LLM Proxy"),r().createElement(l.Radio.Button,{value:"agent"},"AI Employee Agent"))),r().createElement(l.Form.Item,{name:"defaultAiEmployee",label:"Default AI Employee",tooltip:"The AI Employee whose system prompt will be injected into chat completions (when client doesn't provide a system message)"},r().createElement(l.Select,{allowClear:!0,placeholder:"Select an AI Employee (optional)",options:A.map(function(e){return{label:"".concat(e.nickname," (").concat(e.username,")"),value:e.username}})})),r().createElement(l.Form.Item,{name:"defaultLlmService",label:"Default LLM Service",tooltip:"The LLM service used when clients send only a model name (e.g. 'gpt-4o') without a service prefix. This is the main service that powers the API.",rules:[{required:!0,message:"Please select a default LLM service"}]},r().createElement(l.Select,{allowClear:!0,placeholder:"Select an LLM Service",options:I.map(function(e){return{label:"".concat(e.title," (").concat(e.provider,")"),value:e.name}})})),r().createElement(l.Form.Item,{name:"enabledLlmServices",label:"Enabled LLM Services",tooltip:"Only these services will be exposed via the API. Leave empty to expose all enabled services."},r().createElement(l.Select,{mode:"multiple",allowClear:!0,placeholder:"All enabled services (default)",options:I.map(function(e){return{label:"".concat(e.title," (").concat(e.provider,")"),value:e.name}})})),r().createElement(l.Form.Item,{name:"rateLimitPerMinute",label:"Rate Limit (requests/minute)",tooltip:"Maximum API requests per user per minute. Set 0 for unlimited."},r().createElement(l.InputNumber,{min:0,max:1e4,style:{width:200}})),r().createElement(l.Form.Item,null,r().createElement(l.Button,{type:"primary",htmlType:"submit",loading:b},"Save Configuration")))),r().createElement(l.Card,{title:"Quick Start",style:{marginTop:24}},r().createElement(p,{level:5},"cURL Example"),r().createElement(f,null,r().createElement("pre",{style:{background:"#1a1a2e",color:"#e0e0e0",padding:16,borderRadius:8,fontSize:13,overflow:"auto"}},"curl ".concat(C,'/chat/completions \\\n -H "Authorization: Bearer <your-api-key>" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "model": "<service-name>/<model-id>",\n "messages": [\n {"role": "user", "content": "Hello!"}\n ]\n }\''))),r().createElement(p,{level:5},"Python (OpenAI SDK)"),r().createElement(f,null,r().createElement("pre",{style:{background:"#1a1a2e",color:"#e0e0e0",padding:16,borderRadius:8,fontSize:13,overflow:"auto"}},'from openai import OpenAI\n\nclient = OpenAI(\n base_url="'.concat(C,'",\n api_key="<your-nocobase-api-key>"\n)\n\nresponse = client.chat.completions.create(\n model="<service-name>/<model-id>",\n messages=[{"role": "user", "content": "Hello!"}]\n)'))),r().createElement(p,{level:5},"List Available Models"),r().createElement(f,null,r().createElement("pre",{style:{background:"#1a1a2e",color:"#e0e0e0",padding:16,borderRadius:8,fontSize:13,overflow:"auto"}},"curl ".concat(C,'/models \\\n -H "Authorization: Bearer <your-api-key>"')))))}t.default=y}}]);
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- export declare function AiApiConfigPage(): React.JSX.Element;
3
- export default AiApiConfigPage;
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- /**
3
- * Permission tab shown in Settings → Users & Permissions → [Role] → "AI API" tab.
4
- * Lets admins control whether a role can use the AI API and which AI Employees it may access.
5
- */
6
- export declare function AiApiRolePermissions({ role }: {
7
- role: any;
8
- }): React.JSX.Element;