feat(provider): add pipeline reasoning controls (#2373)

* feat(provider): add pipeline reasoning controls

* fix(provider): preserve local agent model compatibility

* refactor(web): use shadcn reasoning slider

* fix(runtime): stabilize reasoning chat delivery

* fix(provider): route reasoning controls by model family

* fix(provider): handle hosted Kimi reasoning protocols

* fix(provider): map qwen reasoning levels to budgets

* fix(provider): preserve think tags in streamed reasoning

* fix(provider): preserve reasoning tool metadata

* style(provider): satisfy ruff checks after merge

* fix(persistence): preserve reasoning migration compatibility
This commit is contained in:
Dongchuan Fu
2026-08-09 17:38:01 +08:00
committed by GitHub
parent 22c389edc1
commit e37987215e
39 changed files with 3499 additions and 87 deletions
+1
View File
@@ -48,6 +48,7 @@
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.4.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
+207
View File
@@ -72,6 +72,9 @@ dependencies:
'@radix-ui/react-separator':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-slider':
specifier: ^1.4.7
version: 1.4.7(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.4(@types/react@19.2.10)(react@19.2.1)
@@ -555,10 +558,18 @@ packages:
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
dev: false
/@radix-ui/number@1.1.3:
resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==}
dev: false
/@radix-ui/primitive@1.1.3:
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
dev: false
/@radix-ui/primitive@1.1.7:
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
dev: false
/@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
peerDependencies:
@@ -682,6 +693,29 @@ packages:
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-context': 1.2.2(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-slot': 1.3.3(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
'@types/react-dom': 19.2.3(@types/react@19.2.10)
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -718,6 +752,19 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==}
peerDependencies:
@@ -769,6 +816,19 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-context@1.2.2(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
peerDependencies:
@@ -815,6 +875,19 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-direction@1.1.4(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
peerDependencies:
@@ -1104,6 +1177,26 @@ packages:
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-slot': 1.3.3(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
'@types/react-dom': 19.2.3(@types/react@19.2.10)
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
@@ -1281,6 +1374,36 @@ packages:
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-context': 1.2.2(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-direction': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-use-size': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
'@types/react-dom': 19.2.3(@types/react@19.2.10)
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
dev: false
/@radix-ui/react-slot@1.2.3(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
@@ -1309,6 +1432,20 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-slot@1.3.3(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
peerDependencies:
@@ -1469,6 +1606,22 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.10)(react@19.2.1)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
@@ -1483,6 +1636,20 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
@@ -1524,6 +1691,19 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-use-previous@1.1.1(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
@@ -1537,6 +1717,19 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-use-previous@1.1.4(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-use-rect@1.1.1(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
@@ -1565,6 +1758,20 @@ packages:
react: 19.2.1
dev: false
/@radix-ui/react-use-size@1.1.4(@types/react@19.2.10)(react@19.2.1):
resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.10)(react@19.2.1)
'@types/react': 19.2.10
react: 19.2.1
dev: false
/@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3)(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
peerDependencies:
@@ -144,6 +144,7 @@ function getValueSchema(spec: DynamicFormValueSpec) {
return z.object({
primary: z.string(),
fallbacks: z.array(z.string()),
reasoning: z.record(z.string()),
});
case DynamicFormItemType.PROMPT_EDITOR:
return z.array(
@@ -488,12 +489,24 @@ export default function DynamicFormComponent({
(v): v is string => typeof v === 'string',
)
: [],
reasoning:
obj.reasoning != null &&
typeof obj.reasoning === 'object' &&
!Array.isArray(obj.reasoning)
? Object.fromEntries(
Object.entries(obj.reasoning).filter(
(entry): entry is [string, string] =>
typeof entry[1] === 'string',
),
)
: {},
};
}
// Legacy string format or any other unexpected type
return {
primary: typeof value === 'string' ? value : '',
fallbacks: [],
reasoning: {},
};
}
if (item.type === 'prompt-editor') {
@@ -25,6 +25,7 @@ import {
EmbeddingModel,
RerankModel,
PluginTool,
ReasoningLevel,
} from '@/app/infra/entities/api';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
@@ -66,6 +67,9 @@ import SettingsDialog, {
} from '@/app/home/components/settings-dialog/SettingsDialog';
import ToolResourceSelectors from '@/app/home/components/dynamic-form/ToolResourceSelectors';
import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-dialog/types';
import ReasoningLevelPicker, {
REASONING_LEVELS,
} from '@/app/home/components/reasoning/ReasoningLevelPicker';
function hasUsableUuid<T extends { uuid?: string | null }>(
item: T,
@@ -874,7 +878,11 @@ export default function DynamicFormItemComponent({
];
const rawModelValue = field.value;
const modelValue: { primary: string; fallbacks: string[] } =
const modelValue: {
primary: string;
fallbacks: string[];
reasoning: Record<string, ReasoningLevel>;
} =
rawModelValue != null &&
typeof rawModelValue === 'object' &&
!Array.isArray(rawModelValue)
@@ -893,10 +901,29 @@ export default function DynamicFormItemComponent({
.fallbacks as unknown[]
).filter((v): v is string => typeof v === 'string')
: [],
reasoning:
(rawModelValue as Record<string, unknown>).reasoning != null &&
typeof (rawModelValue as Record<string, unknown>).reasoning ===
'object' &&
!Array.isArray(
(rawModelValue as Record<string, unknown>).reasoning,
)
? (Object.fromEntries(
Object.entries(
(rawModelValue as Record<string, unknown>)
.reasoning as Record<string, unknown>,
).filter(
(entry): entry is [string, ReasoningLevel] =>
typeof entry[1] === 'string' &&
REASONING_LEVELS.includes(entry[1] as ReasoningLevel),
),
) as Record<string, ReasoningLevel>)
: {},
}
: {
primary: typeof rawModelValue === 'string' ? rawModelValue : '',
fallbacks: [],
reasoning: {},
};
const renderModelSelect = (
@@ -1043,20 +1070,79 @@ export default function DynamicFormItemComponent({
field.onChange({ ...modelValue, ...patch });
};
const updateModelReasoning = (
modelUuid: string,
level: ReasoningLevel,
) => {
if (!modelUuid) return;
const updated = { ...modelValue.reasoning };
if (level === 'provider_default') {
delete updated[modelUuid];
} else {
updated[modelUuid] = level;
}
updateValue({ reasoning: updated });
};
const replaceModel = (
currentUuid: string,
nextUuid: string,
patch: Partial<typeof modelValue>,
) => {
const nextValue = { ...modelValue, ...patch };
const updatedReasoning = { ...modelValue.reasoning };
const currentModelStillSelected =
nextValue.primary === currentUuid ||
nextValue.fallbacks.includes(currentUuid);
if (
currentUuid &&
currentUuid !== nextUuid &&
!currentModelStillSelected
) {
delete updatedReasoning[currentUuid];
}
updateValue({ ...nextValue, reasoning: updatedReasoning });
};
const renderReasoningPicker = (modelUuid: string) => {
if (!modelUuid) return null;
const model = llmModels.find(
(candidate) => candidate.uuid === modelUuid,
);
const currentLevel =
modelValue.reasoning[modelUuid] || 'provider_default';
const availableLevels = model?.reasoning_capabilities?.levels || [
'provider_default',
];
const levels = REASONING_LEVELS.filter(
(level) => availableLevels.includes(level) || level === currentLevel,
);
return (
<ReasoningLevelPicker
value={currentLevel}
levels={levels}
onChange={(level) => updateModelReasoning(modelUuid, level)}
/>
);
};
const addFallbackModel = () => {
updateValue({ fallbacks: [...modelValue.fallbacks, ''] });
};
const updateFallbackModel = (index: number, value: string) => {
const updated = [...modelValue.fallbacks];
const currentUuid = updated[index];
updated[index] = value;
updateValue({ fallbacks: updated });
replaceModel(currentUuid, value, { fallbacks: updated });
};
const removeFallbackModel = (index: number) => {
const updated = [...modelValue.fallbacks];
const removedUuid = updated[index];
updated.splice(index, 1);
updateValue({ fallbacks: updated });
replaceModel(removedUuid, '', { fallbacks: updated });
};
const moveFallbackModel = (index: number, direction: 'up' | 'down') => {
@@ -1081,10 +1167,12 @@ export default function DynamicFormItemComponent({
<div className="min-w-0 flex-1">
{renderModelSelect(
modelValue.primary,
(val) => updateValue({ primary: val }),
(val) =>
replaceModel(modelValue.primary, val, { primary: val }),
t('models.selectModel'),
)}
</div>
{renderReasoningPicker(modelValue.primary)}
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -1118,15 +1206,18 @@ export default function DynamicFormItemComponent({
</p>
{modelValue.fallbacks.map((fbUuid: string, index: number) => (
<div key={index} className="flex min-w-0 items-center gap-2">
<span className="text-xs text-muted-foreground w-4 shrink-0">
<span className="w-4 shrink-0 text-xs text-muted-foreground">
{index + 1}.
</span>
<div className="min-w-0 flex-1">
{renderModelSelect(
fbUuid,
(val) => updateFallbackModel(index, val),
t('models.selectModel'),
)}
<div className="flex min-w-0 flex-1 items-center gap-1.5">
<div className="min-w-0 flex-1">
{renderModelSelect(
fbUuid,
(val) => updateFallbackModel(index, val),
t('models.selectModel'),
)}
</div>
{renderReasoningPicker(fbUuid)}
</div>
<div className="flex gap-1 shrink-0">
<Button
@@ -5,6 +5,56 @@ export type DynamicFormSaveValueSpec = Pick<
'default' | 'name' | 'type'
>;
const reasoningLevels = new Set([
'disabled',
'enabled',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
]);
function normalizeModelFallbackValue(value: unknown): {
primary: string;
fallbacks: string[];
reasoning: Record<string, string>;
} {
const raw =
value != null && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const primary =
typeof raw.primary === 'string'
? raw.primary
: typeof value === 'string'
? value
: '';
const fallbacks = Array.isArray(raw.fallbacks)
? raw.fallbacks.filter(
(fallback): fallback is string => typeof fallback === 'string',
)
: [];
const selectedModels = new Set([primary, ...fallbacks].filter(Boolean));
const rawReasoning =
raw.reasoning != null &&
typeof raw.reasoning === 'object' &&
!Array.isArray(raw.reasoning)
? (raw.reasoning as Record<string, unknown>)
: {};
const reasoning = Object.fromEntries(
Object.entries(rawReasoning).filter(
([modelUuid, level]) =>
selectedModels.has(modelUuid) &&
typeof level === 'string' &&
reasoningLevels.has(level),
),
) as Record<string, string>;
return { primary, fallbacks, reasoning };
}
/**
* Build the value snapshot emitted to parent forms for persistence.
* Only single-line string fields trim surrounding whitespace; multiline text
@@ -16,10 +66,14 @@ export function normalizeDynamicFormValuesForSave(
): Record<string, unknown> {
return specs.reduce<Record<string, unknown>>((values, spec) => {
const value = formValues[spec.name] ?? spec.default;
values[spec.name] =
spec.type === 'string' && typeof value === 'string'
? value.trim()
: value;
if (spec.type === 'model-fallback-selector') {
values[spec.name] = normalizeModelFallbackValue(value);
} else {
values[spec.name] =
spec.type === 'string' && typeof value === 'string'
? value.trim()
: value;
}
return values;
}, {});
}
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { Plus, Boxes } from 'lucide-react';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { ModelProvider } from '@/app/infra/entities/api';
import { ModelProvider, ReasoningConfig } from '@/app/infra/entities/api';
import {
Dialog,
DialogContent,
@@ -15,6 +15,7 @@ import ProviderForm from './component/provider-form/ProviderForm';
import { ProviderCard } from './components';
import {
ExtraArg,
DEFAULT_REASONING_CONFIG,
ModelType,
ScanModelsResult,
SelectedScannedModel,
@@ -285,6 +286,7 @@ export default function ModelsPanel({
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) {
if (!name.trim()) {
@@ -300,6 +302,7 @@ export default function ModelsPanel({
name,
provider_uuid: providerUuid,
abilities,
reasoning_config: reasoningConfig,
context_length: parseContextLength(
contextLength,
t('models.contextLengthInvalid'),
@@ -361,6 +364,7 @@ export default function ModelsPanel({
name: item.model.name,
provider_uuid: providerUuid,
abilities: item.abilities,
reasoning_config: DEFAULT_REASONING_CONFIG,
context_length: item.model.context_length ?? null,
extra_args: {},
} as never);
@@ -398,6 +402,7 @@ export default function ModelsPanel({
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) {
if (!name.trim()) {
@@ -413,6 +418,7 @@ export default function ModelsPanel({
name,
provider_uuid: providerUuid,
abilities,
reasoning_config: reasoningConfig,
context_length: parseContextLength(
contextLength,
t('models.contextLengthInvalid'),
@@ -469,6 +475,7 @@ export default function ModelsPanel({
modelType: ModelType,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) {
setIsTesting(true);
setTestResult(null);
@@ -491,6 +498,7 @@ export default function ModelsPanel({
provider_uuid: '',
provider: providerData,
abilities,
reasoning_config: reasoningConfig,
extra_args: extraArgsObj,
} as never);
} else if (modelType === 'embedding') {
@@ -554,13 +562,21 @@ export default function ModelsPanel({
onSpaceLogin={handleSpaceLogin}
onOpenAddModel={() => setAddModelPopoverOpen(provider.uuid)}
onCloseAddModel={() => setAddModelPopoverOpen(null)}
onAddModel={(modelType, name, abilities, extraArgs, contextLength) =>
onAddModel={(
modelType,
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
) =>
handleAddModel(
provider.uuid,
modelType,
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
)
}
@@ -576,6 +592,7 @@ export default function ModelsPanel({
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
) =>
handleUpdateModel(
@@ -585,6 +602,7 @@ export default function ModelsPanel({
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
)
}
@@ -593,8 +611,15 @@ export default function ModelsPanel({
onDeleteModel={(modelId, modelType) =>
handleDeleteModel(provider.uuid, modelId, modelType)
}
onTestModel={(name, modelType, abilities, extraArgs) =>
handleTestModel(provider.uuid, name, modelType, abilities, extraArgs)
onTestModel={(name, modelType, abilities, extraArgs, reasoningConfig) =>
handleTestModel(
provider.uuid,
name,
modelType,
abilities,
extraArgs,
reasoningConfig,
)
}
isSubmitting={isSubmitting}
isTesting={isTesting}
@@ -6,6 +6,7 @@ import {
ArrowUpDown,
Eye,
Wrench,
BrainCircuit,
Check,
RefreshCw,
} from 'lucide-react';
@@ -20,8 +21,12 @@ import {
} from '@/components/ui/popover';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import { ScannedProviderModel } from '@/app/infra/entities/api';
import {
ReasoningConfig,
ScannedProviderModel,
} from '@/app/infra/entities/api';
import {
DEFAULT_REASONING_CONFIG,
ExtraArg,
ModelType,
ScanModelsResult,
@@ -42,6 +47,7 @@ interface AddModelPopoverProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
@@ -54,6 +60,7 @@ interface AddModelPopoverProps {
modelType: ModelType,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) => Promise<void>;
isSubmitting: boolean;
isTesting: boolean;
@@ -143,11 +150,24 @@ export default function AddModelPopover({
tab === 'llm' && contextLength.trim()
? Number(contextLength.trim())
: null;
await onAddModel(tab, name, abilities, extraArgs, parsedContextLength);
await onAddModel(
tab,
name,
abilities,
extraArgs,
DEFAULT_REASONING_CONFIG,
parsedContextLength,
);
};
const handleTest = async () => {
await onTestModel(name, tab, tab === 'llm' ? abilities : [], extraArgs);
await onTestModel(
name,
tab,
tab === 'llm' ? abilities : [],
extraArgs,
DEFAULT_REASONING_CONFIG,
);
};
const handleScan = async () => {
@@ -322,7 +342,7 @@ export default function AddModelPopover({
{tab === 'llm' && (
<div className="space-y-2">
<Label>{t('models.abilities')}</Label>
<div className="flex gap-4">
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="add-vision"
@@ -349,6 +369,19 @@ export default function AddModelPopover({
{t('models.functionCallAbility')}
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="add-reasoning"
checked={abilities.includes('reasoning')}
onCheckedChange={(checked) =>
toggleAbility('reasoning', checked as boolean)
}
/>
<Label htmlFor="add-reasoning" className="text-sm">
<BrainCircuit className="h-3 w-3 inline mr-1" />
{t('models.reasoningAbility')}
</Label>
</div>
</div>
</div>
)}
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Trash2, Eye, Wrench, Check } from 'lucide-react';
import { Trash2, Eye, Wrench, Check, BrainCircuit } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -11,8 +11,17 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { useTranslation } from 'react-i18next';
import { LLMModel, EmbeddingModel } from '@/app/infra/entities/api';
import { ExtraArg, ModelType, TestResult } from '../types';
import {
LLMModel,
EmbeddingModel,
ReasoningConfig,
} from '@/app/infra/entities/api';
import {
DEFAULT_REASONING_CONFIG,
ExtraArg,
ModelType,
TestResult,
} from '../types';
import ExtraArgsEditor from './ExtraArgsEditor';
import { userInfo } from '@/app/infra/http';
@@ -32,12 +41,14 @@ interface ModelItemProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onTestModel: (
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) => Promise<void>;
isSubmitting: boolean;
isTesting: boolean;
@@ -103,7 +114,6 @@ export default function ModelItem({
const [editExtraArgs, setEditExtraArgs] = useState<ExtraArg[]>(
convertExtraArgsToArray(model.extra_args),
);
const isEditOpen = editModelPopoverOpen === model.uuid;
const isDeleteOpen = deleteConfirmOpen === model.uuid;
@@ -133,12 +143,20 @@ export default function ModelItem({
editName,
editAbilities,
editExtraArgs,
modelType === 'llm'
? (model as LLMModel).reasoning_config || DEFAULT_REASONING_CONFIG
: DEFAULT_REASONING_CONFIG,
parsedContextLength,
);
};
const handleTest = async () => {
await onTestModel(editName, editAbilities, editExtraArgs);
await onTestModel(
editName,
editAbilities,
editExtraArgs,
DEFAULT_REASONING_CONFIG,
);
};
const toggleAbility = (ability: string, checked: boolean) => {
@@ -149,6 +167,12 @@ export default function ModelItem({
}
};
const supportsReasoning =
modelType === 'llm' &&
((model as LLMModel).reasoning_capabilities?.supported === true ||
(model as LLMModel).abilities?.includes('reasoning'));
const canSaveModel = !isLangBotModels;
// Check if popover should be disabled (space models when not logged in)
const isPopoverDisabled =
!canManage || (isLangBotModels && userInfo?.account_type !== 'space');
@@ -194,6 +218,12 @@ export default function ModelItem({
<Wrench className="h-3 w-3" />
</Badge>
)}
{supportsReasoning && (
<Badge variant="outline" className="text-xs gap-1">
<BrainCircuit className="h-3 w-3" />
{t('models.reasoningAbility')}
</Badge>
)}
</div>
{canManage && !isLangBotModels && (
<Popover
@@ -270,7 +300,7 @@ export default function ModelItem({
{modelType === 'llm' && (
<div className="space-y-2">
<Label>{t('models.abilities')}</Label>
<div className="flex gap-4">
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Checkbox
id={`edit-vision-${model.uuid}`}
@@ -305,6 +335,23 @@ export default function ModelItem({
{t('models.functionCallAbility')}
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id={`edit-reasoning-${model.uuid}`}
checked={editAbilities.includes('reasoning')}
disabled={isLangBotModels}
onCheckedChange={(checked) =>
toggleAbility('reasoning', checked as boolean)
}
/>
<Label
htmlFor={`edit-reasoning-${model.uuid}`}
className="text-sm"
>
<BrainCircuit className="h-3 w-3 inline mr-1" />
{t('models.reasoningAbility')}
</Label>
</div>
</div>
</div>
)}
@@ -336,7 +383,7 @@ export default function ModelItem({
/>
<div className="flex gap-2">
{!isLangBotModels && (
{canSaveModel && (
<Button
className="flex-1"
size="sm"
@@ -347,7 +394,7 @@ export default function ModelItem({
</Button>
)}
<Button
className={isLangBotModels ? 'w-full' : 'flex-1'}
className={canSaveModel ? 'flex-1' : 'w-full'}
size="sm"
variant="outline"
onClick={handleTest}
@@ -9,7 +9,7 @@ import {
Radar,
} from 'lucide-react';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { ModelProvider } from '@/app/infra/entities/api';
import { ModelProvider, ReasoningConfig } from '@/app/infra/entities/api';
import { Button } from '@/components/ui/button';
import {
Collapsible,
@@ -63,6 +63,7 @@ interface ProviderCardProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
@@ -78,6 +79,7 @@ interface ProviderCardProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onOpenDeleteConfirm: (modelId: string) => void;
@@ -88,6 +90,7 @@ interface ProviderCardProps {
modelType: ModelType,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) => Promise<void>;
isSubmitting: boolean;
isTesting: boolean;
@@ -432,6 +435,7 @@ export default function ProviderCard({
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
) =>
onUpdateModel(
@@ -440,11 +444,23 @@ export default function ProviderCard({
name,
abilities,
extraArgs,
reasoningConfig,
contextLength,
)
}
onTestModel={(name, abilities, extraArgs) =>
onTestModel(name, 'llm', abilities, extraArgs)
onTestModel={(
name,
abilities,
extraArgs,
reasoningConfig,
) =>
onTestModel(
name,
'llm',
abilities,
extraArgs,
reasoningConfig,
)
}
isSubmitting={isSubmitting}
isTesting={isTesting}
@@ -466,17 +482,34 @@ export default function ProviderCard({
onOpenDeleteConfirm={onOpenDeleteConfirm}
onCloseDeleteConfirm={onCloseDeleteConfirm}
onDeleteModel={() => onDeleteModel(model.uuid, 'embedding')}
onUpdateModel={(name, abilities, extraArgs) =>
onUpdateModel={(
name,
abilities,
extraArgs,
reasoningConfig,
) =>
onUpdateModel(
model.uuid,
'embedding',
name,
abilities,
extraArgs,
reasoningConfig,
)
}
onTestModel={(name, abilities, extraArgs) =>
onTestModel(name, 'embedding', abilities, extraArgs)
onTestModel={(
name,
abilities,
extraArgs,
reasoningConfig,
) =>
onTestModel(
name,
'embedding',
abilities,
extraArgs,
reasoningConfig,
)
}
isSubmitting={isSubmitting}
isTesting={isTesting}
@@ -498,17 +531,34 @@ export default function ProviderCard({
onOpenDeleteConfirm={onOpenDeleteConfirm}
onCloseDeleteConfirm={onCloseDeleteConfirm}
onDeleteModel={() => onDeleteModel(model.uuid, 'rerank')}
onUpdateModel={(name, abilities, extraArgs) =>
onUpdateModel={(
name,
abilities,
extraArgs,
reasoningConfig,
) =>
onUpdateModel(
model.uuid,
'rerank',
name,
abilities,
extraArgs,
reasoningConfig,
)
}
onTestModel={(name, abilities, extraArgs) =>
onTestModel(name, 'rerank', abilities, extraArgs)
onTestModel={(
name,
abilities,
extraArgs,
reasoningConfig,
) =>
onTestModel(
name,
'rerank',
abilities,
extraArgs,
reasoningConfig,
)
}
isSubmitting={isSubmitting}
isTesting={isTesting}
@@ -5,6 +5,7 @@ import {
ModelProvider,
ProviderScanDebugInfo,
ScannedProviderModel,
ReasoningConfig,
} from '@/app/infra/entities/api';
export type ExtraArg = {
@@ -16,6 +17,10 @@ export type ExtraArg = {
export type ModelType = 'llm' | 'embedding' | 'rerank';
export const DEFAULT_REASONING_CONFIG: ReasoningConfig = {
level: 'provider_default',
};
export interface ProviderModels {
llm: LLMModel[];
embedding: EmbeddingModel[];
@@ -53,12 +58,14 @@ export interface ModelItemProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onTest: (
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) => Promise<void>;
isSubmitting: boolean;
isTesting: boolean;
@@ -90,6 +97,7 @@ export interface ProviderCardProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
@@ -105,6 +113,7 @@ export interface ProviderCardProps {
name: string,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
contextLength?: number | null,
) => Promise<void>;
onOpenDeleteConfirm: (modelId: string) => void;
@@ -115,6 +124,7 @@ export interface ProviderCardProps {
modelType: ModelType,
abilities: string[],
extraArgs: ExtraArg[],
reasoningConfig: ReasoningConfig,
) => Promise<void>;
isSubmitting: boolean;
isTesting: boolean;
@@ -0,0 +1,97 @@
import { BrainCircuit, ChevronDown, ChevronRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ReasoningLevel } from '@/app/infra/entities/api';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Slider } from '@/components/ui/slider';
export const REASONING_LEVELS: ReasoningLevel[] = [
'provider_default',
'disabled',
'enabled',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
];
export const REASONING_LEVEL_LABEL_KEYS: Record<ReasoningLevel, string> = {
provider_default: 'models.reasoningLevels.providerDefault',
disabled: 'models.reasoningLevels.disabled',
enabled: 'models.reasoningLevels.enabled',
minimal: 'models.reasoningLevels.minimal',
low: 'models.reasoningLevels.low',
medium: 'models.reasoningLevels.medium',
high: 'models.reasoningLevels.high',
xhigh: 'models.reasoningLevels.xhigh',
max: 'models.reasoningLevels.max',
};
interface ReasoningLevelPickerProps {
value: ReasoningLevel;
levels: ReasoningLevel[];
disabled?: boolean;
onChange: (value: ReasoningLevel) => void;
}
export default function ReasoningLevelPicker({
value,
levels,
disabled = false,
onChange,
}: ReasoningLevelPickerProps) {
const { t } = useTranslation();
const safeLevels: ReasoningLevel[] =
levels.length > 0 ? levels : ['provider_default'];
const safeValue: ReasoningLevel = safeLevels.includes(value)
? value
: safeLevels[0];
const currentLabel = t(REASONING_LEVEL_LABEL_KEYS[safeValue]);
const isExplicit = safeValue !== 'provider_default';
const currentIndex = Math.max(0, safeLevels.indexOf(safeValue));
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
disabled={disabled || safeLevels.length <= 1}
aria-label={`${t('models.reasoningLevel')}: ${currentLabel}`}
className="h-9 w-9 shrink-0 gap-1.5 px-2.5 text-xs font-normal sm:w-auto sm:max-w-36"
>
<BrainCircuit
className={`size-4 shrink-0 ${isExplicit ? 'text-primary' : 'text-muted-foreground'}`}
/>
<span className="hidden min-w-0 truncate sm:block">
{currentLabel}
</span>
<ChevronDown className="hidden size-3.5 shrink-0 text-muted-foreground sm:block" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[272px] p-4">
<div className="flex h-5 items-center gap-0.5 text-sm text-muted-foreground">
<span>{currentLabel}</span>
<ChevronRight className="size-3.5" />
</div>
<Slider
className="mt-5"
min={0}
max={Math.max(0, safeLevels.length - 1)}
step={1}
value={[currentIndex]}
aria-label={t('models.reasoningLevel')}
aria-valuetext={currentLabel}
onValueChange={([index]) => onChange(safeLevels[index])}
/>
</PopoverContent>
</Popover>
);
}
+24
View File
@@ -99,9 +99,33 @@ export interface LLMModel {
provider?: ModelProvider;
abilities?: string[];
context_length?: number | null;
reasoning_config?: ReasoningConfig;
reasoning_capabilities?: ReasoningCapabilities;
extra_args?: object;
}
export type ReasoningLevel =
| 'provider_default'
| 'disabled'
| 'enabled'
| 'minimal'
| 'low'
| 'medium'
| 'high'
| 'xhigh'
| 'max';
export interface ReasoningConfig {
level: ReasoningLevel;
}
export interface ReasoningCapabilities {
supported: boolean;
levels: ReasoningLevel[];
legacy_levels?: ReasoningLevel[];
source: 'litellm' | 'provider' | 'manual' | 'unknown';
}
export interface ApiRespProviderEmbeddingModels {
models: EmbeddingModel[];
}
+36
View File
@@ -0,0 +1,36 @@
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@/lib/utils';
const Slider = React.forwardRef<
React.ComponentRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
data-slot="slider"
className={cn(
'relative flex w-full touch-none select-none items-center data-[disabled]:opacity-50',
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"
>
<SliderPrimitive.Range
data-slot="slider-range"
className="absolute h-full bg-primary"
/>
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
data-slot="slider-thumb"
className="block size-4 shrink-0 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
/>
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
+13
View File
@@ -214,6 +214,19 @@ const enUS = {
selectModelAbilities: 'Select model abilities',
visionAbility: 'Vision Ability',
functionCallAbility: 'Function Call',
reasoningAbility: 'Reasoning',
reasoningLevel: 'Reasoning level',
reasoningLevels: {
providerDefault: 'Provider default',
disabled: 'Off',
enabled: 'On',
minimal: 'Minimal',
low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Maximum',
},
contextLength: 'Context Window',
contextLengthPlaceholder: 'Unknown',
contextLengthInvalid: 'Context window must be a positive integer',
+13
View File
@@ -217,6 +217,19 @@ const jaJP = {
selectModelAbilities: 'モデル機能を選択',
visionAbility: '視覚機能',
functionCallAbility: '関数呼び出し',
reasoningAbility: '推論',
reasoningLevel: '推論レベル',
reasoningLevels: {
providerDefault: 'Provider デフォルト',
disabled: 'オフ',
enabled: 'オン',
minimal: '最小',
low: '低',
medium: '中',
high: '高',
xhigh: '最高',
max: '最大',
},
contextLength: 'コンテキストウィンドウ',
contextLengthPlaceholder: '不明',
contextLengthInvalid:
+13
View File
@@ -204,6 +204,19 @@ const zhHans = {
selectModelAbilities: '选择模型能力',
visionAbility: '视觉能力',
functionCallAbility: '函数调用',
reasoningAbility: '思考能力',
reasoningLevel: '思考档位',
reasoningLevels: {
providerDefault: 'Provider 默认',
disabled: '关闭',
enabled: '开启',
minimal: '最低',
low: '低',
medium: '中',
high: '高',
xhigh: '极高',
max: '最大',
},
contextLength: '上下文窗口',
contextLengthPlaceholder: '未知',
contextLengthInvalid: '上下文窗口必须是正整数',
@@ -34,12 +34,26 @@ test('normalizes only single-line text fields in a dynamic form save snapshot',
{ name: 'multiline', type: 'text', default: '' },
{ name: 'string-list', type: 'array[string]', default: [] },
{ name: 'count', type: 'integer', default: 0 },
{
name: 'model',
type: 'model-fallback-selector',
default: { primary: '', fallbacks: [], reasoning: {} },
},
];
const values = {
'single-line': '\t hello world \n',
multiline: ' keep multiline whitespace \n',
'string-list': [' first ', ' second '],
count: 3,
model: {
primary: 'primary-model',
fallbacks: ['fallback-model'],
reasoning: {
'primary-model': 'high',
'fallback-model': 'provider_default',
'removed-model': 'medium',
},
},
};
assert.deepEqual(normalizeDynamicFormValuesForSave(specs, values), {
@@ -47,5 +61,12 @@ test('normalizes only single-line text fields in a dynamic form save snapshot',
multiline: ' keep multiline whitespace \n',
'string-list': [' first ', ' second '],
count: 3,
model: {
primary: 'primary-model',
fallbacks: ['fallback-model'],
reasoning: {
'primary-model': 'high',
},
},
});
});