plugin-ai-api 1.1.0 → 1.1.2
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/README.md +51 -12
- package/dist/client/185.c47663fefaeb0e5b.js +10 -0
- package/dist/client/562.9012cfd1fa04303d.js +10 -0
- package/dist/client/685.b5b1e0a5b825d253.js +10 -0
- package/dist/client/97.1bc5103fd9d995a8.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
- package/dist/client-v2/562.db2984167250b1be.js +10 -0
- package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
- package/dist/client-v2/97.96da323832251796.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +38 -1
- package/dist/locale/vi-VN.json +177 -138
- package/dist/locale/zh-CN.json +177 -138
- package/dist/server/billing.js +17 -9
- package/dist/server/collections/ai-api-model-metadata.js +26 -0
- package/dist/server/collections/ai-api-model-prices.js +8 -0
- package/dist/server/collections/ai-api-response-records.js +101 -0
- package/dist/server/collections/ai-api-usage-records.js +1 -0
- package/dist/server/collections/ai-api-virtual-models.js +68 -0
- package/dist/server/middleware/response-record-resource.js +66 -0
- package/dist/server/middleware/role-permission.js +43 -18
- package/dist/server/migrations/20260827000000-add-cache-input-price.js +66 -0
- package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
- package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
- package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
- package/dist/server/plugin.js +66 -16
- package/dist/server/routes/auth.js +16 -0
- package/dist/server/routes/chat-completions.js +38 -6
- package/dist/server/routes/completions.js +16 -4
- package/dist/server/routes/embeddings.js +34 -5
- package/dist/server/routes/models.js +29 -0
- package/dist/server/routes/responses.js +530 -0
- package/dist/server/routes/router.js +65 -10
- package/dist/server/usage.js +26 -4
- package/dist/server/utils/direct-llm-context.js +10 -9
- package/dist/server/utils/resolve-service.js +24 -0
- package/dist/server/utils/response-store.js +138 -0
- package/dist/server/utils/responses-format.js +686 -0
- package/dist/server/utils/responses-stream.js +330 -0
- package/dist/server/utils/virtual-models.js +238 -0
- package/dist/server/validation.js +45 -2
- package/dist/swagger.js +137 -0
- package/package.json +34 -32
- package/src/__tests__/locale.test.ts +43 -0
- package/src/client/__tests__/settings-registration.test.tsx +1 -0
- package/src/client/index.tsx +10 -10
- package/src/client/models/index.ts +12 -12
- package/src/client/plugin.tsx +9 -1
- package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
- package/src/client-v2/pages/ModelPricingPage.tsx +15 -0
- package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
- package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
- package/src/client-v2/plugin.tsx +8 -0
- package/src/index.ts +11 -11
- package/src/locale/en-US.json +38 -1
- package/src/locale/vi-VN.json +177 -138
- package/src/locale/zh-CN.json +177 -138
- package/src/server/__tests__/billing-quota.test.ts +8 -2
- package/src/server/__tests__/billing.test.ts +13 -0
- package/src/server/__tests__/embeddings.test.ts +184 -0
- package/src/server/__tests__/models.test.ts +21 -1
- package/src/server/__tests__/response-record-resource.test.ts +50 -0
- package/src/server/__tests__/response-store-integration.test.ts +341 -0
- package/src/server/__tests__/response-store.test.ts +195 -0
- package/src/server/__tests__/responses-contract.test.ts +469 -0
- package/src/server/__tests__/responses-format.test.ts +299 -0
- package/src/server/__tests__/responses-router.test.ts +182 -0
- package/src/server/__tests__/responses-streaming.test.ts +368 -0
- package/src/server/__tests__/responses.test.ts +462 -0
- package/src/server/__tests__/role-permission.test.ts +139 -0
- package/src/server/__tests__/seed-role-permission.test.ts +88 -0
- package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
- package/src/server/__tests__/usage-groups.test.ts +96 -0
- package/src/server/__tests__/usage-route.test.ts +1 -0
- package/src/server/__tests__/usage.test.ts +15 -0
- package/src/server/__tests__/validation.test.ts +66 -7
- package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
- package/src/server/billing.ts +29 -6
- package/src/server/collections/ai-api-model-metadata.ts +26 -0
- package/src/server/collections/ai-api-model-prices.ts +8 -0
- package/src/server/collections/ai-api-response-records.ts +77 -0
- package/src/server/collections/ai-api-usage-records.ts +1 -0
- package/src/server/collections/ai-api-virtual-models.ts +58 -0
- package/src/server/middleware/response-record-resource.ts +44 -0
- package/src/server/middleware/role-permission.ts +69 -35
- package/src/server/migrations/20260827000000-add-cache-input-price.ts +49 -0
- package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
- package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
- package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
- package/src/server/plugin.ts +84 -20
- package/src/server/resource/ai-api-config.ts +2 -1
- package/src/server/routes/agent-completions.ts +3 -0
- package/src/server/routes/auth.ts +21 -1
- package/src/server/routes/chat-completions.ts +34 -10
- package/src/server/routes/completions.ts +16 -4
- package/src/server/routes/embeddings.ts +42 -6
- package/src/server/routes/models.ts +34 -0
- package/src/server/routes/responses.ts +640 -0
- package/src/server/routes/router.ts +81 -12
- package/src/server/services/__tests__/file-processor.test.ts +1 -0
- package/src/server/usage.ts +30 -2
- package/src/server/utils/app-observability.ts +1 -1
- package/src/server/utils/direct-llm-context.ts +23 -12
- package/src/server/utils/openai-format.ts +1 -0
- package/src/server/utils/resolve-service.ts +39 -1
- package/src/server/utils/response-store.ts +148 -0
- package/src/server/utils/responses-format.ts +974 -0
- package/src/server/utils/responses-stream.ts +384 -0
- package/src/server/utils/virtual-models.ts +320 -0
- package/src/server/validation.ts +50 -0
- package/src/swagger.ts +139 -0
- package/dist/client/562.44b16aad4718b4c7.js +0 -10
- package/dist/client/685.ae483e17b6b49c98.js +0 -10
- package/dist/client/97.9b6b2d2b01a4c060.js +0 -10
- package/dist/client-v2/562.45d5c504433be38b.js +0 -10
- package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
- package/dist/client-v2/97.36a42eff36bb3d8a.js +0 -10
- package/dist/server/collections/ai-api-user-permissions.js +0 -67
- package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
- package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
- package/dist/server/resource/ai-api-usage-groups.js +0 -168
- package/src/server/collections/ai-api-user-permissions.ts +0 -46
- package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
- package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
- package/src/server/resource/ai-api-usage-groups.ts +0 -171
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Button, Card, Form, Input, Select, Space, Switch, Typography, message } from 'antd';
|
|
3
|
+
import { useFlowContext } from '@nocobase/flow-engine';
|
|
4
|
+
import { useT } from '../locale';
|
|
5
|
+
import { errorMessage, unwrapData } from './api';
|
|
6
|
+
|
|
7
|
+
const { Text } = Typography;
|
|
8
|
+
|
|
9
|
+
interface VirtualModel {
|
|
10
|
+
id?: string | number;
|
|
11
|
+
name: string;
|
|
12
|
+
mode: string;
|
|
13
|
+
fallbackModel: string;
|
|
14
|
+
visionModels: string[];
|
|
15
|
+
toolModels: string[];
|
|
16
|
+
reasoningModels: string[];
|
|
17
|
+
cheapModels: string[];
|
|
18
|
+
generalModels: string[];
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EnabledLlmService {
|
|
23
|
+
llmService: string;
|
|
24
|
+
llmServiceTitle?: string;
|
|
25
|
+
enabledModels?: Array<{ label?: string; value: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ModelOption {
|
|
29
|
+
value: string; // "serviceName/modelId"
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_ALIAS = 'auto';
|
|
34
|
+
|
|
35
|
+
export default function ModelRoutingPage() {
|
|
36
|
+
const ctx = useFlowContext();
|
|
37
|
+
const t = useT();
|
|
38
|
+
const [form] = Form.useForm<Partial<VirtualModel>>();
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const [saving, setSaving] = useState(false);
|
|
41
|
+
const [options, setOptions] = useState<ModelOption[]>([]);
|
|
42
|
+
const [recordId, setRecordId] = useState<string | number | undefined>();
|
|
43
|
+
|
|
44
|
+
const load = useCallback(async () => {
|
|
45
|
+
setLoading(true);
|
|
46
|
+
try {
|
|
47
|
+
const [vmRes, servicesRes] = await Promise.all([
|
|
48
|
+
ctx.api.request({
|
|
49
|
+
url: 'aiApiVirtualModels:list',
|
|
50
|
+
method: 'get',
|
|
51
|
+
params: { filter: { name: DEFAULT_ALIAS }, pageSize: 1 },
|
|
52
|
+
}),
|
|
53
|
+
// Use the normalized catalog so recommended/provider/custom modes all surface their
|
|
54
|
+
// actual runtime models. The raw llmServices endpoint can return an empty enabledModels
|
|
55
|
+
// array for recommended-mode services, which would leave this page with no choices.
|
|
56
|
+
ctx.api.request({ url: 'ai:listAllEnabledModels', method: 'get' }),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Build the shared option list: every enabled model of every service, as "service/model".
|
|
60
|
+
const svcList = unwrapData<EnabledLlmService[]>(servicesRes, []);
|
|
61
|
+
const opts: ModelOption[] = [];
|
|
62
|
+
for (const service of svcList) {
|
|
63
|
+
const label = service.llmServiceTitle || service.llmService;
|
|
64
|
+
for (const model of service.enabledModels || []) {
|
|
65
|
+
if (!model?.value) continue;
|
|
66
|
+
opts.push({
|
|
67
|
+
value: `${service.llmService}/${model.value}`,
|
|
68
|
+
label: `${label} / ${model.label || model.value}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const existing = unwrapData<VirtualModel[]>(vmRes, [])[0];
|
|
74
|
+
// Keep stale references visible: values already configured on this alias but no longer
|
|
75
|
+
// present in the enabled catalog are merged back into the options so admins can still
|
|
76
|
+
// see, reorder, and replace them instead of being left with an opaque raw value.
|
|
77
|
+
if (existing) {
|
|
78
|
+
const known = new Set(opts.map((option) => option.value));
|
|
79
|
+
const configured = [
|
|
80
|
+
existing.fallbackModel,
|
|
81
|
+
...(existing.visionModels || []),
|
|
82
|
+
...(existing.toolModels || []),
|
|
83
|
+
...(existing.reasoningModels || []),
|
|
84
|
+
...(existing.cheapModels || []),
|
|
85
|
+
...(existing.generalModels || []),
|
|
86
|
+
].filter((value): value is string => typeof value === 'string' && value.length > 0);
|
|
87
|
+
for (const value of configured) {
|
|
88
|
+
if (known.has(value)) continue;
|
|
89
|
+
known.add(value);
|
|
90
|
+
opts.push({ value, label: `${value} (${t('unavailable')})` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
setOptions(opts);
|
|
94
|
+
if (existing) {
|
|
95
|
+
setRecordId(existing.id);
|
|
96
|
+
form.setFieldsValue({
|
|
97
|
+
name: existing.name,
|
|
98
|
+
mode: existing.mode || 'chat',
|
|
99
|
+
fallbackModel: existing.fallbackModel,
|
|
100
|
+
visionModels: existing.visionModels || [],
|
|
101
|
+
toolModels: existing.toolModels || [],
|
|
102
|
+
reasoningModels: existing.reasoningModels || [],
|
|
103
|
+
cheapModels: existing.cheapModels || [],
|
|
104
|
+
generalModels: existing.generalModels || [],
|
|
105
|
+
enabled: existing.enabled !== false,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
form.setFieldsValue({
|
|
109
|
+
name: DEFAULT_ALIAS,
|
|
110
|
+
mode: 'chat',
|
|
111
|
+
fallbackModel: undefined,
|
|
112
|
+
enabled: true,
|
|
113
|
+
visionModels: [],
|
|
114
|
+
toolModels: [],
|
|
115
|
+
reasoningModels: [],
|
|
116
|
+
cheapModels: [],
|
|
117
|
+
generalModels: [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
message.error(errorMessage(error));
|
|
122
|
+
} finally {
|
|
123
|
+
setLoading(false);
|
|
124
|
+
}
|
|
125
|
+
}, [ctx.api, form, t]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
load();
|
|
129
|
+
}, [load]);
|
|
130
|
+
|
|
131
|
+
const save = async () => {
|
|
132
|
+
const values = await form.validateFields();
|
|
133
|
+
setSaving(true);
|
|
134
|
+
try {
|
|
135
|
+
if (recordId) {
|
|
136
|
+
await ctx.api.request({ url: `aiApiVirtualModels:update/${recordId}`, method: 'post', data: values });
|
|
137
|
+
} else {
|
|
138
|
+
const res = await ctx.api.request({ url: 'aiApiVirtualModels:create', method: 'post', data: values });
|
|
139
|
+
const created = unwrapData<VirtualModel | undefined>(res, undefined);
|
|
140
|
+
if (created?.id) setRecordId(created.id);
|
|
141
|
+
}
|
|
142
|
+
message.success(t('Saved successfully'));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
message.error(errorMessage(error));
|
|
145
|
+
} finally {
|
|
146
|
+
setSaving(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Keep stored order visible: render selected tags in the order of the field value,
|
|
151
|
+
// and let the dropdown offer the remaining options.
|
|
152
|
+
const orderedSelectProps = {
|
|
153
|
+
options,
|
|
154
|
+
optionFilterProp: 'label' as const,
|
|
155
|
+
showSearch: true,
|
|
156
|
+
loading,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const listField = (name: keyof VirtualModel, label: string, tooltip: string) => (
|
|
160
|
+
<Form.Item name={name} label={label} tooltip={tooltip}>
|
|
161
|
+
<Select
|
|
162
|
+
mode="multiple"
|
|
163
|
+
placeholder={t('Select models — the order shown is the routing priority')}
|
|
164
|
+
{...orderedSelectProps}
|
|
165
|
+
/>
|
|
166
|
+
</Form.Item>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Card loading={loading} title={t('Model routing (virtual models)')}>
|
|
171
|
+
<Form form={form} layout="vertical">
|
|
172
|
+
<Form.Item name="name" label={t('Alias')} rules={[{ required: true }]}>
|
|
173
|
+
<Input disabled style={{ maxWidth: 240 }} />
|
|
174
|
+
</Form.Item>
|
|
175
|
+
<Form.Item name="mode" hidden>
|
|
176
|
+
<Input />
|
|
177
|
+
</Form.Item>
|
|
178
|
+
<Form.Item
|
|
179
|
+
name="fallbackModel"
|
|
180
|
+
label={t('Fallback model')}
|
|
181
|
+
rules={[{ required: true, message: t('A fallback model is required') }]}
|
|
182
|
+
tooltip={t(
|
|
183
|
+
'Used when no capability bucket candidate is usable and the fallback is permitted for the caller.',
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<Select
|
|
187
|
+
placeholder={t('Select a fallback model')}
|
|
188
|
+
options={options}
|
|
189
|
+
optionFilterProp="label"
|
|
190
|
+
showSearch
|
|
191
|
+
style={{ maxWidth: 480 }}
|
|
192
|
+
/>
|
|
193
|
+
</Form.Item>
|
|
194
|
+
|
|
195
|
+
{listField(
|
|
196
|
+
'visionModels',
|
|
197
|
+
t('Vision models (in order)'),
|
|
198
|
+
t('Requests with an image or file block use the first permitted model here.'),
|
|
199
|
+
)}
|
|
200
|
+
{listField(
|
|
201
|
+
'toolModels',
|
|
202
|
+
t('Tool-calling models (in order)'),
|
|
203
|
+
t('Requests with tools/tool_choice use the first permitted model here.'),
|
|
204
|
+
)}
|
|
205
|
+
{listField(
|
|
206
|
+
'reasoningModels',
|
|
207
|
+
t('Reasoning models (in order)'),
|
|
208
|
+
t('Requests with an explicit reasoning or reasoning_effort parameter use the first permitted model here.'),
|
|
209
|
+
)}
|
|
210
|
+
{listField(
|
|
211
|
+
'cheapModels',
|
|
212
|
+
t('Cheap models (in order)'),
|
|
213
|
+
t('Optional. When set, cheap-eligible requests use the first permitted model here.'),
|
|
214
|
+
)}
|
|
215
|
+
{listField(
|
|
216
|
+
'generalModels',
|
|
217
|
+
t('General models (in order)'),
|
|
218
|
+
t(
|
|
219
|
+
'Default bucket when no capability rule matched. Leave empty to derive from all enabled models ordered by Model metadata sortOrder.',
|
|
220
|
+
),
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
|
|
224
|
+
<Switch />
|
|
225
|
+
</Form.Item>
|
|
226
|
+
|
|
227
|
+
<Space>
|
|
228
|
+
<Button type="primary" onClick={save} loading={saving}>
|
|
229
|
+
{t('Save')}
|
|
230
|
+
</Button>
|
|
231
|
+
<Text type="secondary">
|
|
232
|
+
{t('An empty bucket is derived automatically from Model metadata (capability flags + sortOrder).')}
|
|
233
|
+
</Text>
|
|
234
|
+
</Space>
|
|
235
|
+
</Form>
|
|
236
|
+
</Card>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -66,7 +66,7 @@ interface GroupMember {
|
|
|
66
66
|
export default function UsageGroupsPage() {
|
|
67
67
|
const ctx = useFlowContext();
|
|
68
68
|
const t = useT();
|
|
69
|
-
const [form] = Form.useForm<UsageGroup
|
|
69
|
+
const [form] = Form.useForm<Partial<UsageGroup>>();
|
|
70
70
|
const [memberForm] = Form.useForm<{ userId: string | number }>();
|
|
71
71
|
const selectedServices = Form.useWatch('allowedLlmServices', form);
|
|
72
72
|
const allowAllModels = Form.useWatch('allowAllModels', form);
|
|
@@ -127,13 +127,39 @@ export default function UsageGroupsPage() {
|
|
|
127
127
|
const loadUnassignedUsers = useCallback(
|
|
128
128
|
async (keyword?: string) => {
|
|
129
129
|
try {
|
|
130
|
+
// Plain collection reads on the main data source, so core ACL enforces
|
|
131
|
+
// aiApiGroupMembers:list and users:list. No custom server action needed.
|
|
132
|
+
const memberResponse = await ctx.api.request({
|
|
133
|
+
url: 'aiApiGroupMembers:list',
|
|
134
|
+
method: 'get',
|
|
135
|
+
params: { fields: ['userId'], paginate: false, pageSize: 10000 },
|
|
136
|
+
});
|
|
137
|
+
const assignedIds = new Set(
|
|
138
|
+
unwrapData<Array<{ userId: string | number }>>(memberResponse, []).map((row) => String(row.userId)),
|
|
139
|
+
);
|
|
140
|
+
const conditions: Record<string, unknown>[] = [];
|
|
141
|
+
if (keyword) {
|
|
142
|
+
conditions.push({
|
|
143
|
+
$or: [
|
|
144
|
+
{ username: { $includes: keyword } },
|
|
145
|
+
{ email: { $includes: keyword } },
|
|
146
|
+
{ nickname: { $includes: keyword } },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (assignedIds.size > 0) {
|
|
151
|
+
conditions.push({ id: { $notIn: [...assignedIds] } });
|
|
152
|
+
}
|
|
130
153
|
const response = await ctx.api.request({
|
|
131
|
-
url: '
|
|
154
|
+
url: 'users:list',
|
|
132
155
|
method: 'get',
|
|
133
|
-
params: {
|
|
156
|
+
params: {
|
|
157
|
+
filter: conditions.length ? { $and: conditions } : {},
|
|
158
|
+
fields: ['id', 'username', 'nickname', 'email'],
|
|
159
|
+
pageSize: 100,
|
|
160
|
+
},
|
|
134
161
|
});
|
|
135
|
-
|
|
136
|
-
setUnassignedUsers(rows);
|
|
162
|
+
setUnassignedUsers(unwrapData<UserSummary[]>(response, []));
|
|
137
163
|
} catch (error) {
|
|
138
164
|
message.error(errorMessage(error));
|
|
139
165
|
}
|
|
@@ -162,7 +188,7 @@ export default function UsageGroupsPage() {
|
|
|
162
188
|
allowedLlmServices: [],
|
|
163
189
|
allowAllModels: true,
|
|
164
190
|
allowedModels: [],
|
|
165
|
-
}
|
|
191
|
+
});
|
|
166
192
|
setOpen(true);
|
|
167
193
|
};
|
|
168
194
|
|
|
@@ -170,8 +196,10 @@ export default function UsageGroupsPage() {
|
|
|
170
196
|
setEditing(record);
|
|
171
197
|
form.setFieldsValue(record);
|
|
172
198
|
setOpen(true);
|
|
173
|
-
|
|
199
|
+
// The default group has no explicit members by design; only load the member
|
|
200
|
+
// list and unassigned users for regular groups.
|
|
174
201
|
if (!record.isDefault) {
|
|
202
|
+
await loadMembers(record.id);
|
|
175
203
|
await loadUnassignedUsers();
|
|
176
204
|
}
|
|
177
205
|
};
|
|
@@ -212,8 +240,10 @@ export default function UsageGroupsPage() {
|
|
|
212
240
|
if (!editing) return;
|
|
213
241
|
const values = await memberForm.validateFields();
|
|
214
242
|
try {
|
|
243
|
+
// Plain create on aiApiGroupMembers; the server beforeSave hook enforces the
|
|
244
|
+
// one-group-per-user and no-default-group-membership rules.
|
|
215
245
|
await ctx.api.request({
|
|
216
|
-
url: '
|
|
246
|
+
url: 'aiApiGroupMembers:create',
|
|
217
247
|
method: 'post',
|
|
218
248
|
data: { groupId: editing.id, userId: values.userId },
|
|
219
249
|
});
|
|
@@ -229,9 +259,8 @@ export default function UsageGroupsPage() {
|
|
|
229
259
|
const removeMember = async (member: GroupMember) => {
|
|
230
260
|
try {
|
|
231
261
|
await ctx.api.request({
|
|
232
|
-
url:
|
|
262
|
+
url: `aiApiGroupMembers:destroy/${member.id}`,
|
|
233
263
|
method: 'post',
|
|
234
|
-
data: { groupId: member.groupId, userId: member.userId },
|
|
235
264
|
});
|
|
236
265
|
message.success(t('Member removed'));
|
|
237
266
|
if (editing) {
|
|
@@ -246,23 +275,44 @@ export default function UsageGroupsPage() {
|
|
|
246
275
|
const searchGroupByUser = async () => {
|
|
247
276
|
if (!searchUserKeyword.trim()) return;
|
|
248
277
|
try {
|
|
249
|
-
const
|
|
250
|
-
url: '
|
|
278
|
+
const userResponse = await ctx.api.request({
|
|
279
|
+
url: 'users:list',
|
|
251
280
|
method: 'get',
|
|
252
|
-
params: {
|
|
281
|
+
params: {
|
|
282
|
+
filter: {
|
|
283
|
+
$or: [
|
|
284
|
+
{ username: { $includes: searchUserKeyword } },
|
|
285
|
+
{ email: { $includes: searchUserKeyword } },
|
|
286
|
+
{ nickname: { $includes: searchUserKeyword } },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
fields: ['id', 'username', 'nickname', 'email'],
|
|
290
|
+
pageSize: 1,
|
|
291
|
+
},
|
|
253
292
|
});
|
|
254
|
-
const users = unwrapData<UserSummary[]>(
|
|
293
|
+
const users = unwrapData<UserSummary[]>(userResponse, []);
|
|
255
294
|
if (users.length === 0) {
|
|
256
295
|
message.warning(t('User not found'));
|
|
257
296
|
setSearchedGroup(null);
|
|
258
297
|
return;
|
|
259
298
|
}
|
|
260
|
-
|
|
261
|
-
|
|
299
|
+
// Look up the membership row; a user with no row falls back to the default group.
|
|
300
|
+
const memberResponse = await ctx.api.request({
|
|
301
|
+
url: 'aiApiGroupMembers:list',
|
|
262
302
|
method: 'get',
|
|
263
|
-
params: { userId: users[0].id },
|
|
303
|
+
params: { filter: { userId: users[0].id }, appends: ['group'], pageSize: 1 },
|
|
264
304
|
});
|
|
265
|
-
|
|
305
|
+
const memberRows = unwrapData<Array<{ group?: UsageGroup }>>(memberResponse, []);
|
|
306
|
+
if (memberRows[0]?.group) {
|
|
307
|
+
setSearchedGroup(memberRows[0].group);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const defaultResponse = await ctx.api.request({
|
|
311
|
+
url: 'aiApiUsageGroups:list',
|
|
312
|
+
method: 'get',
|
|
313
|
+
params: { filter: { isDefault: true }, pageSize: 1 },
|
|
314
|
+
});
|
|
315
|
+
setSearchedGroup(unwrapData<UsageGroup[]>(defaultResponse, [])[0] ?? null);
|
|
266
316
|
} catch (error) {
|
|
267
317
|
message.error(errorMessage(error));
|
|
268
318
|
}
|
|
@@ -491,28 +541,15 @@ export default function UsageGroupsPage() {
|
|
|
491
541
|
</Form.Item>
|
|
492
542
|
</Form>
|
|
493
543
|
{editing && (
|
|
494
|
-
<Card title={t('Members')} size="small" style={{ marginTop: 24 }}>
|
|
544
|
+
<Card title={editing.isDefault ? t('Membership') : t('Members')} size="small" style={{ marginTop: 24 }}>
|
|
495
545
|
{editing.isDefault ? (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
'Users who do not belong to any other group automatically use this default group — no need to add members.',
|
|
502
|
-
)}
|
|
503
|
-
style={{ marginBottom: members.length > 0 ? 12 : 0 }}
|
|
504
|
-
/>
|
|
505
|
-
{members.length > 0 && (
|
|
506
|
-
<Table
|
|
507
|
-
rowKey="id"
|
|
508
|
-
columns={memberColumns}
|
|
509
|
-
dataSource={members}
|
|
510
|
-
loading={membersLoading}
|
|
511
|
-
pagination={false}
|
|
512
|
-
size="small"
|
|
513
|
-
/>
|
|
546
|
+
<Alert
|
|
547
|
+
type="info"
|
|
548
|
+
showIcon
|
|
549
|
+
message={t(
|
|
550
|
+
'Users who do not belong to any other group automatically use this default group — no need to add members.',
|
|
514
551
|
)}
|
|
515
|
-
|
|
552
|
+
/>
|
|
516
553
|
) : (
|
|
517
554
|
<>
|
|
518
555
|
<Form form={memberForm} layout="inline">
|
package/src/client-v2/plugin.tsx
CHANGED
|
@@ -51,6 +51,14 @@ export class PluginAiApiClient extends Plugin<Record<string, never>, Application
|
|
|
51
51
|
componentLoader: () => import('./pages/ModelMetadataPage'),
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
55
|
+
menuKey: 'ai-api',
|
|
56
|
+
key: 'model-routing',
|
|
57
|
+
title: this.t('Model routing'),
|
|
58
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
59
|
+
sort: 4,
|
|
60
|
+
componentLoader: () => import('./pages/ModelRoutingPage'),
|
|
61
|
+
});
|
|
54
62
|
this.pluginSettingsManager.addPageTabItem({
|
|
55
63
|
menuKey: 'ai-api',
|
|
56
64
|
key: 'usage-groups',
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export * from './server';
|
|
11
|
-
export { default } from './server';
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './server';
|
|
11
|
+
export { default } from './server';
|
package/src/locale/en-US.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"LLM service": "LLM service",
|
|
21
21
|
"Model": "Model",
|
|
22
22
|
"Input price / 1M": "Input price / 1M",
|
|
23
|
+
"Cache input price / 1M": "Cache input price / 1M",
|
|
23
24
|
"Output price / 1M": "Output price / 1M",
|
|
24
25
|
"Fixed request cost": "Fixed request cost",
|
|
25
26
|
"Currency": "Currency",
|
|
@@ -134,5 +135,41 @@
|
|
|
134
135
|
"All services": "All services",
|
|
135
136
|
"No models": "No models",
|
|
136
137
|
"Leave empty to allow every service enabled in the general configuration.": "Leave empty to allow every service enabled in the general configuration.",
|
|
137
|
-
"Users who do not belong to any other group automatically use this default group — no need to add members.": "Users who do not belong to any other group automatically use this default group — no need to add members."
|
|
138
|
+
"Users who do not belong to any other group automatically use this default group — no need to add members.": "Users who do not belong to any other group automatically use this default group — no need to add members.",
|
|
139
|
+
"Membership": "Membership",
|
|
140
|
+
"Model routing": "Model routing",
|
|
141
|
+
"Model routing (virtual models)": "Model routing (virtual models)",
|
|
142
|
+
"Alias": "Alias",
|
|
143
|
+
"Fallback model": "Fallback model",
|
|
144
|
+
"A fallback model is required": "A fallback model is required",
|
|
145
|
+
"Used when no capability bucket candidate is usable and the fallback is permitted for the caller.": "Used when no capability bucket candidate is usable and the fallback is permitted for the caller.",
|
|
146
|
+
"Vision models (in order)": "Vision models (in order)",
|
|
147
|
+
"Requests with an image or file block use the first permitted model here.": "Requests with an image or file block use the first permitted model here.",
|
|
148
|
+
"Tool-calling models (in order)": "Tool-calling models (in order)",
|
|
149
|
+
"Requests with tools/tool_choice use the first permitted model here.": "Requests with tools/tool_choice use the first permitted model here.",
|
|
150
|
+
"Reasoning models (in order)": "Reasoning models (in order)",
|
|
151
|
+
"Requests with an explicit reasoning or reasoning_effort parameter use the first permitted model here.": "Requests with an explicit reasoning or reasoning_effort parameter use the first permitted model here.",
|
|
152
|
+
"Cheap models (in order)": "Cheap models (in order)",
|
|
153
|
+
"Optional. When set, cheap-eligible requests use the first permitted model here.": "Optional. When set, cheap-eligible requests use the first permitted model here.",
|
|
154
|
+
"General models (in order)": "General models (in order)",
|
|
155
|
+
"Default bucket when no capability rule matched. Leave empty to derive from all enabled models ordered by Model metadata sortOrder.": "Default bucket when no capability rule matched. Leave empty to derive from all enabled models ordered by Model metadata sortOrder.",
|
|
156
|
+
"Type service/model and press Enter — earlier entries are preferred": "Type service/model and press Enter — earlier entries are preferred",
|
|
157
|
+
"An empty bucket is derived automatically from Model metadata (capability flags + sortOrder).": "An empty bucket is derived automatically from Model metadata (capability flags + sortOrder).",
|
|
158
|
+
"Save": "Save",
|
|
159
|
+
"Supports vision": "Supports vision",
|
|
160
|
+
"Used by virtual-model routing for image/file requests.": "Used by virtual-model routing for image/file requests.",
|
|
161
|
+
"Supports tool calling": "Supports tool calling",
|
|
162
|
+
"Used by virtual-model routing for requests with tools/tool_choice.": "Used by virtual-model routing for requests with tools/tool_choice.",
|
|
163
|
+
"Reasoning tier": "Reasoning tier",
|
|
164
|
+
"cheap | general | reasoning. Used by virtual-model routing buckets.": "cheap | general | reasoning. Used by virtual-model routing buckets.",
|
|
165
|
+
"Cheap": "Cheap",
|
|
166
|
+
"General": "General",
|
|
167
|
+
"Reasoning": "Reasoning",
|
|
168
|
+
"Routing priority": "Routing priority",
|
|
169
|
+
"Ascending — lower is preferred when a bucket is derived from metadata.": "Ascending — lower is preferred when a bucket is derived from metadata.",
|
|
170
|
+
"Select models — the order shown is the routing priority": "Select models — the order shown is the routing priority",
|
|
171
|
+
"Select a fallback model": "Select a fallback model",
|
|
172
|
+
"Name": "Name",
|
|
173
|
+
"Rate limit/min": "Rate limit/min",
|
|
174
|
+
"unavailable": "(unavailable)"
|
|
138
175
|
}
|