plugin-ai-api 1.0.20 → 1.0.21

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 (72) hide show
  1. package/dist/client/286.01c0e3c5fff3cccb.js +10 -0
  2. package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
  3. package/dist/client/562.17a0a299d2e5152c.js +10 -0
  4. package/dist/client/{757.71e30f2a1306562d.js → 757.a01403fb7a1bea01.js} +1 -1
  5. package/dist/client/{902.4238b04ac667c30a.js → 902.92e1daaf1ab16ebf.js} +1 -1
  6. package/dist/client/{97.37cda285d7da3a26.js → 97.72979a11a067a7c9.js} +1 -1
  7. package/dist/client/index.js +1 -1
  8. package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
  9. package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
  10. package/dist/client-v2/{757.c377e2f2b054d89d.js → 757.a117ce1cf7119cea.js} +1 -1
  11. package/dist/client-v2/{902.d40d7bda106124c8.js → 902.9054d990ddc223ac.js} +1 -1
  12. package/dist/client-v2/952.94100128b7757f56.js +10 -0
  13. package/dist/client-v2/{97.fc922c37ced86831.js → 97.29c663318eebbd57.js} +1 -1
  14. package/dist/client-v2/index.js +1 -1
  15. package/dist/constants.js +36 -0
  16. package/dist/externalVersion.js +9 -10
  17. package/dist/locale/en-US.json +28 -9
  18. package/dist/locale/vi-VN.json +20 -1
  19. package/dist/locale/zh-CN.json +20 -1
  20. package/dist/server/collections/ai-api-config.js +6 -0
  21. package/dist/server/collections/ai-api-model-metadata.js +83 -0
  22. package/dist/server/plugin.js +13 -1
  23. package/dist/server/resource/ai-api-config.js +17 -0
  24. package/dist/server/routes/agent-completions.js +62 -51
  25. package/dist/server/routes/auth.js +11 -1
  26. package/dist/server/routes/chat-completions.js +145 -4
  27. package/dist/server/routes/completions.js +8 -1
  28. package/dist/server/routes/models.js +78 -20
  29. package/dist/server/routes/router.js +94 -22
  30. package/dist/server/usage.js +2 -0
  31. package/dist/server/utils/app-observability.js +110 -0
  32. package/dist/server/utils/streaming.js +15 -1
  33. package/dist/server/validation.js +18 -0
  34. package/dist/swagger.js +32 -1
  35. package/package.json +1 -1
  36. package/src/client/components/AiApiRolePermissions.tsx +11 -169
  37. package/src/client/locale.ts +11 -21
  38. package/src/client/plugin.tsx +17 -8
  39. package/src/client-v2/__tests__/settings-registration.test.tsx +58 -0
  40. package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
  41. package/src/client-v2/locale.ts +21 -1
  42. package/src/client-v2/pages/GeneralPage.tsx +13 -0
  43. package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
  44. package/src/client-v2/pages/RolePermissionsTab.tsx +14 -0
  45. package/src/client-v2/plugin.tsx +41 -1
  46. package/src/constants.ts +21 -0
  47. package/src/locale/en-US.json +28 -9
  48. package/src/locale/vi-VN.json +20 -1
  49. package/src/locale/zh-CN.json +20 -1
  50. package/src/server/__tests__/app-observability.test.ts +98 -0
  51. package/src/server/__tests__/models.test.ts +74 -0
  52. package/src/server/__tests__/request-body.test.ts +310 -0
  53. package/src/server/__tests__/streaming-observability.test.ts +51 -0
  54. package/src/server/collections/ai-api-config.ts +6 -0
  55. package/src/server/collections/ai-api-model-metadata.ts +72 -0
  56. package/src/server/plugin.ts +24 -4
  57. package/src/server/resource/ai-api-config.ts +23 -0
  58. package/src/server/routes/agent-completions.ts +77 -62
  59. package/src/server/routes/auth.ts +14 -1
  60. package/src/server/routes/chat-completions.ts +262 -4
  61. package/src/server/routes/completions.ts +14 -2
  62. package/src/server/routes/models.ts +290 -195
  63. package/src/server/routes/router.ts +136 -26
  64. package/src/server/usage.ts +2 -0
  65. package/src/server/utils/app-observability.ts +105 -0
  66. package/src/server/utils/streaming.ts +13 -1
  67. package/src/server/validation.ts +27 -0
  68. package/src/swagger.ts +38 -1
  69. package/dist/client/302.25edd5d75460acbf.js +0 -10
  70. package/dist/client/778.5c452944cb747975.js +0 -10
  71. package/dist/client-v2/302.9b27a263901d54d8.js +0 -10
  72. package/src/client/AiApiConfigPage.tsx +0 -309
@@ -22,9 +22,13 @@ import { startUsageRecord, finishUsageRecord } from '../usage';
22
22
  import { isStreamingRequested } from '../utils/streaming';
23
23
  import type PluginAiApiServer from '../plugin';
24
24
  import { finalizeLlmBilling } from '../billing';
25
+ import { finishAiApiObservation, startAiApiObservation } from '../utils/app-observability';
25
26
 
26
27
  const API_PREFIX = '/api/ai-llm/v1';
27
28
 
29
+ /** Shared so the plugin can disable the core body parser for exactly these routes. */
30
+ export const AI_LLM_PREFIX = API_PREFIX;
31
+
28
32
  type DataWrappingContext = Context & { withoutDataWrapping?: boolean };
29
33
 
30
34
  /**
@@ -90,22 +94,9 @@ export function createAiLlmRouter(plugin: PluginAiApiServer) {
90
94
  const requestId = `req-${crypto.randomBytes(12).toString('hex')}`;
91
95
  ctx.set('X-Request-Id', requestId);
92
96
 
93
- // ─── Parse body for POST requests if not already parsed ───────────────
94
- if (method === 'POST' && !ctx.request.body) {
95
- try {
96
- const rawBody = await getRawBody(ctx);
97
- ctx.request.body = JSON.parse(rawBody);
98
- } catch (bodyErr: unknown) {
99
- const status =
100
- bodyErr && typeof bodyErr === 'object' && 'statusCode' in bodyErr && bodyErr.statusCode === 413 ? 413 : 400;
101
- const message = status === 413 ? 'Request body too large (max 10 MB)' : 'Invalid JSON in request body';
102
- ctx.status = status;
103
- ctx.body = toOpenAIError(status, message, 'invalid_request_error');
104
- return;
105
- }
106
- }
107
-
108
97
  // ─── Authenticate ─────────────────────────────────────────────────────
98
+ // Runs before the body is read: buffering a multi-megabyte payload for an
99
+ // anonymous caller would let unauthenticated traffic pin memory.
109
100
  const isAuth = await authenticateBearer(ctx);
110
101
  if (!isAuth) {
111
102
  logRequest(ctx, requestId, '-', 'auth_failed', 0);
@@ -126,6 +117,32 @@ export function createAiLlmRouter(plugin: PluginAiApiServer) {
126
117
  return;
127
118
  }
128
119
 
120
+ // ─── Parse body for POST requests if not already parsed ───────────────
121
+ if (method === 'POST' && !ctx.request.body) {
122
+ const maxBodyBytes = await resolveMaxBodyBytes(ctx);
123
+ const declaredLength = Number(ctx.get('Content-Length'));
124
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
125
+ respondBodyTooLarge(ctx, maxBodyBytes);
126
+ logRequest(ctx, requestId, '-', 'body_too_large', 0);
127
+ return;
128
+ }
129
+ try {
130
+ const rawBody = await getRawBody(ctx, maxBodyBytes);
131
+ ctx.request.body = JSON.parse(rawBody);
132
+ } catch (bodyErr: unknown) {
133
+ const tooLarge =
134
+ bodyErr && typeof bodyErr === 'object' && 'statusCode' in bodyErr && bodyErr.statusCode === 413;
135
+ if (tooLarge) {
136
+ respondBodyTooLarge(ctx, maxBodyBytes);
137
+ logRequest(ctx, requestId, '-', 'body_too_large', 0);
138
+ return;
139
+ }
140
+ ctx.status = 400;
141
+ ctx.body = toOpenAIError(400, 'Invalid JSON in request body', 'invalid_request_error');
142
+ return;
143
+ }
144
+ }
145
+
129
146
  // ─── Route matching ───────────────────────────────────────────────────
130
147
  const requestBody = (ctx.request.body || {}) as Record<string, unknown>;
131
148
  const model = requestBody.model === undefined || requestBody.model === null ? '-' : String(requestBody.model);
@@ -145,6 +162,23 @@ export function createAiLlmRouter(plugin: PluginAiApiServer) {
145
162
  };
146
163
  }
147
164
  const t0 = Date.now();
165
+ if (isUsageEndpoint) {
166
+ const service =
167
+ subPath === '/embeddings'
168
+ ? 'llm.embedding'
169
+ : resolvedMode === 'agent'
170
+ ? 'llm.agent'
171
+ : subPath === '/completions'
172
+ ? 'llm.completion'
173
+ : 'llm.chat';
174
+ startAiApiObservation(ctx, {
175
+ service,
176
+ operation: subPath,
177
+ streaming,
178
+ model: model === '-' ? undefined : model,
179
+ mode: resolvedMode,
180
+ });
181
+ }
148
182
  let usageId: unknown;
149
183
  try {
150
184
  usageId = isUsageEndpoint
@@ -275,32 +309,108 @@ export function createAiLlmRouter(plugin: PluginAiApiServer) {
275
309
  ctx.log.error('AI API quota reservation could not be finalized:', billingError);
276
310
  }
277
311
  }
312
+ if (isUsageEndpoint) {
313
+ const streamResult = ctx.state.aiApiStreamResult;
314
+ finishAiApiObservation(ctx, {
315
+ status:
316
+ streamResult?.errorCode === 'client_disconnected'
317
+ ? 'cancelled'
318
+ : streamResult
319
+ ? streamResult.succeeded
320
+ ? 'succeeded'
321
+ : 'failed'
322
+ : ctx.status >= 200 && ctx.status < 400
323
+ ? 'succeeded'
324
+ : ctx.status >= 500
325
+ ? 'failed'
326
+ : 'rejected',
327
+ errorCode: streamResult?.errorCode,
328
+ });
329
+ }
278
330
  }
279
331
  };
280
332
  }
281
333
 
282
- /** Maximum allowed request body size (10 MB) to prevent OOM DoS attacks. */
283
- const MAX_BODY_BYTES = 10 * 1024 * 1024;
334
+ /** Body size cap used when aiApiConfig has no usable maxRequestBodyMb. */
335
+ const DEFAULT_MAX_BODY_MB = 10;
336
+
337
+ /**
338
+ * Upper bound an admin can configure.
339
+ *
340
+ * The gateway buffers the whole payload in memory (raw chunks, the decoded
341
+ * string, and the parsed object all coexist briefly), so an unbounded value
342
+ * would turn a config typo into an out-of-memory risk.
343
+ */
344
+ export const MAX_REQUEST_BODY_MB_LIMIT = 100;
345
+
346
+ /** Clamp a configured megabyte value into the supported range. */
347
+ export function normalizeMaxRequestBodyMb(value: unknown): number {
348
+ const mb = Number(value);
349
+ if (!Number.isSafeInteger(mb) || mb <= 0) return DEFAULT_MAX_BODY_MB;
350
+ return Math.min(mb, MAX_REQUEST_BODY_MB_LIMIT);
351
+ }
352
+
353
+ async function resolveMaxBodyBytes(ctx: Context): Promise<number> {
354
+ let configuredMb: unknown;
355
+ try {
356
+ const config = await ctx.db.getRepository('aiApiConfig').findOne();
357
+ configuredMb = config?.get('maxRequestBodyMb');
358
+ } catch (err) {
359
+ ctx.log?.warn?.('AI API: could not read maxRequestBodyMb, using default:', err);
360
+ }
361
+ return normalizeMaxRequestBodyMb(configuredMb) * 1024 * 1024;
362
+ }
363
+
364
+ function formatMb(bytes: number): string {
365
+ return `${Math.round(bytes / (1024 * 1024))} MB`;
366
+ }
367
+
368
+ function respondBodyTooLarge(ctx: Context, maxBodyBytes: number): void {
369
+ ctx.status = 413;
370
+ ctx.body = toOpenAIError(
371
+ 413,
372
+ `Request body too large (max ${formatMb(maxBodyBytes)}). ` +
373
+ `Inline base64 images inflate payloads by ~33%; raise "Max request body size" in Settings → AI API Gateway if needed.`,
374
+ 'invalid_request_error',
375
+ );
376
+ }
284
377
 
285
378
  /**
286
- * Read raw body from request stream (fallback if bodyparser didn't handle it).
287
- * Rejects with a 413-style error if the body exceeds MAX_BODY_BYTES.
379
+ * Read the raw request body.
380
+ *
381
+ * The plugin disables the core koa-bodyparser for these routes (see plugin.ts),
382
+ * so this cap is the one that actually governs gateway payload size.
383
+ * Rejects with a 413-style error once maxBodyBytes is exceeded.
384
+ *
385
+ * Exported so tests can drive the real implementation over a live socket rather
386
+ * than a copy that cannot catch a regression here.
288
387
  */
289
- function getRawBody(ctx: Context): Promise<string> {
388
+ export function getRawBody(ctx: Pick<Context, 'req'>, maxBodyBytes: number): Promise<string> {
290
389
  return new Promise((resolve, reject) => {
291
- let body = '';
390
+ const chunks: Buffer[] = [];
292
391
  let byteCount = 0;
392
+ let aborted = false;
293
393
 
294
394
  ctx.req.on('data', (chunk: Buffer) => {
395
+ if (aborted) return;
295
396
  byteCount += chunk.length;
296
- if (byteCount > MAX_BODY_BYTES) {
297
- ctx.req.destroy();
298
- reject(Object.assign(new Error('Request body too large (max 10 MB)'), { statusCode: 413 }));
397
+ if (byteCount > maxBodyBytes) {
398
+ // Drop what we buffered and drain the rest instead of destroying the
399
+ // socket: ctx.req and the response share one connection, so destroying
400
+ // it replaces our 413 JSON with an ECONNRESET on the client.
401
+ aborted = true;
402
+ chunks.length = 0;
403
+ ctx.req.resume();
404
+ reject(Object.assign(new Error(`Request body too large (max ${formatMb(maxBodyBytes)})`), { statusCode: 413 }));
299
405
  return;
300
406
  }
301
- body += chunk.toString();
407
+ chunks.push(chunk);
408
+ });
409
+ // Decode once at the end: a multi-byte UTF-8 character can straddle a chunk
410
+ // boundary, and per-chunk toString() would corrupt it.
411
+ ctx.req.on('end', () => {
412
+ if (!aborted) resolve(Buffer.concat(chunks).toString('utf8'));
302
413
  });
303
- ctx.req.on('end', () => resolve(body));
304
414
  ctx.req.on('error', reject);
305
415
  });
306
416
  }
@@ -1,5 +1,6 @@
1
1
  import { Context } from '@nocobase/actions';
2
2
  import { finalizeLlmBilling, type LlmBillingState } from './billing';
3
+ import { addAiApiUsage } from './utils/app-observability';
3
4
 
4
5
  export type Usage = {
5
6
  prompt_tokens: number | null;
@@ -76,6 +77,7 @@ export function setAiApiUsageResult(
76
77
  getAiApiState(ctx).aiApiUsageResult = usage
77
78
  ? { source: 'provider', usage, ...metadata }
78
79
  : { source: 'unavailable', ...metadata };
80
+ addAiApiUsage(ctx, usage);
79
81
  return usage;
80
82
  }
81
83
 
@@ -0,0 +1,105 @@
1
+ import type { Context } from '@nocobase/actions';
2
+
3
+ interface ObservationFinish {
4
+ status: 'succeeded' | 'failed' | 'cancelled' | 'rejected';
5
+ errorCode?: string;
6
+ inputTokens?: number;
7
+ outputTokens?: number;
8
+ }
9
+ interface ObservationHandle {
10
+ markFirstByte(): void;
11
+ addInputTokens(value: number): void;
12
+ addOutputTokens(value: number): void;
13
+ finish(result: ObservationFinish): void;
14
+ }
15
+ interface AppObservabilityContract {
16
+ start(input: {
17
+ service: string;
18
+ operation: string;
19
+ streaming?: boolean;
20
+ attributes?: Record<string, string | number | boolean | null>;
21
+ }): ObservationHandle;
22
+ }
23
+ interface AiApiObservabilityState {
24
+ aiApiObservabilityHandle?: ObservationHandle;
25
+ aiApiObservabilityOutcome?: ObservationFinish;
26
+ }
27
+
28
+ const CONTRACT_SYMBOL = Symbol.for('nocobase.app-observability.contract');
29
+ const NOOP_HANDLE: ObservationHandle = {
30
+ markFirstByte() {},
31
+ addInputTokens() {},
32
+ addOutputTokens() {},
33
+ finish() {},
34
+ };
35
+
36
+ function state(ctx: Context): AiApiObservabilityState {
37
+ return ctx.state as AiApiObservabilityState;
38
+ }
39
+
40
+ function safely(ctx: Context, callback: () => void): void {
41
+ try {
42
+ callback();
43
+ } catch (error) {
44
+ ctx.app?.logger?.warn?.('[ai-api] App observability callback failed', { error });
45
+ }
46
+ }
47
+
48
+ export function startAiApiObservation(
49
+ ctx: Context,
50
+ input: {
51
+ service: 'llm.chat' | 'llm.agent' | 'llm.completion' | 'llm.embedding';
52
+ operation: string;
53
+ streaming: boolean;
54
+ model?: string;
55
+ mode: 'llm' | 'agent';
56
+ },
57
+ ): void {
58
+ let handle = NOOP_HANDLE;
59
+ safely(ctx, () => {
60
+ const contract = (ctx.app as object & { [CONTRACT_SYMBOL]?: AppObservabilityContract })[CONTRACT_SYMBOL];
61
+ if (!contract || typeof contract.start !== 'function') return;
62
+ const candidate = contract.start({
63
+ service: input.service,
64
+ operation: input.operation,
65
+ streaming: input.streaming,
66
+ attributes: {
67
+ mode: input.mode,
68
+ endpoint: input.operation,
69
+ ...(input.model ? { model: input.model } : {}),
70
+ },
71
+ });
72
+ if (candidate && typeof candidate.finish === 'function') handle = candidate;
73
+ });
74
+ state(ctx).aiApiObservabilityHandle = handle;
75
+ }
76
+
77
+ export function markAiApiFirstProviderOutput(ctx: Context): void {
78
+ safely(ctx, () => state(ctx).aiApiObservabilityHandle?.markFirstByte());
79
+ }
80
+
81
+ export function addAiApiUsage(
82
+ ctx: Context,
83
+ usage?: { prompt_tokens: number | null; completion_tokens: number | null },
84
+ ): void {
85
+ if (!usage) return;
86
+ safely(ctx, () => {
87
+ const handle = state(ctx).aiApiObservabilityHandle;
88
+ if (usage.prompt_tokens !== null) handle?.addInputTokens(usage.prompt_tokens);
89
+ if (usage.completion_tokens !== null) handle?.addOutputTokens(usage.completion_tokens);
90
+ });
91
+ }
92
+
93
+ export function setAiApiObservationOutcome(ctx: Context, outcome: ObservationFinish): void {
94
+ state(ctx).aiApiObservabilityOutcome = outcome;
95
+ }
96
+
97
+ export function finishAiApiObservation(ctx: Context, fallback: ObservationFinish): void {
98
+ const current = state(ctx);
99
+ const handle = current.aiApiObservabilityHandle;
100
+ if (!handle) return;
101
+ current.aiApiObservabilityHandle = undefined;
102
+ const outcome = current.aiApiObservabilityOutcome ?? fallback;
103
+ current.aiApiObservabilityOutcome = undefined;
104
+ safely(ctx, () => handle.finish(outcome));
105
+ }
@@ -1,5 +1,17 @@
1
1
  import type { Context } from '@nocobase/actions';
2
2
 
3
+ export class AiApiClientDisconnectedError extends Error {
4
+ readonly code = 'client_disconnected';
5
+ constructor() {
6
+ super('Client disconnected');
7
+ this.name = 'AiApiClientDisconnectedError';
8
+ }
9
+ }
10
+
11
+ export function isClientDisconnected(ctx: Context, error: unknown): boolean {
12
+ return ctx.req.aborted === true || error instanceof AiApiClientDisconnectedError;
13
+ }
14
+
3
15
  export function isStreamingRequested(value: unknown) {
4
16
  return value !== false;
5
17
  }
@@ -7,7 +19,7 @@ export function isStreamingRequested(value: unknown) {
7
19
  export function createRequestAbortController(ctx: Context) {
8
20
  const controller = new AbortController();
9
21
  const abort = () => {
10
- if (!ctx.res.writableEnded) controller.abort(new Error('Client disconnected'));
22
+ if (!ctx.res.writableEnded) controller.abort(new AiApiClientDisconnectedError());
11
23
  };
12
24
  ctx.req.once('aborted', abort);
13
25
  ctx.res.once('close', abort);
@@ -42,6 +42,33 @@ export async function validateModelPrice(db: Database, model: Model): Promise<vo
42
42
  if (overlap) throw new Error('An enabled price already overlaps this effective period.');
43
43
  }
44
44
 
45
+ function requirePositiveIntegerOrNull(value: unknown, field: string): void {
46
+ if (value === null || value === undefined || value === '') return;
47
+ const parsed = Number(value);
48
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${field} must be a positive integer.`);
49
+ }
50
+
51
+ export function validateModelMetadata(model: Model): void {
52
+ if (!String(model.get('llmService') ?? '').trim()) throw new Error('llmService is required.');
53
+ if (!String(model.get('model') ?? '').trim()) throw new Error('model is required.');
54
+ requirePositiveIntegerOrNull(model.get('contextWindow'), 'contextWindow');
55
+ requirePositiveIntegerOrNull(model.get('maxCompletionTokens'), 'maxCompletionTokens');
56
+
57
+ const contextWindow = model.get('contextWindow');
58
+ const maxCompletionTokens = model.get('maxCompletionTokens');
59
+ if (
60
+ contextWindow !== null &&
61
+ contextWindow !== undefined &&
62
+ contextWindow !== '' &&
63
+ maxCompletionTokens !== null &&
64
+ maxCompletionTokens !== undefined &&
65
+ maxCompletionTokens !== '' &&
66
+ Number(maxCompletionTokens) > Number(contextWindow)
67
+ ) {
68
+ throw new Error('maxCompletionTokens cannot exceed contextWindow.');
69
+ }
70
+ }
71
+
45
72
  export function validateQuotaPolicy(model: Model): void {
46
73
  if (!['daily', 'monthly'].includes(String(model.get('periodType')))) {
47
74
  throw new Error('periodType must be daily or monthly.');
package/src/swagger.ts CHANGED
@@ -258,6 +258,15 @@ export default {
258
258
  type: 'integer',
259
259
  description: 'Max requests per minute per user (0 = unlimited)',
260
260
  },
261
+ maxRequestBodyMb: {
262
+ type: 'integer',
263
+ minimum: 1,
264
+ maximum: 100,
265
+ default: 10,
266
+ description:
267
+ 'Max request body size in megabytes. Requests above this return 413. ' +
268
+ 'The gateway buffers each body in memory, so values above 100 are rejected.',
269
+ },
261
270
  },
262
271
  },
263
272
  ModelObject: {
@@ -269,11 +278,39 @@ export default {
269
278
  owned_by: { type: 'string' },
270
279
  },
271
280
  },
281
+ ContentBlock: {
282
+ type: 'object',
283
+ description:
284
+ 'A multimodal content block. Only text and image_url blocks are forwarded to the provider; ' +
285
+ 'any other type is rejected with 400 unsupported_content_block.',
286
+ properties: {
287
+ type: { type: 'string', enum: ['text', 'image_url'] },
288
+ text: { type: 'string' },
289
+ image_url: {
290
+ type: 'object',
291
+ properties: {
292
+ url: {
293
+ type: 'string',
294
+ description: 'An https URL or a base64 data URL, e.g. data:image/png;base64,iVBORw0KGgo...',
295
+ example: 'data:image/png;base64,iVBORw0KGgo...',
296
+ },
297
+ detail: { type: 'string', enum: ['auto', 'low', 'high'] },
298
+ },
299
+ required: ['url'],
300
+ },
301
+ },
302
+ required: ['type'],
303
+ },
272
304
  ChatMessage: {
273
305
  type: 'object',
274
306
  properties: {
275
307
  role: { type: 'string', enum: ['system', 'user', 'assistant', 'tool'] },
276
- content: { type: 'string' },
308
+ content: {
309
+ description:
310
+ 'Plain text, or an array of content blocks for multimodal requests. ' +
311
+ 'Inline base64 images inflate the payload by about 33%; see "Max request body size" in the gateway settings.',
312
+ oneOf: [{ type: 'string' }, { type: 'array', items: { $ref: '#/components/schemas/ContentBlock' } }],
313
+ },
277
314
  name: { type: 'string' },
278
315
  tool_call_id: { type: 'string' },
279
316
  tool_calls: { type: 'array', items: { $ref: '#/components/schemas/ToolCall' } },
@@ -1,10 +0,0 @@
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
- "use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["302"],{581:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(155),a=r.n(n),l=r(59),o=r(694),i=r(235),u=r(630);function c(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 s(e,t,r,n,a,l,o){try{var i=e[l](o),u=i.value}catch(e){r(e);return}i.done?t(u):Promise.resolve(u).then(n,a)}function m(e){return function(){var t=this,r=arguments;return new Promise(function(n,a){var l=e.apply(t,r);function o(e){s(l,n,a,o,i,"next",e)}function i(e){s(l,n,a,o,i,"throw",e)}o(void 0)})}}function p(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,a=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=a){var l=[],o=!0,i=!1;try{for(a=a.call(e);!(o=(r=a.next()).done)&&(l.push(r.value),!t||l.length!==t);o=!0);}catch(e){i=!0,n=e}finally{try{o||null==a.return||a.return()}finally{if(i)throw n}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return c(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 c(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 f(e,t){var r,n,a,l={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),i=Object.defineProperty;return i(o,"next",{value:u(0)}),i(o,"throw",{value:u(1)}),i(o,"return",{value:u(2)}),"function"==typeof Symbol&&i(o,Symbol.iterator,{value:function(){return this}}),o;function u(i){return function(u){var c=[i,u];if(r)throw TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(l=0)),l;)try{if(r=1,n&&(a=2&c[0]?n.return:c[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,c[1])).done)return a;switch(n=0,a&&(c=[2&c[0],a.value]),c[0]){case 0:case 1:a=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,n=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(!(a=(a=l.trys).length>0&&a[a.length-1])&&(6===c[0]||2===c[0])){l=0;continue}if(3===c[0]&&(!a||c[1]>a[0]&&c[1]<a[3])){l.label=c[1];break}if(6===c[0]&&l.label<a[1]){l.label=a[1],a=c;break}if(a&&l.label<a[2]){l.label=a[2],l.ops.push(c);break}a[2]&&l.ops.pop(),l.trys.pop();continue}c=t.call(e,l)}catch(e){c=[6,e],n=0}finally{r=a=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}var y={mode:"llm",enabledLlmServices:[],rateLimitPerMinute:60,quotaEnabled:!1,defaultReservationOutputTokens:4096};function d(){var e=(0,o.useFlowContext)(),t=(0,i.k)(),r=p(l.Form.useForm(),1)[0],c=l.Form.useWatch("mode",r),s=p((0,n.useState)(!0),2),d=s[0],b=s[1],h=p((0,n.useState)(!1),2),v=h[0],g=h[1],E=p((0,n.useState)([]),2),S=E[0],w=E[1],k=p((0,n.useState)([]),2),I=k[0],P=k[1],A=p((0,n.useState)(),2),F=A[0],C=A[1],O=(0,n.useCallback)(function(){return m(function(){var t,n,a,l,o;return f(this,function(i){switch(i.label){case 0:b(!0),C(void 0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiConfig:get",method:"get"}),e.api.request({url:"ai:listLLMServices",method:"get"}),e.api.request({url:"aiEmployees:list",method:"get",params:{paginate:!1}})])];case 2:return n=(t=p.apply(void 0,[i.sent(),3]))[0],a=t[1],l=t[2],r.setFieldsValue(function(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}({},y,(0,u.m)(n,{}))),w((0,u.m)(a,[])),P((0,u.m)(l,[])),[3,5];case 3:return o=i.sent(),C((0,u.g)(o)),[3,5];case 4:return b(!1),[7];case 5:return[2]}})})()},[e.api,r]);(0,n.useEffect)(function(){O()},[O]);var L=S.map(function(e){return{label:e.title||e.name,value:e.name}}),T=I.map(function(e){return{label:e.nickname?"".concat(e.nickname," (").concat(e.username,")"):e.username,value:e.username}}),j="".concat(window.location.origin,"/api/ai-llm/v1");return a().createElement(l.Card,{title:t("Configuration"),loading:d},F?a().createElement(l.Alert,{type:"error",showIcon:!0,message:F,style:{marginBottom:16}}):null,a().createElement(l.Form,{form:r,layout:"vertical",style:{maxWidth:720},initialValues:y},a().createElement(l.Form.Item,{name:"mode",label:t("API mode"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Direct LLM"),value:"llm"},{label:t("AI Employee agent"),value:"agent"}]})),a().createElement(l.Form.Item,{name:"defaultLlmService",label:t("Default LLM service")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",options:L})),a().createElement(l.Form.Item,{name:"enabledLlmServices",label:t("Enabled LLM Services")},a().createElement(l.Select,{mode:"multiple",allowClear:!0,showSearch:!0,optionFilterProp:"label",options:L})),"agent"===c?a().createElement(l.Form.Item,{name:"defaultAiEmployee",label:t("Default AI Employee")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",placeholder:t("Select an AI Employee"),options:T})):null,a().createElement(l.Form.Item,{name:"rateLimitPerMinute",label:t("Rate Limit"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"quotaEnabled",label:t("Enable user quotas"),valuePropName:"checked"},a().createElement(l.Switch,null)),a().createElement(l.Form.Item,{name:"defaultReservationOutputTokens",label:t("Default reserved output tokens"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Space,null,a().createElement(l.Button,{type:"primary",loading:v,onClick:function(){return m(function(){var n,a;return f(this,function(o){switch(o.label){case 0:return[4,r.validateFields()];case 1:n=o.sent(),g(!0),o.label=2;case 2:return o.trys.push([2,4,5,6]),[4,e.api.request({url:"aiApiConfig:save",method:"post",data:n})];case 3:return o.sent(),l.message.success(t("Configuration saved")),[3,6];case 4:return a=o.sent(),l.message.error("".concat(t("Failed to save configuration"),": ").concat((0,u.g)(a))),[3,6];case 5:return g(!1),[7];case 6:return[2]}})})()}},t("Save Configuration")),a().createElement(l.Button,{onClick:O},t("Refresh")))),a().createElement(l.Card,{title:t("Usage guide"),size:"small",style:{marginTop:24}},a().createElement(l.Alert,{type:"info",showIcon:!0,message:t("OpenAI-compatible endpoint"),description:a().createElement(l.Space,{direction:"vertical",size:4},a().createElement(l.Typography.Text,null,t("Base URL")),a().createElement(l.Typography.Text,{code:!0,copyable:!0},j),a().createElement(l.Typography.Text,null,t("Use a NocoBase API key as the Bearer token."))),style:{marginBottom:16}}),a().createElement(l.Typography.Paragraph,null,t("List available models")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(j,'/models -H "Authorization: Bearer <your-api-key>"')),a().createElement(l.Typography.Paragraph,null,t("Send a chat completion")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(j,'/chat/completions \\\n -H "Authorization: Bearer <your-api-key>" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"<service>/<model>","messages":[{"role":"user","content":"Hello"}]}\''))))}},235:function(e,t,r){r.d(t,{k:function(){return l}});var n=r(694),a=JSON.parse('{"UU":"plugin-ai-api"}');function l(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[a.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function a(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return a},m:function(){return n}})}}]);
@@ -1,10 +0,0 @@
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
- "use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["778"],{905:function(e,t,r){r.r(t),r.d(t,{AiApiRolePermissions:function(){return d}});var n=r(155),l=r.n(n),a=r(485),o=r(59);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:"u">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 f(e,t){var r,n,l,a={label:0,sent:function(){if(1&l[0])throw l[1];return l[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),i=Object.defineProperty;return i(o,"next",{value:u(0)}),i(o,"throw",{value:u(1)}),i(o,"return",{value:u(2)}),"function"==typeof Symbol&&i(o,Symbol.iterator,{value:function(){return this}}),o;function u(i){return function(u){var c=[i,u];if(r)throw TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(a=0)),a;)try{if(r=1,n&&(l=2&c[0]?n.return:c[0]?n.throw||((l=n.return)&&l.call(n),0):n.next)&&!(l=l.call(n,c[1])).done)return l;switch(n=0,l&&(c=[2&c[0],l.value]),c[0]){case 0:case 1:l=c;break;case 4:return a.label++,{value:c[1],done:!1};case 5:a.label++,n=c[1],c=[0];continue;case 7:c=a.ops.pop(),a.trys.pop();continue;default:if(!(l=(l=a.trys).length>0&&l[l.length-1])&&(6===c[0]||2===c[0])){a=0;continue}if(3===c[0]&&(!l||c[1]>l[0]&&c[1]<l[3])){a.label=c[1];break}if(6===c[0]&&a.label<l[1]){a.label=l[1],l=c;break}if(l&&a.label<l[2]){a.label=l[2],a.ops.push(c);break}l[2]&&a.ops.pop(),a.trys.pop();continue}c=t.call(e,a)}catch(e){c=[6,e],n=0}finally{r=l=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}var m=o.Typography.Text;function d(e){var t=e.role,r=(0,a.useApp)().apiClient,i=p((0,n.useState)(!0),2),u=i[0],d=i[1],y=p((0,n.useState)(!1),2),b=y[0],h=y[1],v=p((0,n.useState)([]),2),w=v[0],E=v[1],g=p((0,n.useState)(null),2),A=g[0],O=g[1],S=null==t?void 0:t.name;(0,n.useEffect)(function(){S&&P()},[S]);var P=function(){return c(function(){var e,t,n,l,a,o,i;return f(this,function(u){switch(u.label){case 0:d(!0),u.label=1;case 1:return u.trys.push([1,3,4,5]),[4,Promise.all([r.request({url:"aiApiRolePermissions",params:{filter:{roleName:S},paginate:!1}}),r.request({url:"aiEmployees:list",params:{paginate:!1}})])];case 2:return a=(l=p.apply(void 0,[u.sent(),2]))[0],o=l[1],O((i=null==a||null==(t=a.data)||null==(e=t.data)?void 0:e[0])?{id:i.id,roleName:i.roleName,enabled:!!i.enabled,allowAllEmployees:!1!==i.allowAllEmployees,allowedEmployees:i.allowedEmployees||[]}:{roleName:S,enabled:!1,allowAllEmployees:!0,allowedEmployees:[]}),E(((null==o||null==(n=o.data)?void 0:n.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:",u.sent()),[3,5];case 4:return d(!1),[7];case 5:return[2]}})})()},j=function(e){return c(function(){var t,n,l,a;return f(this,function(o){switch(o.label){case 0:if(!A)return[2];O(t=s({},A,e)),h(!0),o.label=1;case 1:if(o.trys.push([1,6,7,8]),!t.id)return[3,3];return[4,r.request({url:"aiApiRolePermissions/".concat(t.id),method:"PUT",data:t})];case 2:return o.sent(),[3,5];case 3:return[4,r.request({url:"aiApiRolePermissions",method:"POST",data:t})];case 4:var i,u;(null==(a=null==(l=o.sent())||null==(n=l.data)?void 0:n.data)?void 0:a.id)&&O((i=s({},t),u=u={id:a.id},Object.getOwnPropertyDescriptors?Object.defineProperties(i,Object.getOwnPropertyDescriptors(u)):(function(e){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t.push.apply(t,r)}return t})(Object(u)).forEach(function(e){Object.defineProperty(i,e,Object.getOwnPropertyDescriptor(u,e))}),i)),o.label=5;case 5:return[3,8];case 6:return console.error("Failed to save AI API role permissions:",o.sent()),O(A),[3,8];case 7:return h(!1),[7];case 8:return[2]}})})()};return u?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==A?void 0:A.enabled),loading:b,onChange:function(e){return j({enabled:e})}}),l().createElement(m,{strong:!0},"Allow this role to use the AI API")),(null==A?void 0:A.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==A?void 0:A.allowAllEmployees),loading:b,onChange:function(e){return j({allowAllEmployees:e})}}),l().createElement(m,null,"Allow all AI Employees")),!(null==A?void 0:A.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==A?void 0:A.allowedEmployees)||[],options:w.map(function(e){return{label:"".concat(e.nickname," (").concat(e.username,")"),value:e.username}}),onChange:function(e){return j({allowedEmployees:e})},disabled:b})))))}}}]);
@@ -1,10 +0,0 @@
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
- "use strict";(self.webpackChunkplugin_ai_api_client_v2=self.webpackChunkplugin_ai_api_client_v2||[]).push([["302"],{581:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(155),a=r.n(n),l=r(59),o=r(694),i=r(235),u=r(630);function c(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 s(e,t,r,n,a,l,o){try{var i=e[l](o),u=i.value}catch(e){r(e);return}i.done?t(u):Promise.resolve(u).then(n,a)}function m(e){return function(){var t=this,r=arguments;return new Promise(function(n,a){var l=e.apply(t,r);function o(e){s(l,n,a,o,i,"next",e)}function i(e){s(l,n,a,o,i,"throw",e)}o(void 0)})}}function p(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,a=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=a){var l=[],o=!0,i=!1;try{for(a=a.call(e);!(o=(r=a.next()).done)&&(l.push(r.value),!t||l.length!==t);o=!0);}catch(e){i=!0,n=e}finally{try{o||null==a.return||a.return()}finally{if(i)throw n}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return c(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 c(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 f(e,t){var r,n,a,l={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),i=Object.defineProperty;return i(o,"next",{value:u(0)}),i(o,"throw",{value:u(1)}),i(o,"return",{value:u(2)}),"function"==typeof Symbol&&i(o,Symbol.iterator,{value:function(){return this}}),o;function u(i){return function(u){var c=[i,u];if(r)throw TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(l=0)),l;)try{if(r=1,n&&(a=2&c[0]?n.return:c[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,c[1])).done)return a;switch(n=0,a&&(c=[2&c[0],a.value]),c[0]){case 0:case 1:a=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,n=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(!(a=(a=l.trys).length>0&&a[a.length-1])&&(6===c[0]||2===c[0])){l=0;continue}if(3===c[0]&&(!a||c[1]>a[0]&&c[1]<a[3])){l.label=c[1];break}if(6===c[0]&&l.label<a[1]){l.label=a[1],a=c;break}if(a&&l.label<a[2]){l.label=a[2],l.ops.push(c);break}a[2]&&l.ops.pop(),l.trys.pop();continue}c=t.call(e,l)}catch(e){c=[6,e],n=0}finally{r=a=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}var y={mode:"llm",enabledLlmServices:[],rateLimitPerMinute:60,quotaEnabled:!1,defaultReservationOutputTokens:4096};function d(){var e=(0,o.useFlowContext)(),t=(0,i.k)(),r=p(l.Form.useForm(),1)[0],c=l.Form.useWatch("mode",r),s=p((0,n.useState)(!0),2),d=s[0],b=s[1],h=p((0,n.useState)(!1),2),v=h[0],g=h[1],E=p((0,n.useState)([]),2),S=E[0],w=E[1],k=p((0,n.useState)([]),2),I=k[0],P=k[1],A=p((0,n.useState)(),2),F=A[0],C=A[1],O=(0,n.useCallback)(function(){return m(function(){var t,n,a,l,o;return f(this,function(i){switch(i.label){case 0:b(!0),C(void 0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiConfig:get",method:"get"}),e.api.request({url:"ai:listLLMServices",method:"get"}),e.api.request({url:"aiEmployees:list",method:"get",params:{paginate:!1}})])];case 2:return n=(t=p.apply(void 0,[i.sent(),3]))[0],a=t[1],l=t[2],r.setFieldsValue(function(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}({},y,(0,u.m)(n,{}))),w((0,u.m)(a,[])),P((0,u.m)(l,[])),[3,5];case 3:return o=i.sent(),C((0,u.g)(o)),[3,5];case 4:return b(!1),[7];case 5:return[2]}})})()},[e.api,r]);(0,n.useEffect)(function(){O()},[O]);var L=S.map(function(e){return{label:e.title||e.name,value:e.name}}),T=I.map(function(e){return{label:e.nickname?"".concat(e.nickname," (").concat(e.username,")"):e.username,value:e.username}}),j="".concat(window.location.origin,"/api/ai-llm/v1");return a().createElement(l.Card,{title:t("Configuration"),loading:d},F?a().createElement(l.Alert,{type:"error",showIcon:!0,message:F,style:{marginBottom:16}}):null,a().createElement(l.Form,{form:r,layout:"vertical",style:{maxWidth:720},initialValues:y},a().createElement(l.Form.Item,{name:"mode",label:t("API mode"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Direct LLM"),value:"llm"},{label:t("AI Employee agent"),value:"agent"}]})),a().createElement(l.Form.Item,{name:"defaultLlmService",label:t("Default LLM service")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",options:L})),a().createElement(l.Form.Item,{name:"enabledLlmServices",label:t("Enabled LLM Services")},a().createElement(l.Select,{mode:"multiple",allowClear:!0,showSearch:!0,optionFilterProp:"label",options:L})),"agent"===c?a().createElement(l.Form.Item,{name:"defaultAiEmployee",label:t("Default AI Employee")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",placeholder:t("Select an AI Employee"),options:T})):null,a().createElement(l.Form.Item,{name:"rateLimitPerMinute",label:t("Rate Limit"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"quotaEnabled",label:t("Enable user quotas"),valuePropName:"checked"},a().createElement(l.Switch,null)),a().createElement(l.Form.Item,{name:"defaultReservationOutputTokens",label:t("Default reserved output tokens"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Space,null,a().createElement(l.Button,{type:"primary",loading:v,onClick:function(){return m(function(){var n,a;return f(this,function(o){switch(o.label){case 0:return[4,r.validateFields()];case 1:n=o.sent(),g(!0),o.label=2;case 2:return o.trys.push([2,4,5,6]),[4,e.api.request({url:"aiApiConfig:save",method:"post",data:n})];case 3:return o.sent(),l.message.success(t("Configuration saved")),[3,6];case 4:return a=o.sent(),l.message.error("".concat(t("Failed to save configuration"),": ").concat((0,u.g)(a))),[3,6];case 5:return g(!1),[7];case 6:return[2]}})})()}},t("Save Configuration")),a().createElement(l.Button,{onClick:O},t("Refresh")))),a().createElement(l.Card,{title:t("Usage guide"),size:"small",style:{marginTop:24}},a().createElement(l.Alert,{type:"info",showIcon:!0,message:t("OpenAI-compatible endpoint"),description:a().createElement(l.Space,{direction:"vertical",size:4},a().createElement(l.Typography.Text,null,t("Base URL")),a().createElement(l.Typography.Text,{code:!0,copyable:!0},j),a().createElement(l.Typography.Text,null,t("Use a NocoBase API key as the Bearer token."))),style:{marginBottom:16}}),a().createElement(l.Typography.Paragraph,null,t("List available models")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(j,'/models -H "Authorization: Bearer <your-api-key>"')),a().createElement(l.Typography.Paragraph,null,t("Send a chat completion")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(j,'/chat/completions \\\n -H "Authorization: Bearer <your-api-key>" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"<service>/<model>","messages":[{"role":"user","content":"Hello"}]}\''))))}},235:function(e,t,r){r.d(t,{k:function(){return l}});var n=r(694),a=JSON.parse('{"UU":"plugin-ai-api"}');function l(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[a.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function a(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return a},m:function(){return n}})}}]);