plugin-ai-api 1.0.24 → 1.0.28
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 → 286.a1ee0420172cd5de.js} +1 -1
- package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
- package/dist/client/562.44b16aad4718b4c7.js +10 -0
- package/dist/client/685.ae483e17b6b49c98.js +10 -0
- package/dist/client/757.6568d3504ad29352.js +10 -0
- package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.3971233415999b2c.js +10 -0
- package/dist/client-v2/562.45d5c504433be38b.js +10 -0
- package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
- package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
- package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
- package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +2 -5
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +27 -8
- package/dist/locale/vi-VN.json +27 -8
- package/dist/locale/zh-CN.json +27 -8
- package/dist/server/billing.js +31 -33
- package/dist/server/collections/ai-api-config.js +7 -7
- package/dist/server/collections/ai-api-group-members.js +62 -0
- package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
- package/dist/server/collections/ai-api-model-metadata.js +6 -0
- package/dist/server/collections/ai-api-usage-groups.js +74 -0
- package/dist/server/collections/ai-api-usage-records.js +2 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
- package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
- package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
- package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
- package/dist/server/plugin.js +100 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +5 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/resource/ai-api-usage-monitor.js +3 -1
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +121 -42
- package/dist/server/routes/completions.js +48 -29
- package/dist/server/routes/embeddings.js +2 -1
- package/dist/server/routes/models.js +2 -1
- package/dist/server/routes/router.js +3 -2
- package/dist/server/services/file-processor.js +426 -0
- package/dist/server/usage.js +37 -3
- package/dist/server/utils/direct-llm-context.js +163 -26
- package/dist/server/utils/openai-format.js +21 -2
- package/dist/server/utils/rate-limiter.js +1 -1
- package/dist/server/utils/request-cache.js +61 -0
- package/dist/server/utils/resolve-service.js +2 -1
- package/dist/server/utils/user-permissions.js +25 -39
- package/dist/server/validation.js +7 -0
- package/dist/swagger.js +48 -10
- package/package.json +1 -1
- package/src/client/__tests__/settings-registration.test.tsx +6 -29
- package/src/client/plugin.tsx +5 -16
- package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
- package/src/client-v2/locale.ts +3 -1
- package/src/client-v2/pages/GeneralPage.tsx +0 -5
- package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
- package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
- package/src/client-v2/pages/UsagePage.tsx +9 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +27 -8
- package/src/locale/vi-VN.json +27 -8
- package/src/locale/zh-CN.json +27 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +209 -10
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/openai-format.test.ts +12 -2
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/request-body.test.ts +45 -2
- package/src/server/__tests__/usage-groups.test.ts +160 -0
- package/src/server/__tests__/usage-monitor.test.ts +2 -0
- package/src/server/__tests__/usage-route.test.ts +382 -5
- package/src/server/__tests__/usage.test.ts +57 -0
- package/src/server/__tests__/user-permissions.test.ts +214 -133
- package/src/server/__tests__/validation.test.ts +11 -0
- package/src/server/billing.ts +36 -39
- package/src/server/collections/ai-api-config.ts +9 -7
- package/src/server/collections/ai-api-group-members.ts +41 -0
- package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
- package/src/server/collections/ai-api-model-metadata.ts +7 -0
- package/src/server/collections/ai-api-role-permissions.ts +41 -41
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +2 -0
- package/src/server/index.ts +10 -10
- package/src/server/middleware/rate-limit.ts +68 -70
- package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
- package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
- package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
- package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
- package/src/server/plugin.ts +121 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +5 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/resource/ai-api-usage-monitor.ts +3 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +173 -47
- package/src/server/routes/completions.ts +50 -27
- package/src/server/routes/embeddings.ts +2 -1
- package/src/server/routes/models.ts +4 -3
- package/src/server/routes/router.ts +4 -3
- package/src/server/services/__tests__/file-processor.test.ts +184 -0
- package/src/server/services/file-processor.ts +513 -0
- package/src/server/usage.ts +51 -1
- package/src/server/utils/direct-llm-context.ts +218 -31
- package/src/server/utils/openai-format.ts +25 -2
- package/src/server/utils/rate-limiter.ts +83 -83
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +83 -82
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +52 -11
- package/dist/client/123.e6fe04c856ce6417.js +0 -10
- package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
- package/dist/client/562.17a0a299d2e5152c.js +0 -10
- package/dist/client/757.a01403fb7a1bea01.js +0 -10
- package/dist/client/902.e74518750f1e4201.js +0 -10
- package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
- package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
- package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
- package/dist/client-v2/902.c7c00a565085438a.js +0 -10
- package/dist/server/resource/ai-api-user-permissions.js +0 -75
- package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
- package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
- package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
- package/src/server/resource/ai-api-user-permissions.ts +0 -76
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context } from '@nocobase/actions';
|
|
11
|
+
import type { Model } from '@nocobase/database';
|
|
12
|
+
|
|
13
|
+
export type QuotaMode = 'share' | 'per_user';
|
|
14
|
+
|
|
15
|
+
export interface AiApiUsageGroup {
|
|
16
|
+
id: string | number | bigint;
|
|
17
|
+
name: string;
|
|
18
|
+
isDefault: boolean;
|
|
19
|
+
quotaMode: QuotaMode;
|
|
20
|
+
rateLimitPerMinute: number;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
periodType: string;
|
|
23
|
+
timezone: string;
|
|
24
|
+
requestLimit: string | number | bigint | null;
|
|
25
|
+
totalTokenLimit: string | number | bigint | null;
|
|
26
|
+
costLimit: string | null;
|
|
27
|
+
currency: string;
|
|
28
|
+
rejectUnpricedModel: boolean;
|
|
29
|
+
missingUsageBehavior: 'allow' | 'use_reserved';
|
|
30
|
+
contextOverflowBehavior: 'reject' | 'truncate';
|
|
31
|
+
allowedLlmServices: string[];
|
|
32
|
+
allowAllModels: boolean;
|
|
33
|
+
allowedModels: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getDefaultGroup(ctx: Context): Promise<AiApiUsageGroup> {
|
|
37
|
+
const existing = await ctx.db.getRepository('aiApiUsageGroups').findOne({
|
|
38
|
+
filter: { isDefault: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (existing) {
|
|
42
|
+
return modelToGroup(existing);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const created = await ctx.db.getRepository('aiApiUsageGroups').create({
|
|
46
|
+
values: {
|
|
47
|
+
name: 'Default',
|
|
48
|
+
isDefault: true,
|
|
49
|
+
quotaMode: 'per_user',
|
|
50
|
+
rateLimitPerMinute: 60,
|
|
51
|
+
enabled: false,
|
|
52
|
+
periodType: 'monthly',
|
|
53
|
+
timezone: 'UTC',
|
|
54
|
+
requestLimit: null,
|
|
55
|
+
totalTokenLimit: null,
|
|
56
|
+
costLimit: null,
|
|
57
|
+
currency: 'USD',
|
|
58
|
+
rejectUnpricedModel: true,
|
|
59
|
+
missingUsageBehavior: 'use_reserved',
|
|
60
|
+
contextOverflowBehavior: 'reject',
|
|
61
|
+
allowedLlmServices: [],
|
|
62
|
+
allowAllModels: true,
|
|
63
|
+
allowedModels: [],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return modelToGroup(created);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function resolveUserGroup(
|
|
71
|
+
ctx: Context,
|
|
72
|
+
userId: string | number | bigint | undefined | null,
|
|
73
|
+
): Promise<AiApiUsageGroup> {
|
|
74
|
+
if (userId === undefined || userId === null) {
|
|
75
|
+
return getDefaultGroup(ctx);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const member = await ctx.db.getRepository('aiApiGroupMembers').findOne({
|
|
79
|
+
filter: { userId },
|
|
80
|
+
appends: ['group'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (member && member.get('group')) {
|
|
84
|
+
return modelToGroup(member.get('group'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getDefaultGroup(ctx);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function modelToGroup(model: Model | Record<string, unknown>): AiApiUsageGroup {
|
|
91
|
+
const record =
|
|
92
|
+
typeof (model as Model).get === 'function' ? (model as Model).get() : (model as Record<string, unknown>);
|
|
93
|
+
return {
|
|
94
|
+
id: record.id as string | number | bigint,
|
|
95
|
+
name: (record.name as string) ?? '',
|
|
96
|
+
isDefault: !!record.isDefault,
|
|
97
|
+
quotaMode: (record.quotaMode as QuotaMode) ?? 'per_user',
|
|
98
|
+
rateLimitPerMinute: Number(record.rateLimitPerMinute ?? 60),
|
|
99
|
+
enabled: !!record.enabled,
|
|
100
|
+
periodType: (record.periodType as string) ?? 'monthly',
|
|
101
|
+
timezone: (record.timezone as string) ?? 'UTC',
|
|
102
|
+
requestLimit: (record.requestLimit as string | number | bigint | null) ?? null,
|
|
103
|
+
totalTokenLimit: (record.totalTokenLimit as string | number | bigint | null) ?? null,
|
|
104
|
+
costLimit: (record.costLimit as string | null) ?? null,
|
|
105
|
+
currency: (record.currency as string) ?? 'USD',
|
|
106
|
+
rejectUnpricedModel: !!record.rejectUnpricedModel,
|
|
107
|
+
missingUsageBehavior: (record.missingUsageBehavior as 'allow' | 'use_reserved') ?? 'use_reserved',
|
|
108
|
+
contextOverflowBehavior: (record.contextOverflowBehavior as 'reject' | 'truncate') ?? 'reject',
|
|
109
|
+
allowedLlmServices: stringArray(record.allowedLlmServices),
|
|
110
|
+
allowAllModels: record.allowAllModels !== false,
|
|
111
|
+
allowedModels: stringArray(record.allowedModels),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stringArray(value: unknown): string[] {
|
|
116
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
117
|
+
}
|
|
@@ -46,8 +46,8 @@ const aiApiConfigResource: ResourceOptions = {
|
|
|
46
46
|
defaultAiEmployee: '',
|
|
47
47
|
defaultLlmService: '',
|
|
48
48
|
enabledLlmServices: [],
|
|
49
|
-
rateLimitPerMinute: 60,
|
|
50
49
|
maxRequestBodyMb: 10,
|
|
50
|
+
pdfRenderPagesAsImages: false,
|
|
51
51
|
quotaEnabled: false,
|
|
52
52
|
defaultReservationOutputTokens: 4096,
|
|
53
53
|
options: {},
|
|
@@ -70,8 +70,8 @@ const aiApiConfigResource: ResourceOptions = {
|
|
|
70
70
|
defaultAiEmployee: values.defaultAiEmployee ?? '',
|
|
71
71
|
defaultLlmService: values.defaultLlmService ?? '',
|
|
72
72
|
enabledLlmServices: values.enabledLlmServices ?? [],
|
|
73
|
-
rateLimitPerMinute: values.rateLimitPerMinute ?? 60,
|
|
74
73
|
maxRequestBodyMb: coerceMaxRequestBodyMb(values.maxRequestBodyMb ?? DEFAULT_MAX_REQUEST_BODY_MB),
|
|
74
|
+
pdfRenderPagesAsImages: values.pdfRenderPagesAsImages ?? false,
|
|
75
75
|
quotaEnabled: values.quotaEnabled ?? false,
|
|
76
76
|
defaultReservationOutputTokens: values.defaultReservationOutputTokens ?? 4096,
|
|
77
77
|
options: values.options ?? {},
|
|
@@ -83,10 +83,12 @@ const aiApiConfigResource: ResourceOptions = {
|
|
|
83
83
|
if (values.defaultAiEmployee !== undefined) updateData.defaultAiEmployee = values.defaultAiEmployee;
|
|
84
84
|
if (values.defaultLlmService !== undefined) updateData.defaultLlmService = values.defaultLlmService;
|
|
85
85
|
if (values.enabledLlmServices !== undefined) updateData.enabledLlmServices = values.enabledLlmServices;
|
|
86
|
-
if (values.rateLimitPerMinute !== undefined) updateData.rateLimitPerMinute = values.rateLimitPerMinute;
|
|
87
86
|
if (values.maxRequestBodyMb !== undefined) {
|
|
88
87
|
updateData.maxRequestBodyMb = coerceMaxRequestBodyMb(values.maxRequestBodyMb);
|
|
89
88
|
}
|
|
89
|
+
if (values.pdfRenderPagesAsImages !== undefined) {
|
|
90
|
+
updateData.pdfRenderPagesAsImages = Boolean(values.pdfRenderPagesAsImages);
|
|
91
|
+
}
|
|
90
92
|
if (values.quotaEnabled !== undefined) updateData.quotaEnabled = values.quotaEnabled;
|
|
91
93
|
if (values.defaultReservationOutputTokens !== undefined) {
|
|
92
94
|
updateData.defaultReservationOutputTokens = values.defaultReservationOutputTokens;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context } from '@nocobase/actions';
|
|
11
|
+
|
|
12
|
+
const USER_SUMMARY_FIELDS = ['id', 'username', 'nickname', 'email'];
|
|
13
|
+
|
|
14
|
+
async function getByUser(ctx: Context, next: () => Promise<void>) {
|
|
15
|
+
const { userId } = ctx.action.params;
|
|
16
|
+
if (!userId) {
|
|
17
|
+
ctx.throw(400, 'userId is required');
|
|
18
|
+
}
|
|
19
|
+
const member = await ctx.db.getRepository('aiApiGroupMembers').findOne({
|
|
20
|
+
filter: { userId },
|
|
21
|
+
appends: ['group'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (member && member.get('group')) {
|
|
25
|
+
ctx.body = member.get('group');
|
|
26
|
+
return next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultGroup = await ctx.db.getRepository('aiApiUsageGroups').findOne({
|
|
30
|
+
filter: { isDefault: true },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!defaultGroup) {
|
|
34
|
+
ctx.throw(404, 'Default group not found');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctx.body = defaultGroup;
|
|
38
|
+
return next();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function listUnassignedUsers(ctx: Context, next: () => Promise<void>) {
|
|
42
|
+
const { page = 1, pageSize = 20, keyword } = ctx.action.params;
|
|
43
|
+
|
|
44
|
+
const memberRows = (await ctx.db.getRepository('aiApiGroupMembers').find({
|
|
45
|
+
fields: ['userId'],
|
|
46
|
+
pageSize: 10000,
|
|
47
|
+
})) as unknown[];
|
|
48
|
+
const assignedUserIds = memberRows.map((row) => {
|
|
49
|
+
const value = (row as Record<string, unknown>).get?.('userId') ?? (row as Record<string, unknown>).userId;
|
|
50
|
+
return String(value);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const where: Record<string, unknown> = {};
|
|
54
|
+
if (keyword) {
|
|
55
|
+
where.$or = [
|
|
56
|
+
{ username: { $includes: keyword } },
|
|
57
|
+
{ email: { $includes: keyword } },
|
|
58
|
+
{ nickname: { $includes: keyword } },
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
if (assignedUserIds.length > 0) {
|
|
62
|
+
where.id = { $notIn: assignedUserIds };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const repository = ctx.db.getRepository('users');
|
|
66
|
+
const [rows, count] = await repository.findAndCount({
|
|
67
|
+
filter: where,
|
|
68
|
+
fields: USER_SUMMARY_FIELDS,
|
|
69
|
+
page: Number(page),
|
|
70
|
+
pageSize: Number(pageSize),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ctx.body = { rows, count };
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function searchUsers(ctx: Context, next: () => Promise<void>) {
|
|
78
|
+
const { keyword, pageSize = 20 } = ctx.action.params;
|
|
79
|
+
|
|
80
|
+
const where: Record<string, unknown> = {};
|
|
81
|
+
if (keyword) {
|
|
82
|
+
where.$or = [
|
|
83
|
+
{ username: { $includes: keyword } },
|
|
84
|
+
{ email: { $includes: keyword } },
|
|
85
|
+
{ nickname: { $includes: keyword } },
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [rows, count] = await ctx.db.getRepository('users').findAndCount({
|
|
90
|
+
filter: where,
|
|
91
|
+
fields: USER_SUMMARY_FIELDS,
|
|
92
|
+
page: 1,
|
|
93
|
+
pageSize: Number(pageSize),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
ctx.body = { rows, count };
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function addMember(ctx: Context, next: () => Promise<void>) {
|
|
101
|
+
const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
|
|
102
|
+
if (!groupId || !userId) {
|
|
103
|
+
ctx.throw(400, 'groupId and userId are required');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const group = await ctx.db.getRepository('aiApiUsageGroups').findOne({
|
|
107
|
+
filterByTk: groupId,
|
|
108
|
+
});
|
|
109
|
+
if (!group) {
|
|
110
|
+
ctx.throw(404, 'Usage group not found');
|
|
111
|
+
}
|
|
112
|
+
// The default group is a fallback, not a membership target: an explicit row would
|
|
113
|
+
// pin the user there and hide them from listUnassignedUsers forever.
|
|
114
|
+
if (group.get('isDefault')) {
|
|
115
|
+
ctx.throw(400, 'Cannot add members to the default group: users without another group use it automatically');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existing = await ctx.db.getRepository('aiApiGroupMembers').findOne({
|
|
119
|
+
filter: { userId },
|
|
120
|
+
});
|
|
121
|
+
if (existing) {
|
|
122
|
+
ctx.throw(409, 'User already belongs to a group');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const member = await ctx.db.getRepository('aiApiGroupMembers').create({
|
|
126
|
+
values: { groupId, userId },
|
|
127
|
+
});
|
|
128
|
+
ctx.body = member;
|
|
129
|
+
return next();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function removeMember(ctx: Context, next: () => Promise<void>) {
|
|
133
|
+
const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
|
|
134
|
+
if (!groupId || !userId) {
|
|
135
|
+
ctx.throw(400, 'groupId and userId are required');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const member = await ctx.db.getRepository('aiApiGroupMembers').findOne({
|
|
139
|
+
filter: { groupId, userId },
|
|
140
|
+
});
|
|
141
|
+
if (!member) {
|
|
142
|
+
ctx.throw(404, 'Member not found');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await ctx.db.getRepository('aiApiGroupMembers').destroy({
|
|
146
|
+
filterByTk: member.get('id'),
|
|
147
|
+
});
|
|
148
|
+
ctx.body = member;
|
|
149
|
+
return next();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default {
|
|
153
|
+
name: 'aiApiUsageGroups',
|
|
154
|
+
actions: {
|
|
155
|
+
getByUser: {
|
|
156
|
+
handler: getByUser,
|
|
157
|
+
},
|
|
158
|
+
listUnassignedUsers: {
|
|
159
|
+
handler: listUnassignedUsers,
|
|
160
|
+
},
|
|
161
|
+
searchUsers: {
|
|
162
|
+
handler: searchUsers,
|
|
163
|
+
},
|
|
164
|
+
addMember: {
|
|
165
|
+
handler: addMember,
|
|
166
|
+
},
|
|
167
|
+
removeMember: {
|
|
168
|
+
handler: removeMember,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -7,6 +7,7 @@ interface UsageSummaryRow {
|
|
|
7
7
|
inputTokens?: string | number;
|
|
8
8
|
outputTokens?: string | number;
|
|
9
9
|
totalTokens?: string | number;
|
|
10
|
+
promptCacheTokens?: string | number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
interface CostSummaryRow {
|
|
@@ -45,6 +46,7 @@ const aiApiUsageMonitorResource: ResourceOptions = {
|
|
|
45
46
|
[fn('COALESCE', fn('SUM', col('inputTokens')), 0), 'inputTokens'],
|
|
46
47
|
[fn('COALESCE', fn('SUM', col('outputTokens')), 0), 'outputTokens'],
|
|
47
48
|
[fn('COALESCE', fn('SUM', col('totalTokens')), 0), 'totalTokens'],
|
|
49
|
+
[fn('COALESCE', fn('SUM', col('promptCacheTokens')), 0), 'promptCacheTokens'],
|
|
48
50
|
],
|
|
49
51
|
where,
|
|
50
52
|
raw: true,
|
|
@@ -61,6 +63,7 @@ const aiApiUsageMonitorResource: ResourceOptions = {
|
|
|
61
63
|
inputTokens: Number(totals?.inputTokens ?? 0),
|
|
62
64
|
outputTokens: Number(totals?.outputTokens ?? 0),
|
|
63
65
|
totalTokens: Number(totals?.totalTokens ?? 0),
|
|
66
|
+
promptCacheTokens: Number(totals?.promptCacheTokens ?? 0),
|
|
64
67
|
costsByCurrency: costs.map((item) => ({
|
|
65
68
|
currency: item.currency || 'USD',
|
|
66
69
|
totalCost: String(item.totalCost ?? 0),
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { resolveModelString } from '../utils/resolve-service';
|
|
20
20
|
import { checkEmployeeAccess } from '../middleware/role-permission';
|
|
21
21
|
import { enforceModelAccess } from '../utils/user-permissions';
|
|
22
|
+
import { getAiApiConfig } from '../utils/request-cache';
|
|
22
23
|
import { isStreamingRequested } from '../utils/streaming';
|
|
23
24
|
import {
|
|
24
25
|
AgentRuntimeContext,
|
|
@@ -89,7 +90,7 @@ export async function handleAgentCompletions(ctx: Context, plugin: PluginAiApiSe
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// ─── Load config and validate AI Employee ─────────────────────────────────
|
|
92
|
-
const config = await ctx
|
|
93
|
+
const config = await getAiApiConfig(ctx);
|
|
93
94
|
const defaultAiEmployee = config ? config.get('defaultAiEmployee') || config.defaultAiEmployee : null;
|
|
94
95
|
if (!defaultAiEmployee) {
|
|
95
96
|
ctx.status = 400;
|