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.
Files changed (130) hide show
  1. package/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
  2. package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
  3. package/dist/client/562.44b16aad4718b4c7.js +10 -0
  4. package/dist/client/685.ae483e17b6b49c98.js +10 -0
  5. package/dist/client/757.6568d3504ad29352.js +10 -0
  6. package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
  7. package/dist/client/index.js +1 -1
  8. package/dist/client-v2/302.3971233415999b2c.js +10 -0
  9. package/dist/client-v2/562.45d5c504433be38b.js +10 -0
  10. package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
  11. package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
  12. package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
  13. package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
  14. package/dist/client-v2/index.js +1 -1
  15. package/dist/constants.js +2 -5
  16. package/dist/externalVersion.js +8 -8
  17. package/dist/locale/en-US.json +27 -8
  18. package/dist/locale/vi-VN.json +27 -8
  19. package/dist/locale/zh-CN.json +27 -8
  20. package/dist/server/billing.js +31 -33
  21. package/dist/server/collections/ai-api-config.js +7 -7
  22. package/dist/server/collections/ai-api-group-members.js +62 -0
  23. package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
  24. package/dist/server/collections/ai-api-model-metadata.js +6 -0
  25. package/dist/server/collections/ai-api-usage-groups.js +74 -0
  26. package/dist/server/collections/ai-api-usage-records.js +2 -0
  27. package/dist/server/middleware/rate-limit.js +7 -6
  28. package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
  29. package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
  30. package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
  31. package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
  32. package/dist/server/plugin.js +100 -22
  33. package/dist/server/quota-groups.js +108 -0
  34. package/dist/server/resource/ai-api-config.js +5 -3
  35. package/dist/server/resource/ai-api-usage-groups.js +168 -0
  36. package/dist/server/resource/ai-api-usage-monitor.js +3 -1
  37. package/dist/server/routes/agent-completions.js +2 -1
  38. package/dist/server/routes/chat-completions.js +121 -42
  39. package/dist/server/routes/completions.js +48 -29
  40. package/dist/server/routes/embeddings.js +2 -1
  41. package/dist/server/routes/models.js +2 -1
  42. package/dist/server/routes/router.js +3 -2
  43. package/dist/server/services/file-processor.js +426 -0
  44. package/dist/server/usage.js +37 -3
  45. package/dist/server/utils/direct-llm-context.js +163 -26
  46. package/dist/server/utils/openai-format.js +21 -2
  47. package/dist/server/utils/rate-limiter.js +1 -1
  48. package/dist/server/utils/request-cache.js +61 -0
  49. package/dist/server/utils/resolve-service.js +2 -1
  50. package/dist/server/utils/user-permissions.js +25 -39
  51. package/dist/server/validation.js +7 -0
  52. package/dist/swagger.js +48 -10
  53. package/package.json +1 -1
  54. package/src/client/__tests__/settings-registration.test.tsx +6 -29
  55. package/src/client/plugin.tsx +5 -16
  56. package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
  57. package/src/client-v2/locale.ts +3 -1
  58. package/src/client-v2/pages/GeneralPage.tsx +0 -5
  59. package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
  60. package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
  61. package/src/client-v2/pages/UsagePage.tsx +9 -0
  62. package/src/client-v2/plugin.tsx +4 -13
  63. package/src/constants.ts +0 -7
  64. package/src/locale/en-US.json +27 -8
  65. package/src/locale/vi-VN.json +27 -8
  66. package/src/locale/zh-CN.json +27 -8
  67. package/src/server/__tests__/billing-quota.test.ts +28 -9
  68. package/src/server/__tests__/direct-llm-context.test.ts +209 -10
  69. package/src/server/__tests__/file-processor.test.ts +225 -0
  70. package/src/server/__tests__/models.test.ts +1 -1
  71. package/src/server/__tests__/openai-format.test.ts +12 -2
  72. package/src/server/__tests__/permission-sync.test.ts +34 -35
  73. package/src/server/__tests__/request-body.test.ts +45 -2
  74. package/src/server/__tests__/usage-groups.test.ts +160 -0
  75. package/src/server/__tests__/usage-monitor.test.ts +2 -0
  76. package/src/server/__tests__/usage-route.test.ts +382 -5
  77. package/src/server/__tests__/usage.test.ts +57 -0
  78. package/src/server/__tests__/user-permissions.test.ts +214 -133
  79. package/src/server/__tests__/validation.test.ts +11 -0
  80. package/src/server/billing.ts +36 -39
  81. package/src/server/collections/ai-api-config.ts +9 -7
  82. package/src/server/collections/ai-api-group-members.ts +41 -0
  83. package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
  84. package/src/server/collections/ai-api-model-metadata.ts +7 -0
  85. package/src/server/collections/ai-api-role-permissions.ts +41 -41
  86. package/src/server/collections/ai-api-usage-groups.ts +53 -0
  87. package/src/server/collections/ai-api-usage-records.ts +2 -0
  88. package/src/server/index.ts +10 -10
  89. package/src/server/middleware/rate-limit.ts +68 -70
  90. package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
  91. package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
  92. package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
  93. package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
  94. package/src/server/plugin.ts +121 -30
  95. package/src/server/quota-groups.ts +117 -0
  96. package/src/server/resource/ai-api-config.ts +5 -3
  97. package/src/server/resource/ai-api-usage-groups.ts +171 -0
  98. package/src/server/resource/ai-api-usage-monitor.ts +3 -0
  99. package/src/server/routes/agent-completions.ts +2 -1
  100. package/src/server/routes/chat-completions.ts +173 -47
  101. package/src/server/routes/completions.ts +50 -27
  102. package/src/server/routes/embeddings.ts +2 -1
  103. package/src/server/routes/models.ts +4 -3
  104. package/src/server/routes/router.ts +4 -3
  105. package/src/server/services/__tests__/file-processor.test.ts +184 -0
  106. package/src/server/services/file-processor.ts +513 -0
  107. package/src/server/usage.ts +51 -1
  108. package/src/server/utils/direct-llm-context.ts +218 -31
  109. package/src/server/utils/openai-format.ts +25 -2
  110. package/src/server/utils/rate-limiter.ts +83 -83
  111. package/src/server/utils/request-cache.ts +59 -0
  112. package/src/server/utils/resolve-service.ts +83 -82
  113. package/src/server/utils/user-permissions.ts +49 -69
  114. package/src/server/validation.ts +7 -0
  115. package/src/swagger.ts +52 -11
  116. package/dist/client/123.e6fe04c856ce6417.js +0 -10
  117. package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
  118. package/dist/client/562.17a0a299d2e5152c.js +0 -10
  119. package/dist/client/757.a01403fb7a1bea01.js +0 -10
  120. package/dist/client/902.e74518750f1e4201.js +0 -10
  121. package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
  122. package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
  123. package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
  124. package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
  125. package/dist/client-v2/902.c7c00a565085438a.js +0 -10
  126. package/dist/server/resource/ai-api-user-permissions.js +0 -75
  127. package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
  128. package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
  129. package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
  130. package/src/server/resource/ai-api-user-permissions.ts +0 -76
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { Application } from '@nocobase/client';
11
11
  import { describe, expect, it } from 'vitest';
12
- import { AI_API_ACL_SNIPPET, AI_API_USER_PERMISSIONS_SNIPPET } from '../../constants';
12
+ import { AI_API_ACL_SNIPPET } from '../../constants';
13
13
  import { PluginAiApiClient } from '../plugin';
14
14
 
15
15
  describe('AI API v1 settings registration', () => {
@@ -29,41 +29,18 @@ describe('AI API v1 settings registration', () => {
29
29
  'ai-api.config',
30
30
  'ai-api.model-pricing',
31
31
  'ai-api.model-metadata',
32
- 'ai-api.user-permissions',
33
- 'ai-api.user-quotas',
32
+ 'ai-api.usage-groups',
34
33
  'ai-api.usage',
35
34
  ]);
36
- expect(app.pluginSettingsManager.getAclSnippet('ai-api.user-permissions')).toBe(AI_API_USER_PERMISSIONS_SNIPPET);
37
- expect(app.pluginSettingsManager.getRoutePath('ai-api.user-permissions')).toBe(
38
- '/admin/settings/ai-api/user-permissions',
39
- );
40
- expect(app.pluginSettingsManager.getList().map((item) => item.name)).not.toContain('ai-api-user-permissions');
35
+ for (const child of menu?.children ?? []) {
36
+ expect(app.pluginSettingsManager.getAclSnippet(child.name), child.name).toBe(AI_API_ACL_SNIPPET);
37
+ }
41
38
  });
42
39
 
43
- it('requires access to the legacy parent before exposing the user permissions tab', async () => {
40
+ it('hides the whole settings surface when the snippet is denied', async () => {
44
41
  const app = await loadPlugin();
45
42
  app.pluginSettingsManager.setAclSnippets([`!${AI_API_ACL_SNIPPET}`]);
46
43
 
47
44
  expect(app.pluginSettingsManager.getList()).toEqual([]);
48
45
  });
49
-
50
- it('hides only the user permissions tab when its own snippet is denied', async () => {
51
- const app = await loadPlugin();
52
- app.pluginSettingsManager.setAclSnippets([`!${AI_API_USER_PERMISSIONS_SNIPPET}`]);
53
-
54
- expect(app.pluginSettingsManager.get('ai-api')?.children?.map((item) => item.name)).toEqual([
55
- 'ai-api.config',
56
- 'ai-api.model-pricing',
57
- 'ai-api.model-metadata',
58
- 'ai-api.user-quotas',
59
- 'ai-api.usage',
60
- ]);
61
- });
62
-
63
- it('hides both settings surfaces when both snippets are denied', async () => {
64
- const app = await loadPlugin();
65
- app.pluginSettingsManager.setAclSnippets([`!${AI_API_ACL_SNIPPET}`, `!${AI_API_USER_PERMISSIONS_SNIPPET}`]);
66
-
67
- expect(app.pluginSettingsManager.getList()).toEqual([]);
68
- });
69
46
  });
@@ -9,14 +9,13 @@
9
9
 
10
10
  import { Plugin, lazy } from '@nocobase/client';
11
11
  import PluginACLClient from '@nocobase/plugin-acl/client';
12
- import { AI_API_ACL_SNIPPET, AI_API_USER_PERMISSIONS_SNIPPET } from '../constants';
12
+ import { AI_API_ACL_SNIPPET } from '../constants';
13
13
  import React from 'react';
14
14
 
15
15
  const AiApiConfigPage = React.lazy(() => import('../client-v2/pages/GeneralPage'));
16
16
  const AiApiModelPricingPage = React.lazy(() => import('../client-v2/pages/ModelPricingPage'));
17
17
  const AiApiModelMetadataPage = React.lazy(() => import('../client-v2/pages/ModelMetadataPage'));
18
- const AiApiUserPermissionsPage = React.lazy(() => import('../client-v2/pages/UserPermissionsPage'));
19
- const AiApiUserQuotasPage = React.lazy(() => import('../client-v2/pages/UserQuotasPage'));
18
+ const AiApiUsageGroupsPage = React.lazy(() => import('../client-v2/pages/UsageGroupsPage'));
20
19
  const AiApiUsagePage = React.lazy(() => import('../client-v2/pages/UsagePage'));
21
20
  const { AiApiRolePermissions } = lazy(() => import('./components/AiApiRolePermissions'), 'AiApiRolePermissions');
22
21
 
@@ -49,19 +48,9 @@ export class PluginAiApiClient extends Plugin {
49
48
  sort: 3,
50
49
  });
51
50
 
52
- // Keep the legacy settings layout aligned with client-v2. The v1 settings
53
- // manager evaluates the parent ACL first, so this tab additionally requires
54
- // access to the AI API settings parent even though it keeps its own snippet.
55
- this.app.pluginSettingsManager.add('ai-api.user-permissions', {
56
- title: this.t('User LLM permissions'),
57
- Component: AiApiUserPermissionsPage,
58
- aclSnippet: AI_API_USER_PERMISSIONS_SNIPPET,
59
- sort: 4,
60
- });
61
-
62
- this.app.pluginSettingsManager.add('ai-api.user-quotas', {
63
- title: this.t('User quotas'),
64
- Component: AiApiUserQuotasPage,
51
+ this.app.pluginSettingsManager.add('ai-api.usage-groups', {
52
+ title: this.t('Usage groups'),
53
+ Component: AiApiUsageGroupsPage,
65
54
  aclSnippet: AI_API_ACL_SNIPPET,
66
55
  sort: 5,
67
56
  });
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { createMockClient } from '@nocobase/client-v2';
11
- import { AI_API_ACL_SNIPPET, AI_API_USER_PERMISSIONS_SNIPPET } from '../../constants';
11
+ import { AI_API_ACL_SNIPPET } from '../../constants';
12
12
  import { PluginAiApiClient } from '../plugin';
13
13
 
14
14
  /**
@@ -18,10 +18,8 @@ import { PluginAiApiClient } from '../plugin';
18
18
  * omitting `aclSnippet` makes addPageTabItem default to `pm.ai-api.<key>`, a snippet the
19
19
  * server never registers, and omitting `sort` silently reorders the tabs alphabetically.
20
20
  *
21
- * The tabs deliberately span two snippets: everything sits under AI_API_ACL_SNIPPET except
22
- * user permissions, which is gated separately so granting it does not also grant gateway
23
- * configuration. Each snippet is evaluated independently, so denying one leaves the other's
24
- * tabs — and therefore the menu — reachable.
21
+ * Every tab sits under AI_API_ACL_SNIPPET model access is managed on the usage groups
22
+ * page, so there is no longer a separately-gated permissions tab.
25
23
  */
26
24
  describe('AI API v2 settings registration', () => {
27
25
  async function loadPlugin() {
@@ -36,41 +34,18 @@ describe('AI API v2 settings registration', () => {
36
34
 
37
35
  expect(menu?.aclSnippet).toBe(AI_API_ACL_SNIPPET);
38
36
  for (const child of menu?.children ?? []) {
39
- expect(app.pluginSettingsManager.getAclSnippet(child.name), child.name).toBe(
40
- child.name === 'ai-api.user-permissions' ? AI_API_USER_PERMISSIONS_SNIPPET : AI_API_ACL_SNIPPET,
41
- );
37
+ expect(app.pluginSettingsManager.getAclSnippet(child.name), child.name).toBe(AI_API_ACL_SNIPPET);
42
38
  }
43
39
  });
44
40
 
45
- it('leaves only the separately-gated tab when the main snippet is denied', async () => {
41
+ it('hides the menu and every tab when the snippet is denied', async () => {
46
42
  const app = await loadPlugin();
47
43
  app.pluginSettingsManager.setAclSnippets([`!${AI_API_ACL_SNIPPET}`]);
48
44
 
49
- // User permissions carries its own snippet, so it survives on its own merit and keeps the
50
- // menu reachable. That separation is the point: a role can be allowed to hand out model
51
- // access without also being allowed to reconfigure the gateway.
52
- expect(app.pluginSettingsManager.get('ai-api')?.children?.map((item) => item.name)).toEqual([
53
- 'ai-api.user-permissions',
54
- ]);
55
- });
56
-
57
- it('hides the menu and every tab when both snippets are denied', async () => {
58
- const app = await loadPlugin();
59
- app.pluginSettingsManager.setAclSnippets([`!${AI_API_ACL_SNIPPET}`, `!${AI_API_USER_PERMISSIONS_SNIPPET}`]);
60
-
61
45
  expect(app.pluginSettingsManager.get('ai-api')).toBeNull();
62
46
  expect(app.pluginSettingsManager.getList().map((item) => item.name)).not.toContain('ai-api');
63
47
  });
64
48
 
65
- it('hides only the user permissions tab when its own snippet is denied', async () => {
66
- const app = await loadPlugin();
67
- app.pluginSettingsManager.setAclSnippets([`!${AI_API_USER_PERMISSIONS_SNIPPET}`]);
68
-
69
- const children = app.pluginSettingsManager.get('ai-api')?.children?.map((item) => item.name) ?? [];
70
- expect(children).not.toContain('ai-api.user-permissions');
71
- expect(children).toContain('ai-api.user-quotas');
72
- });
73
-
74
49
  it('keeps registration order instead of sorting tabs by name', async () => {
75
50
  const app = await loadPlugin();
76
51
  app.pluginSettingsManager.setAclSnippets([]);
@@ -79,8 +54,7 @@ describe('AI API v2 settings registration', () => {
79
54
  'ai-api.index',
80
55
  'ai-api.model-pricing',
81
56
  'ai-api.model-metadata',
82
- 'ai-api.user-permissions',
83
- 'ai-api.user-quotas',
57
+ 'ai-api.usage-groups',
84
58
  'ai-api.usage',
85
59
  ]);
86
60
  });
@@ -7,13 +7,15 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import { useCallback } from 'react';
10
11
  import { tExpr as _tExpr, useFlowEngine } from '@nocobase/flow-engine';
11
12
  // @ts-ignore
12
13
  import pkg from './../../package.json';
13
14
 
14
15
  export function useT() {
15
16
  const engine = useFlowEngine();
16
- return (str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] });
17
+ // Stable identity so callers can safely list `t` in hook dependency arrays.
18
+ return useCallback((str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] }), [engine]);
17
19
  }
18
20
 
19
21
  export function tExpr(key: string) {
@@ -9,7 +9,6 @@ interface GeneralSettings {
9
9
  defaultAiEmployee?: string;
10
10
  defaultLlmService?: string;
11
11
  enabledLlmServices: string[];
12
- rateLimitPerMinute: number;
13
12
  maxRequestBodyMb: number;
14
13
  quotaEnabled: boolean;
15
14
  defaultReservationOutputTokens: number;
@@ -28,7 +27,6 @@ interface AiEmployee {
28
27
  const defaults: GeneralSettings = {
29
28
  mode: 'llm',
30
29
  enabledLlmServices: [],
31
- rateLimitPerMinute: 60,
32
30
  maxRequestBodyMb: 10,
33
31
  quotaEnabled: false,
34
32
  defaultReservationOutputTokens: 4096,
@@ -123,9 +121,6 @@ export default function GeneralPage() {
123
121
  />
124
122
  </Form.Item>
125
123
  ) : null}
126
- <Form.Item name="rateLimitPerMinute" label={t('Rate Limit')} rules={[{ required: true }]}>
127
- <InputNumber min={1} style={{ width: '100%' }} />
128
- </Form.Item>
129
124
  <Form.Item
130
125
  name="maxRequestBodyMb"
131
126
  label={t('Max request body size (MB)')}
@@ -28,6 +28,7 @@ interface ModelMetadata {
28
28
  ownedByOverride?: string | null;
29
29
  displayName?: string | null;
30
30
  description?: string | null;
31
+ systemPrompt?: string | null;
31
32
  enabled: boolean;
32
33
  }
33
34
 
@@ -136,6 +137,7 @@ export default function ModelMetadataPage() {
136
137
  ownedByOverride: values.ownedByOverride?.trim() || null,
137
138
  displayName: values.displayName?.trim() || null,
138
139
  description: values.description?.trim() || null,
140
+ systemPrompt: values.systemPrompt?.trim() || null,
139
141
  };
140
142
  setSaving(true);
141
143
  try {
@@ -171,6 +173,14 @@ export default function ModelMetadataPage() {
171
173
  { title: t('Max completion tokens'), dataIndex: 'maxCompletionTokens', key: 'maxCompletionTokens', width: 170 },
172
174
  { title: t('Owned by'), dataIndex: 'ownedByOverride', key: 'ownedByOverride', width: 140 },
173
175
  { title: t('Display name'), dataIndex: 'displayName', key: 'displayName', width: 160 },
176
+ {
177
+ title: t('Initial system prompt'),
178
+ dataIndex: 'systemPrompt',
179
+ key: 'systemPrompt',
180
+ width: 220,
181
+ ellipsis: true,
182
+ render: (value?: string | null) => value || '-',
183
+ },
174
184
  {
175
185
  title: t('Status'),
176
186
  dataIndex: 'enabled',
@@ -209,7 +219,7 @@ export default function ModelMetadataPage() {
209
219
  </Button>
210
220
  }
211
221
  >
212
- <Table rowKey="id" columns={columns} dataSource={rows} loading={loading} scroll={{ x: 1200 }} />
222
+ <Table rowKey="id" columns={columns} dataSource={rows} loading={loading} scroll={{ x: 1450 }} />
213
223
  <Modal
214
224
  title={editing ? t('Edit override') : t('Add override')}
215
225
  open={open}
@@ -270,6 +280,15 @@ export default function ModelMetadataPage() {
270
280
  <Form.Item name="description" label={t('Description')}>
271
281
  <Input.TextArea rows={3} />
272
282
  </Form.Item>
283
+ <Form.Item
284
+ name="systemPrompt"
285
+ label={t('Initial system prompt')}
286
+ tooltip={t(
287
+ 'Prepended as the first system message, before any system prompt sent by the client. If the client sends no system prompt, this becomes the system prompt sent to the provider.',
288
+ )}
289
+ >
290
+ <Input.TextArea rows={4} placeholder={t('Leave empty to not override')} />
291
+ </Form.Item>
273
292
  <Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
274
293
  <Switch />
275
294
  </Form.Item>