plugin-ai-api 1.0.20 → 1.0.23
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/123.e6fe04c856ce6417.js +10 -0
- 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/123.05f1f649923f93eb.js +10 -0
- 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 +39 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +39 -9
- package/dist/locale/vi-VN.json +31 -1
- package/dist/locale/zh-CN.json +31 -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/collections/ai-api-user-permissions.js +67 -0
- package/dist/server/plugin.js +45 -1
- package/dist/server/resource/ai-api-config.js +17 -0
- package/dist/server/resource/ai-api-user-permissions.js +75 -0
- package/dist/server/routes/agent-completions.js +67 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +174 -20
- package/dist/server/routes/completions.js +41 -21
- package/dist/server/routes/embeddings.js +6 -14
- package/dist/server/routes/models.js +102 -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/openai-format.js +17 -3
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/utils/user-permissions.js +160 -0
- package/dist/server/validation.js +18 -0
- package/dist/swagger.js +36 -4
- package/package.json +2 -2
- package/src/client/__tests__/settings-registration.test.tsx +69 -0
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +28 -8
- package/src/client-v2/__tests__/settings-registration.test.tsx +87 -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/pages/UserPermissionsPage.tsx +322 -0
- package/src/client-v2/plugin.tsx +50 -1
- package/src/constants.ts +28 -0
- package/src/locale/en-US.json +39 -9
- package/src/locale/vi-VN.json +31 -1
- package/src/locale/zh-CN.json +31 -1
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/models.test.ts +116 -0
- package/src/server/__tests__/openai-format.test.ts +52 -1
- package/src/server/__tests__/permission-sync.test.ts +109 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -0
- package/src/server/__tests__/usage-route.test.ts +213 -0
- package/src/server/__tests__/user-permissions-resource.test.ts +66 -0
- package/src/server/__tests__/user-permissions.test.ts +284 -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/collections/ai-api-user-permissions.ts +46 -0
- package/src/server/plugin.ts +65 -4
- package/src/server/resource/ai-api-config.ts +23 -0
- package/src/server/resource/ai-api-user-permissions.ts +76 -0
- package/src/server/routes/agent-completions.ts +84 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +294 -20
- package/src/server/routes/completions.ts +54 -20
- package/src/server/routes/embeddings.ts +10 -15
- package/src/server/routes/models.ts +318 -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/openai-format.ts +26 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/utils/user-permissions.ts +218 -0
- package/src/server/validation.ts +27 -0
- package/src/swagger.ts +47 -4
- 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
|
@@ -8,12 +8,25 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Context } from '@nocobase/actions';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
generateCompletionId,
|
|
13
|
+
toOpenAIError,
|
|
14
|
+
formatSSE,
|
|
15
|
+
formatSSEDone,
|
|
16
|
+
toOpenAIUsageChunk,
|
|
17
|
+
} from '../utils/openai-format';
|
|
12
18
|
import { resolveModelString } from '../utils/resolve-service';
|
|
13
|
-
import {
|
|
19
|
+
import { enforceModelAccess } from '../utils/user-permissions';
|
|
20
|
+
import {
|
|
21
|
+
createRequestAbortController,
|
|
22
|
+
isClientDisconnected,
|
|
23
|
+
isStreamingRequested,
|
|
24
|
+
writeResponse,
|
|
25
|
+
} from '../utils/streaming';
|
|
14
26
|
import { extractProviderRequestId, normalizeUsage, setAiApiUsageResult, type Usage } from '../usage';
|
|
15
27
|
import type PluginAiApiServer from '../plugin';
|
|
16
28
|
import { AiApiQuotaError, markLlmProviderAttempted, prepareLlmBilling } from '../billing';
|
|
29
|
+
import { markAiApiFirstProviderOutput } from '../utils/app-observability';
|
|
17
30
|
|
|
18
31
|
/**
|
|
19
32
|
* POST /api/ai-llm/v1/completions
|
|
@@ -87,22 +100,10 @@ export async function handleCompletions(ctx: Context, plugin: PluginAiApiServer)
|
|
|
87
100
|
return;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
|
-
// ─── Check whitelist ───
|
|
103
|
+
// ─── Check whitelist (global config ∩ per-user grant) ───
|
|
91
104
|
const config = await ctx.db.getRepository('aiApiConfig').findOne();
|
|
92
|
-
if (config?.enabledLlmServices
|
|
93
|
-
|
|
94
|
-
const serviceTitle = service.title;
|
|
95
|
-
const isAllowed = config.enabledLlmServices.some((s: string) => s === serviceName || s === serviceTitle);
|
|
96
|
-
if (!isAllowed) {
|
|
97
|
-
ctx.status = 403;
|
|
98
|
-
ctx.body = toOpenAIError(
|
|
99
|
-
403,
|
|
100
|
-
`LLM service '${service.title || service.name}' is not enabled for API access`,
|
|
101
|
-
'invalid_request_error',
|
|
102
|
-
'model_not_available',
|
|
103
|
-
);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
105
|
+
if (!(await enforceModelAccess(ctx, config?.enabledLlmServices, service, modelId))) {
|
|
106
|
+
return;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// ─── Create LLM provider instance ───
|
|
@@ -160,7 +161,14 @@ export async function handleCompletions(ctx: Context, plugin: PluginAiApiServer)
|
|
|
160
161
|
markLlmProviderAttempted(ctx);
|
|
161
162
|
|
|
162
163
|
if (stream) {
|
|
163
|
-
await handleStreamingTextCompletion(
|
|
164
|
+
await handleStreamingTextCompletion(
|
|
165
|
+
ctx,
|
|
166
|
+
chatModel,
|
|
167
|
+
langchainMessages,
|
|
168
|
+
completionId,
|
|
169
|
+
body.model,
|
|
170
|
+
body.stream_options,
|
|
171
|
+
);
|
|
164
172
|
} else {
|
|
165
173
|
await handleNonStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
|
|
166
174
|
}
|
|
@@ -231,6 +239,7 @@ async function handleStreamingTextCompletion(
|
|
|
231
239
|
messages: [string, string][],
|
|
232
240
|
completionId: string,
|
|
233
241
|
modelName: string,
|
|
242
|
+
streamOptions: Record<string, unknown> | undefined,
|
|
234
243
|
) {
|
|
235
244
|
ctx.set({
|
|
236
245
|
'Content-Type': 'text/event-stream',
|
|
@@ -244,7 +253,10 @@ async function handleStreamingTextCompletion(
|
|
|
244
253
|
let usage: Usage | undefined;
|
|
245
254
|
let providerRequestId: string | undefined;
|
|
246
255
|
try {
|
|
247
|
-
const stream = await chatModel.stream(messages, {
|
|
256
|
+
const stream = await chatModel.stream(messages, {
|
|
257
|
+
stream_options: { ...streamOptions, include_usage: true },
|
|
258
|
+
signal: requestAbort.signal,
|
|
259
|
+
});
|
|
248
260
|
|
|
249
261
|
for await (const chunk of stream) {
|
|
250
262
|
if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
|
|
@@ -257,6 +269,7 @@ async function handleStreamingTextCompletion(
|
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
if (text) {
|
|
272
|
+
markAiApiFirstProviderOutput(ctx);
|
|
260
273
|
await writeResponse(
|
|
261
274
|
ctx,
|
|
262
275
|
formatSSE({
|
|
@@ -273,6 +286,7 @@ async function handleStreamingTextCompletion(
|
|
|
273
286
|
finish_reason: null,
|
|
274
287
|
},
|
|
275
288
|
],
|
|
289
|
+
usage: null,
|
|
276
290
|
}),
|
|
277
291
|
);
|
|
278
292
|
}
|
|
@@ -299,13 +313,29 @@ async function handleStreamingTextCompletion(
|
|
|
299
313
|
finish_reason: 'stop',
|
|
300
314
|
},
|
|
301
315
|
],
|
|
316
|
+
usage: null,
|
|
302
317
|
}),
|
|
303
318
|
);
|
|
304
319
|
|
|
320
|
+
if (usage) {
|
|
321
|
+
await writeResponse(
|
|
322
|
+
ctx,
|
|
323
|
+
formatSSE(
|
|
324
|
+
toOpenAIUsageChunk({
|
|
325
|
+
id: completionId,
|
|
326
|
+
model: modelName,
|
|
327
|
+
object: 'text_completion',
|
|
328
|
+
usage,
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
305
334
|
await writeResponse(ctx, formatSSEDone());
|
|
306
335
|
setAiApiUsageResult(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
307
336
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
308
337
|
} catch (err) {
|
|
338
|
+
const cancelled = isClientDisconnected(ctx, err);
|
|
309
339
|
ctx.log.error('AI API completions streaming error:', err);
|
|
310
340
|
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
311
341
|
await writeResponse(
|
|
@@ -319,7 +349,11 @@ async function handleStreamingTextCompletion(
|
|
|
319
349
|
);
|
|
320
350
|
}
|
|
321
351
|
setAiApiUsageResult(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
322
|
-
ctx.state.aiApiStreamResult = {
|
|
352
|
+
ctx.state.aiApiStreamResult = {
|
|
353
|
+
succeeded: false,
|
|
354
|
+
id: completionId,
|
|
355
|
+
errorCode: cancelled ? 'client_disconnected' : 'stream_error',
|
|
356
|
+
};
|
|
323
357
|
} finally {
|
|
324
358
|
requestAbort.dispose();
|
|
325
359
|
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { Context } from '@nocobase/actions';
|
|
11
11
|
import { toOpenAIError, toOpenAIEmbeddingsResponse } from '../utils/openai-format';
|
|
12
12
|
import { resolveModelString } from '../utils/resolve-service';
|
|
13
|
+
import { enforceModelAccess } from '../utils/user-permissions';
|
|
13
14
|
import { setAiApiUsageUnavailable } from '../usage';
|
|
14
15
|
import type PluginAiApiServer from '../plugin';
|
|
15
16
|
|
|
@@ -112,24 +113,18 @@ export async function handleEmbeddings(ctx: Context, plugin: PluginAiApiServer)
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
// ─── Check service whitelist
|
|
116
|
+
// ─── Check service whitelist (global config ∩ per-user grant) ─────────────
|
|
117
|
+
// A config read failure falls open on the global list, but the per-user grant is
|
|
118
|
+
// still enforced: an explicit deny must never be bypassed by an unreadable config.
|
|
119
|
+
let globalEnabledServices: unknown = [];
|
|
116
120
|
try {
|
|
117
121
|
const config = await ctx.db.getRepository('aiApiConfig').findOne();
|
|
118
|
-
|
|
119
|
-
const allowed = config.enabledLlmServices.some((s: string) => s === service.name || s === service.title);
|
|
120
|
-
if (!allowed) {
|
|
121
|
-
ctx.status = 403;
|
|
122
|
-
ctx.body = toOpenAIError(
|
|
123
|
-
403,
|
|
124
|
-
`LLM service '${service.title || service.name}' is not enabled for API access`,
|
|
125
|
-
'invalid_request_error',
|
|
126
|
-
'model_not_available',
|
|
127
|
-
);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
122
|
+
globalEnabledServices = config?.enabledLlmServices ?? [];
|
|
131
123
|
} catch {
|
|
132
|
-
// Config read failure: fail open
|
|
124
|
+
// Config read failure: fail open on the global whitelist only.
|
|
125
|
+
}
|
|
126
|
+
if (!(await enforceModelAccess(ctx, globalEnabledServices, service, modelId))) {
|
|
127
|
+
return;
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
// ─── Get embedding provider ───────────────────────────────────────────────
|