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
package/dist/swagger.js CHANGED
@@ -197,6 +197,143 @@ var swagger_default = {
197
197
  }
198
198
  }
199
199
  },
200
+ "/ai-llm/v1/responses": {
201
+ post: {
202
+ tags: ["ai-llm"],
203
+ summary: "Create a model response",
204
+ description: "OpenAI Responses API endpoint. Supports text/image/file inputs, function calling, streaming, reasoning output when exposed by the provider, and owner-scoped previous_response_id chains. Built-in web_search and file_search tools are not supported.",
205
+ security: [{ BearerAuth: [] }],
206
+ requestBody: {
207
+ required: true,
208
+ content: {
209
+ "application/json": {
210
+ schema: {
211
+ type: "object",
212
+ properties: {
213
+ model: { type: "string", example: "openai/gpt-4o" },
214
+ input: {
215
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "object" } }]
216
+ },
217
+ instructions: { type: "string" },
218
+ max_output_tokens: { type: "integer" },
219
+ temperature: { type: "number", minimum: 0, maximum: 2 },
220
+ top_p: { type: "number", minimum: 0, maximum: 1 },
221
+ tools: { type: "array", description: "Function tools only", items: { type: "object" } },
222
+ tool_choice: { oneOf: [{ type: "string" }, { type: "object" }] },
223
+ metadata: { type: "object" },
224
+ previous_response_id: { type: "string" },
225
+ store: { type: "boolean", default: true },
226
+ stream: { type: "boolean", default: false },
227
+ truncation: { type: "string", enum: ["auto", "disabled"], default: "disabled" },
228
+ service_tier: { type: "string", enum: ["auto", "default", "flex", "scale", "priority"] },
229
+ prompt_cache_key: { type: "string" },
230
+ prompt_cache_retention: { type: "string", enum: ["in_memory", "24h"], nullable: true },
231
+ reasoning: { type: "object" },
232
+ safety_identifier: { type: "string", maxLength: 64 },
233
+ parallel_tool_calls: { type: "boolean", default: true },
234
+ text: { type: "object" }
235
+ },
236
+ required: ["model", "input"]
237
+ }
238
+ }
239
+ }
240
+ },
241
+ responses: {
242
+ 200: {
243
+ description: "OpenAI Response object, or Responses API SSE events when stream=true",
244
+ content: {
245
+ "application/json": {
246
+ schema: {
247
+ type: "object",
248
+ properties: {
249
+ id: { type: "string", example: "resp_abc123" },
250
+ object: { type: "string", const: "response" },
251
+ created_at: { type: "integer" },
252
+ completed_at: { type: "integer", nullable: true },
253
+ model: { type: "string" },
254
+ status: {
255
+ type: "string",
256
+ enum: ["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]
257
+ },
258
+ output_text: { type: "string" },
259
+ output: { type: "array", items: { type: "object" } },
260
+ usage: {
261
+ type: "object",
262
+ properties: {
263
+ input_tokens: { type: "integer" },
264
+ input_tokens_details: {
265
+ type: "object",
266
+ properties: { cached_tokens: { type: "integer" } }
267
+ },
268
+ output_tokens: { type: "integer" },
269
+ output_tokens_details: {
270
+ type: "object",
271
+ properties: { reasoning_tokens: { type: "integer" } }
272
+ },
273
+ total_tokens: { type: "integer" }
274
+ }
275
+ },
276
+ metadata: { type: "object", nullable: true },
277
+ previous_response_id: { type: "string", nullable: true },
278
+ truncation: { type: "string", enum: ["auto", "disabled"] },
279
+ error: { type: "object", nullable: true }
280
+ }
281
+ }
282
+ },
283
+ "text/event-stream": { schema: { type: "string" } }
284
+ }
285
+ },
286
+ 400: { description: "Invalid request or unsupported tool" },
287
+ 404: { description: "Model or previous response not found" },
288
+ 429: { description: "Rate limit or quota exceeded" }
289
+ }
290
+ }
291
+ },
292
+ "/ai-llm/v1/responses/{id}": {
293
+ get: {
294
+ tags: ["ai-llm"],
295
+ summary: "Retrieve a stored model response",
296
+ description: "Returns a non-expired response created by the authenticated user. Responses created with store=false cannot be retrieved.",
297
+ security: [{ BearerAuth: [] }],
298
+ parameters: [
299
+ {
300
+ name: "id",
301
+ in: "path",
302
+ required: true,
303
+ schema: { type: "string", example: "resp_abc123" }
304
+ },
305
+ {
306
+ name: "stream",
307
+ in: "query",
308
+ schema: { type: "boolean", default: false },
309
+ description: "Only false is currently supported; stream retrieval is rejected."
310
+ }
311
+ ],
312
+ responses: {
313
+ 200: { description: "Stored OpenAI Response object" },
314
+ 400: { description: "Unsupported retrieve expansion or streaming parameter" },
315
+ 404: { description: "Response not found, expired, or owned by another user" }
316
+ }
317
+ },
318
+ delete: {
319
+ tags: ["ai-llm"],
320
+ summary: "Delete a stored model response",
321
+ description: "Deletes a stored response owned by the authenticated user.",
322
+ security: [{ BearerAuth: [] }],
323
+ parameters: [
324
+ {
325
+ name: "id",
326
+ in: "path",
327
+ required: true,
328
+ schema: { type: "string", example: "resp_abc123" }
329
+ }
330
+ ],
331
+ responses: {
332
+ 204: { description: "Response deleted" },
333
+ 404: { description: "Response not found, expired, or owned by another user" }
334
+ }
335
+ }
336
+ },
200
337
  "/ai-llm/v1/embeddings": {
201
338
  post: {
202
339
  tags: ["ai-llm"],
package/package.json CHANGED
@@ -1,33 +1,35 @@
1
- {
2
- "name": "plugin-ai-api",
3
- "version": "1.1.0",
4
- "main": "dist/server/index.js",
5
- "dependencies": {},
6
- "peerDependencies": {
7
- "@nocobase/client": "2.x",
8
- "@nocobase/server": "2.x",
9
- "@nocobase/database": "2.x",
10
- "@nocobase/plugin-acl": "2.x",
11
- "@nocobase/plugin-ai": "2.x",
12
- "@nocobase/plugin-api-keys": "2.x",
13
- "@nocobase/client-v2": "2.x",
14
- "@nocobase/flow-engine": "2.x"
15
- },
16
- "nocobase": {
17
- "supportedVersions": [
18
- "2.x"
19
- ],
20
- "editionLevel": 0
21
- },
22
- "files": [
23
- "dist",
24
- "src",
25
- "client.js",
26
- "client-v2.js",
27
- "server.js",
28
- "client.d.ts",
29
- "client-v2.d.ts",
30
- "server.d.ts"
31
- ],
32
- "license": "Apache-2.0"
1
+ {
2
+ "name": "plugin-ai-api",
3
+ "version": "1.1.2",
4
+ "main": "dist/server/index.js",
5
+ "dependencies": {},
6
+ "peerDependencies": {
7
+ "@nocobase/client": "2.x",
8
+ "@nocobase/server": "2.x",
9
+ "@nocobase/database": "2.x",
10
+ "@nocobase/plugin-acl": "2.x",
11
+ "@nocobase/plugin-ai": "2.x",
12
+ "@nocobase/plugin-api-keys": "2.x",
13
+ "@nocobase/client-v2": "2.x",
14
+ "@nocobase/flow-engine": "2.x"
15
+ },
16
+ "nocobase": {
17
+ "supportedVersions": [
18
+ "2.x"
19
+ ],
20
+ "editionLevel": 0
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "client.js",
26
+ "client-v2.js",
27
+ "server.js",
28
+ "client.d.ts",
29
+ "client-v2.d.ts",
30
+ "server.d.ts"
31
+ ],
32
+ "license": "Apache-2.0"
33
+ ,
34
+ "keywords": "AI"
33
35
  }
@@ -0,0 +1,43 @@
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 { readFileSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { describe, expect, it } from 'vitest';
13
+
14
+ const LOCALE_DIR = join(__dirname, '..', 'locale');
15
+ const FILES = ['en-US.json', 'zh-CN.json', 'vi-VN.json'] as const;
16
+
17
+ function loadLocale(file: string): Record<string, string> {
18
+ return JSON.parse(readFileSync(join(LOCALE_DIR, file), 'utf8')) as Record<string, string>;
19
+ }
20
+
21
+ describe('plugin-ai-api locale files', () => {
22
+ it('all locale files are valid JSON with no empty values', () => {
23
+ for (const file of FILES) {
24
+ const locale = loadLocale(file);
25
+ expect(Object.keys(locale).length, `${file} should not be empty`).toBeGreaterThan(0);
26
+ for (const [key, value] of Object.entries(locale)) {
27
+ expect(typeof value, `${file}[${JSON.stringify(key)}] must be a string`).toBe('string');
28
+ expect(value.trim().length, `${file}[${JSON.stringify(key)}] must not be blank`).toBeGreaterThan(0);
29
+ }
30
+ }
31
+ });
32
+
33
+ it('all locales expose the identical set of keys', () => {
34
+ const [base, ...rest] = FILES;
35
+ const baseKeys = Object.keys(loadLocale(base)).sort();
36
+ for (const file of rest) {
37
+ const keys = Object.keys(loadLocale(file)).sort();
38
+ const missing = baseKeys.filter((key) => !keys.includes(key));
39
+ const extra = keys.filter((key) => !baseKeys.includes(key));
40
+ expect({ file, missing, extra }).toEqual({ file, missing: [], extra: [] });
41
+ }
42
+ });
43
+ });
@@ -29,6 +29,7 @@ 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.model-routing',
32
33
  'ai-api.usage-groups',
33
34
  'ai-api.usage',
34
35
  ]);
@@ -1,10 +1,10 @@
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 { default } from './plugin';
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 { default } from './plugin';
@@ -1,12 +1,12 @@
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 { ModelConstructor } from '@nocobase/flow-engine';
11
-
12
- export default {} as Record<string, ModelConstructor>;
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 { ModelConstructor } from '@nocobase/flow-engine';
11
+
12
+ export default {} as Record<string, ModelConstructor>;
@@ -8,13 +8,14 @@
8
8
  */
9
9
 
10
10
  import { Plugin, lazy } from '@nocobase/client';
11
- import PluginACLClient from '@nocobase/plugin-acl/client';
11
+ import PluginACLClient from '@nocobase/plugin-acl';
12
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 AiApiModelRoutingPage = React.lazy(() => import('../client-v2/pages/ModelRoutingPage'));
18
19
  const AiApiUsageGroupsPage = React.lazy(() => import('../client-v2/pages/UsageGroupsPage'));
19
20
  const AiApiUsagePage = React.lazy(() => import('../client-v2/pages/UsagePage'));
20
21
  const { AiApiRolePermissions } = lazy(() => import('./components/AiApiRolePermissions'), 'AiApiRolePermissions');
@@ -48,6 +49,13 @@ export class PluginAiApiClient extends Plugin {
48
49
  sort: 3,
49
50
  });
50
51
 
52
+ this.app.pluginSettingsManager.add('ai-api.model-routing', {
53
+ title: this.t('Model routing'),
54
+ Component: AiApiModelRoutingPage,
55
+ aclSnippet: AI_API_ACL_SNIPPET,
56
+ sort: 4,
57
+ });
58
+
51
59
  this.app.pluginSettingsManager.add('ai-api.usage-groups', {
52
60
  title: this.t('Usage groups'),
53
61
  Component: AiApiUsageGroupsPage,
@@ -54,6 +54,7 @@ describe('AI API v2 settings registration', () => {
54
54
  'ai-api.index',
55
55
  'ai-api.model-pricing',
56
56
  'ai-api.model-metadata',
57
+ 'ai-api.model-routing',
57
58
  'ai-api.usage-groups',
58
59
  'ai-api.usage',
59
60
  ]);
@@ -23,6 +23,10 @@ interface ModelMetadata {
23
23
  id: string | number;
24
24
  llmService: string;
25
25
  model: string;
26
+ supportsVision?: boolean;
27
+ supportsToolCalling?: boolean;
28
+ reasoningTier?: string;
29
+ sortOrder?: number;
26
30
  contextWindow?: number | null;
27
31
  maxCompletionTokens?: number | null;
28
32
  ownedByOverride?: string | null;
@@ -138,6 +142,10 @@ export default function ModelMetadataPage() {
138
142
  displayName: values.displayName?.trim() || null,
139
143
  description: values.description?.trim() || null,
140
144
  systemPrompt: values.systemPrompt?.trim() || null,
145
+ supportsVision: !!values.supportsVision,
146
+ supportsToolCalling: values.supportsToolCalling !== false,
147
+ reasoningTier: values.reasoningTier || 'general',
148
+ sortOrder: values.sortOrder ?? 0,
141
149
  };
142
150
  setSaving(true);
143
151
  try {
@@ -289,6 +297,42 @@ export default function ModelMetadataPage() {
289
297
  >
290
298
  <Input.TextArea rows={4} placeholder={t('Leave empty to not override')} />
291
299
  </Form.Item>
300
+ <Form.Item
301
+ name="supportsVision"
302
+ label={t('Supports vision')}
303
+ valuePropName="checked"
304
+ tooltip={t('Used by virtual-model routing for image/file requests.')}
305
+ >
306
+ <Switch />
307
+ </Form.Item>
308
+ <Form.Item
309
+ name="supportsToolCalling"
310
+ label={t('Supports tool calling')}
311
+ valuePropName="checked"
312
+ tooltip={t('Used by virtual-model routing for requests with tools/tool_choice.')}
313
+ >
314
+ <Switch defaultChecked />
315
+ </Form.Item>
316
+ <Form.Item
317
+ name="reasoningTier"
318
+ label={t('Reasoning tier')}
319
+ tooltip={t('cheap | general | reasoning. Used by virtual-model routing buckets.')}
320
+ >
321
+ <Select
322
+ options={[
323
+ { value: 'cheap', label: t('Cheap') },
324
+ { value: 'general', label: t('General') },
325
+ { value: 'reasoning', label: t('Reasoning') },
326
+ ]}
327
+ />
328
+ </Form.Item>
329
+ <Form.Item
330
+ name="sortOrder"
331
+ label={t('Routing priority')}
332
+ tooltip={t('Ascending — lower is preferred when a bucket is derived from metadata.')}
333
+ >
334
+ <InputNumber precision={0} style={{ width: '100%' }} placeholder="0" />
335
+ </Form.Item>
292
336
  <Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
293
337
  <Switch />
294
338
  </Form.Item>
@@ -28,6 +28,7 @@ interface ModelPrice {
28
28
  model: string;
29
29
  currency: string;
30
30
  inputPricePerMillionTokens: string;
31
+ cacheInputPricePerMillionTokens: string;
31
32
  outputPricePerMillionTokens: string;
32
33
  fixedCostPerRequest: string;
33
34
  effectiveFrom: string;
@@ -95,6 +96,7 @@ export default function ModelPricingPage() {
95
96
  form.setFieldsValue({
96
97
  currency: 'USD',
97
98
  inputPricePerMillionTokens: '0',
99
+ cacheInputPricePerMillionTokens: '0',
98
100
  outputPricePerMillionTokens: '0',
99
101
  fixedCostPerRequest: '0',
100
102
  effectiveFrom: dayjs(),
@@ -182,6 +184,12 @@ export default function ModelPricingPage() {
182
184
  { title: t('LLM service'), dataIndex: 'llmService', key: 'llmService', width: 180 },
183
185
  { title: t('Model'), dataIndex: 'model', key: 'model', width: 180 },
184
186
  { title: t('Input price / 1M'), dataIndex: 'inputPricePerMillionTokens', key: 'inputPrice', width: 150 },
187
+ {
188
+ title: t('Cache input price / 1M'),
189
+ dataIndex: 'cacheInputPricePerMillionTokens',
190
+ key: 'cacheInputPrice',
191
+ width: 170,
192
+ },
185
193
  { title: t('Output price / 1M'), dataIndex: 'outputPricePerMillionTokens', key: 'outputPrice', width: 150 },
186
194
  { title: t('Fixed request cost'), dataIndex: 'fixedCostPerRequest', key: 'fixedCost', width: 150 },
187
195
  { title: t('Currency'), dataIndex: 'currency', key: 'currency', width: 90 },
@@ -260,6 +268,13 @@ export default function ModelPricingPage() {
260
268
  <Form.Item name="inputPricePerMillionTokens" label={t('Input price / 1M')} rules={[{ required: true }]}>
261
269
  <InputNumber min={0} stringMode style={{ width: '100%' }} />
262
270
  </Form.Item>
271
+ <Form.Item
272
+ name="cacheInputPricePerMillionTokens"
273
+ label={t('Cache input price / 1M')}
274
+ rules={[{ required: true }]}
275
+ >
276
+ <InputNumber min={0} stringMode style={{ width: '100%' }} />
277
+ </Form.Item>
263
278
  <Form.Item name="outputPricePerMillionTokens" label={t('Output price / 1M')} rules={[{ required: true }]}>
264
279
  <InputNumber min={0} stringMode style={{ width: '100%' }} />
265
280
  </Form.Item>