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.
- package/dist/client/286.01c0e3c5fff3cccb.js +10 -0
- package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
- package/dist/client/562.17a0a299d2e5152c.js +10 -0
- package/dist/client/{757.71e30f2a1306562d.js → 757.a01403fb7a1bea01.js} +1 -1
- package/dist/client/{902.4238b04ac667c30a.js → 902.92e1daaf1ab16ebf.js} +1 -1
- package/dist/client/{97.37cda285d7da3a26.js → 97.72979a11a067a7c9.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
- package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
- package/dist/client-v2/{757.c377e2f2b054d89d.js → 757.a117ce1cf7119cea.js} +1 -1
- package/dist/client-v2/{902.d40d7bda106124c8.js → 902.9054d990ddc223ac.js} +1 -1
- package/dist/client-v2/952.94100128b7757f56.js +10 -0
- package/dist/client-v2/{97.fc922c37ced86831.js → 97.29c663318eebbd57.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +36 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +28 -9
- package/dist/locale/vi-VN.json +20 -1
- package/dist/locale/zh-CN.json +20 -1
- package/dist/server/collections/ai-api-config.js +6 -0
- package/dist/server/collections/ai-api-model-metadata.js +83 -0
- package/dist/server/plugin.js +13 -1
- package/dist/server/resource/ai-api-config.js +17 -0
- package/dist/server/routes/agent-completions.js +62 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +145 -4
- package/dist/server/routes/completions.js +8 -1
- package/dist/server/routes/models.js +78 -20
- package/dist/server/routes/router.js +94 -22
- package/dist/server/usage.js +2 -0
- package/dist/server/utils/app-observability.js +110 -0
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/validation.js +18 -0
- package/dist/swagger.js +32 -1
- package/package.json +1 -1
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +17 -8
- package/src/client-v2/__tests__/settings-registration.test.tsx +58 -0
- package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
- package/src/client-v2/locale.ts +21 -1
- package/src/client-v2/pages/GeneralPage.tsx +13 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
- package/src/client-v2/pages/RolePermissionsTab.tsx +14 -0
- package/src/client-v2/plugin.tsx +41 -1
- package/src/constants.ts +21 -0
- package/src/locale/en-US.json +28 -9
- package/src/locale/vi-VN.json +20 -1
- package/src/locale/zh-CN.json +20 -1
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/models.test.ts +74 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -0
- package/src/server/collections/ai-api-config.ts +6 -0
- package/src/server/collections/ai-api-model-metadata.ts +72 -0
- package/src/server/plugin.ts +24 -4
- package/src/server/resource/ai-api-config.ts +23 -0
- package/src/server/routes/agent-completions.ts +77 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +262 -4
- package/src/server/routes/completions.ts +14 -2
- package/src/server/routes/models.ts +290 -195
- package/src/server/routes/router.ts +136 -26
- package/src/server/usage.ts +2 -0
- package/src/server/utils/app-observability.ts +105 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/validation.ts +27 -0
- package/src/swagger.ts +38 -1
- package/dist/client/302.25edd5d75460acbf.js +0 -10
- package/dist/client/778.5c452944cb747975.js +0 -10
- package/dist/client-v2/302.9b27a263901d54d8.js +0 -10
- 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
|
-
/**
|
|
283
|
-
const
|
|
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
|
|
287
|
-
*
|
|
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
|
-
|
|
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 >
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/server/usage.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/server/validation.ts
CHANGED
|
@@ -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: {
|
|
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}})}}]);
|