diff --git a/frontend/graphql-schema.graphql b/frontend/graphql-schema.graphql index c26f35f7..0cc6479d 100644 --- a/frontend/graphql-schema.graphql +++ b/frontend/graphql-schema.graphql @@ -240,6 +240,9 @@ fragment modelConfigFragment on ModelConfig { reasoning { mode efforts + supported + cannotDisable + defaultOn } price { input diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index a1c26ad1..18428ba0 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -229,6 +229,7 @@ export enum ReasoningEffort { export enum ReasoningMode { Adaptive = 'adaptive', Budget = 'budget', + Off = 'off', } export enum ResultFormat { @@ -497,7 +498,13 @@ export type ProviderTestResultFragmentFragment = { export type ModelConfigFragmentFragment = { name: string; thinking: boolean | null; - reasoning: { mode: ModelReasoningMode | null; efforts: Array | null } | null; + reasoning: { + mode: ModelReasoningMode | null; + efforts: Array | null; + supported: boolean | null; + cannotDisable: boolean | null; + defaultOn: boolean | null; + } | null; price: { input: number; output: number; cacheRead: number; cacheWrite: number } | null; }; @@ -2113,6 +2120,9 @@ export const ModelConfigFragmentFragmentDoc = { selections: [ { kind: 'Field', name: { kind: 'Name', value: 'mode' } }, { kind: 'Field', name: { kind: 'Name', value: 'efforts' } }, + { kind: 'Field', name: { kind: 'Name', value: 'supported' } }, + { kind: 'Field', name: { kind: 'Name', value: 'cannotDisable' } }, + { kind: 'Field', name: { kind: 'Name', value: 'defaultOn' } }, ], }, }, @@ -4079,6 +4089,9 @@ export const SettingsProvidersDocument = { selections: [ { kind: 'Field', name: { kind: 'Name', value: 'mode' } }, { kind: 'Field', name: { kind: 'Name', value: 'efforts' } }, + { kind: 'Field', name: { kind: 'Name', value: 'supported' } }, + { kind: 'Field', name: { kind: 'Name', value: 'cannotDisable' } }, + { kind: 'Field', name: { kind: 'Name', value: 'defaultOn' } }, ], }, }, diff --git a/frontend/src/pages/settings/settings-provider.tsx b/frontend/src/pages/settings/settings-provider.tsx index 29fb5e1f..e289754e 100644 --- a/frontend/src/pages/settings/settings-provider.tsx +++ b/frontend/src/pages/settings/settings-provider.tsx @@ -160,7 +160,13 @@ interface FormModelComboboxItemProps extend interface ModelOption { name: string; price?: null | { cacheRead: number; cacheWrite: number; input: number; output: number }; - reasoning?: null | { efforts?: null | ReasoningEffort[]; mode?: ModelReasoningMode | null }; + reasoning?: null | { + cannotDisable?: boolean | null; + defaultOn?: boolean | null; + efforts?: null | ReasoningEffort[]; + mode?: ModelReasoningMode | null; + supported?: boolean | null; + }; thinking?: boolean | null; } @@ -643,6 +649,10 @@ const getReasoningMode = (mode: null | string | undefined): null | ReasoningMode return ReasoningMode.Budget; } + case ReasoningMode.Off: { + return ReasoningMode.Off; + } + default: { return null; } @@ -675,18 +685,34 @@ function ReasoningFields({ setValue: UseFormSetValue; }) { const selectedModel = useWatch({ control, name: `agents.${agentKey}.model` }); + const reasoningMode = useWatch({ control, name: `agents.${agentKey}.reasoning.mode` }); const capability = models.find((model) => model.name === selectedModel)?.reasoning ?? null; const isAdaptiveOnly = capability?.mode === ModelReasoningMode.AdaptiveOnly; const supportsAdaptive = isAdaptiveOnly || capability?.mode === ModelReasoningMode.Adaptive; + // Off is offered only where the capability confirms a disable actually takes + // effect (cannotDisable=false). An absent capability, an always-on model, or a + // model where Off would be a silent no-op keeps the Off option hidden. + const canDisable = capability != null && capability.cannotDisable !== true; + const isOff = reasoningMode === ReasoningMode.Off; const allowedEfforts = capability?.efforts && capability.efforts.length > 0 ? capability.efforts : defaultReasoningEfforts; + // Reconcile a stale Off when the current model can't disable (e.g. after typing + // a custom model name): otherwise mode=off is orphaned with no control to clear + // it and silently persists, disabling thinking with no UI affordance. Guarded on + // models.length so a still-loading capability list does not wipe a saved Off. + useEffect(() => { + if (isOff && !canDisable && models.length > 0) { + setValue(`agents.${agentKey}.reasoning.mode` as const, null); + } + }, [isOff, canDisable, models.length, agentKey, setValue]); + return (

Reasoning Configuration

- {supportsAdaptive && ( + {(supportsAdaptive || canDisable) && ( Reasoning Mode {isAdaptiveOnly - ? 'This model supports only adaptive thinking.' - : 'Adaptive lets the model decide how much to think; budget uses a fixed token budget.'} + ? canDisable + ? 'This model thinks adaptively; choose Off to disable thinking.' + : 'This model supports only adaptive thinking and cannot be disabled.' + : 'Adaptive lets the model decide how much to think; budget uses a fixed token budget; off disables thinking.'} @@ -729,7 +762,7 @@ function ReasoningFields({ Reasoning Effort