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.
Files changed (128) hide show
  1. package/README.md +51 -12
  2. package/dist/client/185.c47663fefaeb0e5b.js +10 -0
  3. package/dist/client/562.9012cfd1fa04303d.js +10 -0
  4. package/dist/client/685.b5b1e0a5b825d253.js +10 -0
  5. package/dist/client/97.1bc5103fd9d995a8.js +10 -0
  6. package/dist/client/index.js +1 -1
  7. package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
  8. package/dist/client-v2/562.db2984167250b1be.js +10 -0
  9. package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
  10. package/dist/client-v2/97.96da323832251796.js +10 -0
  11. package/dist/client-v2/index.js +1 -1
  12. package/dist/externalVersion.js +8 -8
  13. package/dist/locale/en-US.json +38 -1
  14. package/dist/locale/vi-VN.json +177 -138
  15. package/dist/locale/zh-CN.json +177 -138
  16. package/dist/server/billing.js +17 -9
  17. package/dist/server/collections/ai-api-model-metadata.js +26 -0
  18. package/dist/server/collections/ai-api-model-prices.js +8 -0
  19. package/dist/server/collections/ai-api-response-records.js +101 -0
  20. package/dist/server/collections/ai-api-usage-records.js +1 -0
  21. package/dist/server/collections/ai-api-virtual-models.js +68 -0
  22. package/dist/server/middleware/response-record-resource.js +66 -0
  23. package/dist/server/middleware/role-permission.js +43 -18
  24. package/dist/server/migrations/20260827000000-add-cache-input-price.js +66 -0
  25. package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
  26. package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
  27. package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
  28. package/dist/server/plugin.js +66 -16
  29. package/dist/server/routes/auth.js +16 -0
  30. package/dist/server/routes/chat-completions.js +38 -6
  31. package/dist/server/routes/completions.js +16 -4
  32. package/dist/server/routes/embeddings.js +34 -5
  33. package/dist/server/routes/models.js +29 -0
  34. package/dist/server/routes/responses.js +530 -0
  35. package/dist/server/routes/router.js +65 -10
  36. package/dist/server/usage.js +26 -4
  37. package/dist/server/utils/direct-llm-context.js +10 -9
  38. package/dist/server/utils/resolve-service.js +24 -0
  39. package/dist/server/utils/response-store.js +138 -0
  40. package/dist/server/utils/responses-format.js +686 -0
  41. package/dist/server/utils/responses-stream.js +330 -0
  42. package/dist/server/utils/virtual-models.js +238 -0
  43. package/dist/server/validation.js +45 -2
  44. package/dist/swagger.js +137 -0
  45. package/package.json +34 -32
  46. package/src/__tests__/locale.test.ts +43 -0
  47. package/src/client/__tests__/settings-registration.test.tsx +1 -0
  48. package/src/client/index.tsx +10 -10
  49. package/src/client/models/index.ts +12 -12
  50. package/src/client/plugin.tsx +9 -1
  51. package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
  52. package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
  53. package/src/client-v2/pages/ModelPricingPage.tsx +15 -0
  54. package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
  55. package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
  56. package/src/client-v2/plugin.tsx +8 -0
  57. package/src/index.ts +11 -11
  58. package/src/locale/en-US.json +38 -1
  59. package/src/locale/vi-VN.json +177 -138
  60. package/src/locale/zh-CN.json +177 -138
  61. package/src/server/__tests__/billing-quota.test.ts +8 -2
  62. package/src/server/__tests__/billing.test.ts +13 -0
  63. package/src/server/__tests__/embeddings.test.ts +184 -0
  64. package/src/server/__tests__/models.test.ts +21 -1
  65. package/src/server/__tests__/response-record-resource.test.ts +50 -0
  66. package/src/server/__tests__/response-store-integration.test.ts +341 -0
  67. package/src/server/__tests__/response-store.test.ts +195 -0
  68. package/src/server/__tests__/responses-contract.test.ts +469 -0
  69. package/src/server/__tests__/responses-format.test.ts +299 -0
  70. package/src/server/__tests__/responses-router.test.ts +182 -0
  71. package/src/server/__tests__/responses-streaming.test.ts +368 -0
  72. package/src/server/__tests__/responses.test.ts +462 -0
  73. package/src/server/__tests__/role-permission.test.ts +139 -0
  74. package/src/server/__tests__/seed-role-permission.test.ts +88 -0
  75. package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
  76. package/src/server/__tests__/usage-groups.test.ts +96 -0
  77. package/src/server/__tests__/usage-route.test.ts +1 -0
  78. package/src/server/__tests__/usage.test.ts +15 -0
  79. package/src/server/__tests__/validation.test.ts +66 -7
  80. package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
  81. package/src/server/billing.ts +29 -6
  82. package/src/server/collections/ai-api-model-metadata.ts +26 -0
  83. package/src/server/collections/ai-api-model-prices.ts +8 -0
  84. package/src/server/collections/ai-api-response-records.ts +77 -0
  85. package/src/server/collections/ai-api-usage-records.ts +1 -0
  86. package/src/server/collections/ai-api-virtual-models.ts +58 -0
  87. package/src/server/middleware/response-record-resource.ts +44 -0
  88. package/src/server/middleware/role-permission.ts +69 -35
  89. package/src/server/migrations/20260827000000-add-cache-input-price.ts +49 -0
  90. package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
  91. package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
  92. package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
  93. package/src/server/plugin.ts +84 -20
  94. package/src/server/resource/ai-api-config.ts +2 -1
  95. package/src/server/routes/agent-completions.ts +3 -0
  96. package/src/server/routes/auth.ts +21 -1
  97. package/src/server/routes/chat-completions.ts +34 -10
  98. package/src/server/routes/completions.ts +16 -4
  99. package/src/server/routes/embeddings.ts +42 -6
  100. package/src/server/routes/models.ts +34 -0
  101. package/src/server/routes/responses.ts +640 -0
  102. package/src/server/routes/router.ts +81 -12
  103. package/src/server/services/__tests__/file-processor.test.ts +1 -0
  104. package/src/server/usage.ts +30 -2
  105. package/src/server/utils/app-observability.ts +1 -1
  106. package/src/server/utils/direct-llm-context.ts +23 -12
  107. package/src/server/utils/openai-format.ts +1 -0
  108. package/src/server/utils/resolve-service.ts +39 -1
  109. package/src/server/utils/response-store.ts +148 -0
  110. package/src/server/utils/responses-format.ts +974 -0
  111. package/src/server/utils/responses-stream.ts +384 -0
  112. package/src/server/utils/virtual-models.ts +320 -0
  113. package/src/server/validation.ts +50 -0
  114. package/src/swagger.ts +139 -0
  115. package/dist/client/562.44b16aad4718b4c7.js +0 -10
  116. package/dist/client/685.ae483e17b6b49c98.js +0 -10
  117. package/dist/client/97.9b6b2d2b01a4c060.js +0 -10
  118. package/dist/client-v2/562.45d5c504433be38b.js +0 -10
  119. package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
  120. package/dist/client-v2/97.36a42eff36bb3d8a.js +0 -10
  121. package/dist/server/collections/ai-api-user-permissions.js +0 -67
  122. package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
  123. package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
  124. package/dist/server/resource/ai-api-usage-groups.js +0 -168
  125. package/src/server/collections/ai-api-user-permissions.ts +0 -46
  126. package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
  127. package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
  128. 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: 'aiApiUsageGroups:listUnassignedUsers',
154
+ url: 'users:list',
132
155
  method: 'get',
133
- params: { keyword, pageSize: 100 },
156
+ params: {
157
+ filter: conditions.length ? { $and: conditions } : {},
158
+ fields: ['id', 'username', 'nickname', 'email'],
159
+ pageSize: 100,
160
+ },
134
161
  });
135
- const rows = unwrapData<UserSummary[]>(response, []);
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
- } as UsageGroup);
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
- await loadMembers(record.id);
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: 'aiApiUsageGroups:addMember',
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: 'aiApiUsageGroups:removeMember',
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 response = await ctx.api.request({
250
- url: 'aiApiUsageGroups:searchUsers',
278
+ const userResponse = await ctx.api.request({
279
+ url: 'users:list',
251
280
  method: 'get',
252
- params: { keyword: searchUserKeyword, pageSize: 1 },
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[]>(response, []);
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
- const groupResponse = await ctx.api.request({
261
- url: 'aiApiUsageGroups:getByUser',
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
- setSearchedGroup(unwrapData<UsageGroup>(groupResponse, null));
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
- <Alert
498
- type="info"
499
- showIcon
500
- message={t(
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">
@@ -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';
@@ -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
  }