diff --git a/src/api/transform/__tests__/model-params.spec.ts b/src/api/transform/__tests__/model-params.spec.ts index 0440a3ccdb2..75b5c50c592 100644 --- a/src/api/transform/__tests__/model-params.spec.ts +++ b/src/api/transform/__tests__/model-params.spec.ts @@ -275,7 +275,6 @@ describe("getModelParams", () => { expect(result.reasoningBudget).toBeUndefined() expect(result.temperature).toBe(0) - expect(result.reasoning).toBeUndefined() }) it("should honor customMaxTokens for reasoning budget models", () => { @@ -557,7 +556,6 @@ describe("getModelParams", () => { }) expect(result.reasoningEffort).toBeUndefined() - expect(result.reasoning).toBeUndefined() }) it("should handle reasoning effort for openrouter format", () => { @@ -624,7 +622,6 @@ describe("getModelParams", () => { }) expect(result.reasoningEffort).toBeUndefined() - expect(result.reasoning).toBeUndefined() }) it("should include 'minimal' and 'none' for openrouter format", () => { @@ -670,7 +667,7 @@ describe("getModelParams", () => { it("should use reasoningEffort if supportsReasoningEffort is false but reasoningEffort is set", () => { const model: ModelInfo = { ...baseModel, - maxTokens: 3000, // Changed to 3000 (18.75% of 16000), which is within 20% threshold + maxTokens: 3000, // 3000 is 18.75% of 16000, within 20% threshold supportsReasoningEffort: false, reasoningEffort: "medium", } @@ -681,7 +678,8 @@ describe("getModelParams", () => { model, }) - expect(result.maxTokens).toBe(3000) // Now uses model.maxTokens since it's within 20% threshold + expect(result.maxTokens).toBe(3000) + // Now uses model.maxTokens since it's within 20% threshold expect(result.reasoningEffort).toBe("medium") }) }) @@ -735,7 +733,7 @@ describe("getModelParams", () => { }) expect(result.reasoningBudget).toBe(3200) // 80% of 4000 - expect(result.reasoningEffort).toBeUndefined() + expect(result.reasoningEffort).toBeUndefined() // Budget takes precedence expect(result.temperature).toBe(1.0) }) @@ -889,8 +887,6 @@ describe("getModelParams", () => { settings: {}, model, }) - - expect(result.reasoning).toBeUndefined() }) }) diff --git a/src/api/transform/model-params.ts b/src/api/transform/model-params.ts index 246fc3f1fdb..9e1d421f6fe 100644 --- a/src/api/transform/model-params.ts +++ b/src/api/transform/model-params.ts @@ -130,10 +130,12 @@ export function getModelParams({ temperature = 1.0 } else if (shouldUseReasoningEffort({ model, settings })) { // "Traditional" reasoning models use the `reasoningEffort` parameter. - const effort = (customReasoningEffort ?? model.reasoningEffort) as - | ReasoningEffortExtended - | "disable" - | undefined + // Only fallback to model default if user hasn't explicitly set a value. + // If customReasoningEffort is "disable", don't fallback to model default. + const effort = + customReasoningEffort !== undefined + ? customReasoningEffort + : (model.reasoningEffort as ReasoningEffortExtended | "disable" | undefined) // Capability and settings checks are handled by shouldUseReasoningEffort. // Here we simply propagate the resolved effort into the params, while // still treating "disable" as an omission. diff --git a/src/shared/__tests__/api.spec.ts b/src/shared/__tests__/api.spec.ts index 61839d14a5a..46b948bf2fd 100644 --- a/src/shared/__tests__/api.spec.ts +++ b/src/shared/__tests__/api.spec.ts @@ -379,7 +379,6 @@ describe("shouldUseReasoningEffort", () => { reasoningEffort: "medium", } - // Should return true regardless of settings (unless explicitly disabled) expect(shouldUseReasoningEffort({ model })).toBe(true) expect(shouldUseReasoningEffort({ model, settings: {} })).toBe(true) expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: undefined } })).toBe(true) @@ -444,7 +443,7 @@ describe("shouldUseReasoningEffort", () => { expect(shouldUseReasoningEffort({ model })).toBe(false) }) - test("should return false when model doesn't support reasoning effort", () => { + test("should return false when model doesn't support reasoning effort and has no default", () => { const model: ModelInfo = { contextWindow: 200_000, supportsPromptCache: true, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 9defd0695ab..2a1f792eb48 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -109,7 +109,6 @@ import { inputEventTransform, noTransform } from "./transforms" import { ModelInfoView } from "./ModelInfoView" import { ApiErrorMessage } from "./ApiErrorMessage" import { ThinkingBudget } from "./ThinkingBudget" -import { SimpleThinkingBudget } from "./SimpleThinkingBudget" import { Verbosity } from "./Verbosity" import { DiffSettingsControl } from "./DiffSettingsControl" import { TodoListSettingsControl } from "./TodoListSettingsControl" @@ -855,22 +854,14 @@ const ApiOptions = ({ )} - {!fromWelcomeView && - (selectedProvider === "roo" ? ( - - ) : ( - - ))} + {!fromWelcomeView && ( + + )} {/* Gate Verbosity UI by capability flag */} {!fromWelcomeView && selectedModelInfo?.supportsVerbosity && ( diff --git a/webview-ui/src/components/settings/SimpleThinkingBudget.tsx b/webview-ui/src/components/settings/SimpleThinkingBudget.tsx deleted file mode 100644 index 60b163738dd..00000000000 --- a/webview-ui/src/components/settings/SimpleThinkingBudget.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useEffect } from "react" - -import { type ProviderSettings, type ModelInfo, type ReasoningEffort, reasoningEfforts } from "@roo-code/types" - -import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" - -interface SimpleThinkingBudgetProps { - apiConfiguration: ProviderSettings - setApiConfigurationField: ( - field: K, - value: ProviderSettings[K], - isUserAction?: boolean, - ) => void - modelInfo?: ModelInfo -} - -// Extended type to include "none" option -type ReasoningEffortWithNone = ReasoningEffort | "none" - -export const SimpleThinkingBudget = ({ - apiConfiguration, - setApiConfigurationField, - modelInfo, -}: SimpleThinkingBudgetProps) => { - const { t } = useAppTranslation() - - // Check model capabilities - const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort - const isReasoningEffortRequired = !!modelInfo && modelInfo.requiredReasoningEffort - - // Build available reasoning efforts list - // Include "none" option unless reasoning effort is required - const baseEfforts = [...reasoningEfforts] as ReasoningEffort[] - const availableReasoningEfforts: ReadonlyArray = isReasoningEffortRequired - ? baseEfforts - : (["none", ...baseEfforts] as ReasoningEffortWithNone[]) - - // Default reasoning effort - use model's default if available, otherwise "medium" - const modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffort | undefined - const defaultReasoningEffort: ReasoningEffortWithNone = isReasoningEffortRequired - ? modelDefaultReasoningEffort || "medium" - : "none" - - // Current reasoning effort - treat undefined/null as "none" - const currentReasoningEffort: ReasoningEffortWithNone = - (apiConfiguration.reasoningEffort as ReasoningEffort | undefined) || defaultReasoningEffort - - // Set default reasoning effort when model supports it and no value is set - useEffect(() => { - if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) { - // Only set a default if reasoning is required, otherwise leave as undefined (which maps to "none") - if (isReasoningEffortRequired && defaultReasoningEffort !== "none") { - setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffort, false) - } - } - }, [ - isReasoningEffortSupported, - isReasoningEffortRequired, - apiConfiguration.reasoningEffort, - defaultReasoningEffort, - setApiConfigurationField, - ]) - - useEffect(() => { - if (!isReasoningEffortSupported) return - const shouldEnable = isReasoningEffortRequired || currentReasoningEffort !== "none" - if (shouldEnable && apiConfiguration.enableReasoningEffort !== true) { - setApiConfigurationField("enableReasoningEffort", true, false) - } - }, [ - isReasoningEffortSupported, - isReasoningEffortRequired, - currentReasoningEffort, - apiConfiguration.enableReasoningEffort, - setApiConfigurationField, - ]) - if (!modelInfo || !isReasoningEffortSupported) { - return null - } - - return ( -
-
- -
- -
- ) -} diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 151b8c58cef..62640b72d88 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -78,26 +78,62 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod // Build available reasoning efforts list from capability const supports = modelInfo?.supportsReasoningEffort - const availableOptions: ReadonlyArray = + const baseAvailableOptions: ReadonlyArray = supports === true ? (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) : Array.isArray(supports) ? (supports as ReadonlyArray) : (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) + // "disable" turns off reasoning entirely; "none" is a valid reasoning level. + // Both display as "None" in the UI but behave differently. + // Add "disable" option if reasoning effort is not required. + type ReasoningEffortOption = ReasoningEffortWithMinimal | "none" | "disable" + const availableOptions: ReadonlyArray = modelInfo?.requiredReasoningEffort + ? (baseAvailableOptions as ReadonlyArray) + : (["disable", ...baseAvailableOptions] as ReasoningEffortOption[]) + // Default reasoning effort - use model's default if available // GPT-5 models have "medium" as their default in the model configuration const modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffortWithMinimal | undefined - const defaultReasoningEffort: ReasoningEffortWithMinimal = modelDefaultReasoningEffort || "medium" - const currentReasoningEffort: ReasoningEffortWithMinimal = - (apiConfiguration.reasoningEffort as ReasoningEffortWithMinimal | undefined) || defaultReasoningEffort + const defaultReasoningEffort: ReasoningEffortOption = modelInfo?.requiredReasoningEffort + ? modelDefaultReasoningEffort || "medium" + : "disable" + // Current reasoning effort from settings, or fall back to default + const storedReasoningEffort = apiConfiguration.reasoningEffort as ReasoningEffortOption | undefined + const currentReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort // Set default reasoning effort when model supports it and no value is set useEffect(() => { - if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort && defaultReasoningEffort) { - setApiConfigurationField("reasoningEffort", defaultReasoningEffort, false) + if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) { + // Only set a default if reasoning is required, otherwise leave as undefined (which maps to "disable") + if (modelInfo?.requiredReasoningEffort && defaultReasoningEffort !== "disable") { + setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortWithMinimal, false) + } + } + }, [ + isReasoningEffortSupported, + apiConfiguration.reasoningEffort, + defaultReasoningEffort, + modelInfo?.requiredReasoningEffort, + setApiConfigurationField, + ]) + + // Sync enableReasoningEffort based on selection + // "disable" turns off reasoning; "none" is a valid level (reasoning enabled) + useEffect(() => { + if (!isReasoningEffortSupported) return + const shouldEnable = modelInfo?.requiredReasoningEffort || currentReasoningEffort !== "disable" + if (shouldEnable && apiConfiguration.enableReasoningEffort !== true) { + setApiConfigurationField("enableReasoningEffort", true, false) } - }, [isReasoningEffortSupported, apiConfiguration.reasoningEffort, defaultReasoningEffort, setApiConfigurationField]) + }, [ + isReasoningEffortSupported, + modelInfo?.requiredReasoningEffort, + currentReasoningEffort, + apiConfiguration.enableReasoningEffort, + setApiConfigurationField, + ]) const enableReasoningEffort = apiConfiguration.enableReasoningEffort const customMaxOutputTokens = apiConfiguration.modelMaxTokens || DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS @@ -193,14 +229,24 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod