diff --git a/src/api/transform/__tests__/reasoning.spec.ts b/src/api/transform/__tests__/reasoning.spec.ts index 211a02f152..fc0983d741 100644 --- a/src/api/transform/__tests__/reasoning.spec.ts +++ b/src/api/transform/__tests__/reasoning.spec.ts @@ -1,6 +1,6 @@ // npx vitest run src/api/transform/__tests__/reasoning.spec.ts -import type { ModelInfo, ProviderSettings } from "@roo-code/types" +import type { ModelInfo, ProviderSettings, ReasoningEffortWithMinimal } from "@roo-code/types" import { getOpenRouterReasoning, @@ -154,24 +154,81 @@ describe("reasoning.ts", () => { const result = getOpenRouterReasoning(optionsWithoutEffort) - expect(result).toEqual({ effort: undefined }) + // When reasoningEffort is undefined, the function should return undefined + expect(result).toBeUndefined() }) - it("should handle all reasoning effort values", () => { - const efforts: Array<"low" | "medium" | "high"> = ["low", "medium", "high"] + it("should handle all reasoning effort values including minimal", () => { + const efforts: Array = ["minimal", "low", "medium", "high"] efforts.forEach((effort) => { const modelWithEffort: ModelInfo = { ...baseModel, + supportsReasoningEffort: true, + } + + const settingsWithEffort: ProviderSettings = { reasoningEffort: effort, } - const options = { ...baseOptions, model: modelWithEffort, reasoningEffort: effort } + const options = { + ...baseOptions, + model: modelWithEffort, + settings: settingsWithEffort, + reasoningEffort: effort, + } const result = getOpenRouterReasoning(options) + // All effort values including "minimal" should be passed through expect(result).toEqual({ effort }) }) }) + it("should handle minimal reasoning effort specifically", () => { + const modelWithSupported: ModelInfo = { + ...baseModel, + supportsReasoningEffort: true, + } + + const settingsWithEffort: ProviderSettings = { + reasoningEffort: "minimal", + } + + const options = { + ...baseOptions, + model: modelWithSupported, + settings: settingsWithEffort, + reasoningEffort: "minimal" as ReasoningEffortWithMinimal, + } + + const result = getOpenRouterReasoning(options) + + // "minimal" should be passed through to OpenRouter + expect(result).toEqual({ effort: "minimal" }) + }) + + it("should handle minimal reasoning effort from settings", () => { + const modelWithSupported: ModelInfo = { + ...baseModel, + supportsReasoningEffort: true, + } + + const settingsWithMinimal: ProviderSettings = { + reasoningEffort: "minimal" as ReasoningEffortWithMinimal, + } + + const options = { + ...baseOptions, + model: modelWithSupported, + settings: settingsWithMinimal, + reasoningEffort: "minimal" as ReasoningEffortWithMinimal, + } + + const result = getOpenRouterReasoning(options) + + // "minimal" should be passed through to OpenRouter + expect(result).toEqual({ effort: "minimal" }) + }) + it("should handle zero reasoningBudget", () => { const modelWithRequired: ModelInfo = { ...baseModel, diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index 46ef029ea3..100b1c2684 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -6,10 +6,8 @@ import type { ModelInfo, ProviderSettings, ReasoningEffortWithMinimal } from "@r import { shouldUseReasoningBudget, shouldUseReasoningEffort } from "../../shared/api" -type ReasoningEffort = "low" | "medium" | "high" - export type OpenRouterReasoningParams = { - effort?: ReasoningEffort + effort?: ReasoningEffortWithMinimal max_tokens?: number exclude?: boolean } @@ -36,7 +34,7 @@ export const getOpenRouterReasoning = ({ shouldUseReasoningBudget({ model, settings }) ? { max_tokens: reasoningBudget } : shouldUseReasoningEffort({ model, settings }) - ? reasoningEffort !== "minimal" + ? reasoningEffort ? { effort: reasoningEffort } : undefined : undefined diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index a3e2d428b4..c2aa234983 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -24,6 +24,17 @@ interface ThinkingBudgetProps { modelInfo?: ModelInfo } +// Helper function to determine if minimal option should be shown +const shouldShowMinimalOption = ( + provider: string | undefined, + modelId: string | undefined, + supportsEffort: boolean | undefined, +): boolean => { + const isGpt5Model = provider === "openai-native" && modelId?.startsWith("gpt-5") + const isOpenRouterWithEffort = provider === "openrouter" && supportsEffort === true + return !!(isGpt5Model || isOpenRouterWithEffort) +} + export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { const { t } = useAppTranslation() const { id: selectedModelId } = useSelectedModel(apiConfiguration) @@ -32,14 +43,21 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod const isGemini25Pro = selectedModelId && selectedModelId.includes("gemini-2.5-pro") const minThinkingTokens = isGemini25Pro ? GEMINI_25_PRO_MIN_THINKING_TOKENS : 1024 - // Check if this is a GPT-5 model to show "minimal" option - // Only show minimal for OpenAI Native provider GPT-5 models - const isOpenAiNativeProvider = apiConfiguration.apiProvider === "openai-native" - const isGpt5Model = isOpenAiNativeProvider && selectedModelId && selectedModelId.startsWith("gpt-5") - // Add "minimal" option for GPT-5 models - // Spread to convert readonly tuple into a mutable array, then expose as readonly for safety + // Check model capabilities + const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget + const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget + const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort + + // Determine if minimal option should be shown + const showMinimalOption = shouldShowMinimalOption( + apiConfiguration.apiProvider, + selectedModelId, + isReasoningEffortSupported, + ) + + // Build available reasoning efforts list const baseEfforts = [...reasoningEfforts] as ReasoningEffortWithMinimal[] - const availableReasoningEfforts: ReadonlyArray = isGpt5Model + const availableReasoningEfforts: ReadonlyArray = showMinimalOption ? (["minimal", ...baseEfforts] as ReasoningEffortWithMinimal[]) : baseEfforts @@ -50,10 +68,6 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod const currentReasoningEffort: ReasoningEffortWithMinimal = (apiConfiguration.reasoningEffort as ReasoningEffortWithMinimal | undefined) || defaultReasoningEffort - const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget - const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget - const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort - // Set default reasoning effort when model supports it and no value is set useEffect(() => { if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort && defaultReasoningEffort) {