From 5299fea8d50ad5b3992093c85822535f24a7f5b2 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Tue, 30 Jun 2026 22:01:13 +0000 Subject: [PATCH 1/3] fix(ThinkingBudget): handling xhigh, max and disabled values --- .../components/settings/ThinkingBudget.tsx | 42 ++++++------- .../__tests__/ThinkingBudget.spec.tsx | 60 +++++++++++++++++-- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 0f7af4d549..a95b0a6f4b 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -2,9 +2,9 @@ Semantics for Reasoning Effort (ThinkingBudget) Capability surface: -- modelInfo.supportsReasoningEffort: boolean | Array<"disable" | "none" | "minimal" | "low" | "medium" | "high"> +- modelInfo.supportsReasoningEffort: boolean | Array<"disable" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"> - true → UI shows ["low","medium","high"] - - array → UI shows exactly the provided values + - array → UI shows exactly the provided values (e.g. GPT-5.5 includes "xhigh") Selection behavior: - "disable": @@ -17,7 +17,7 @@ Selection behavior: - set enableReasoningEffort = true - persist reasoningEffort = "none" - request builders include reasoning with value "none" -- "minimal" | "low" | "medium" | "high": +- "minimal" | "low" | "medium" | "high" | "xhigh" | "max": - set enableReasoningEffort = true - persist the selected value - request builders include reasoning with the selected effort @@ -35,12 +35,7 @@ Notes: import { useEffect } from "react" import { Checkbox } from "vscrui" -import { - type ProviderSettings, - type ModelInfo, - type ReasoningEffortWithMinimal, - reasoningEfforts, -} from "@roo-code/types" +import { type ProviderSettings, type ModelInfo, type ReasoningEffortExtended, reasoningEfforts } from "@roo-code/types" import { DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS, @@ -81,31 +76,32 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod // max-tokens control, so only surface this standalone slider when that branch is inactive. const isMaxTokensConfigurable = !!modelInfo && modelInfo.supportsMaxTokens && !isReasoningBudgetSupported - // Build available reasoning efforts list from capability + // "disable" turns off reasoning entirely; "none" is a valid reasoning level. + // Both display as "None" in the UI but behave differently. + // Arrays from supportsReasoningEffort may include "disable" (e.g. Z.ai GLM), so type the + // full option set as ReasoningEffortExtended | "disable" from the start to avoid casts. + type ReasoningEffortOption = ReasoningEffortExtended | "disable" const supports = modelInfo?.supportsReasoningEffort - const baseAvailableOptions: ReadonlyArray = + const baseAvailableOptions: ReadonlyArray = supports === true - ? (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) + ? (reasoningEfforts as readonly ReasoningEffortOption[]) : Array.isArray(supports) - ? (supports as ReadonlyArray) - : (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) + ? (supports as ReadonlyArray) + : (reasoningEfforts as readonly ReasoningEffortOption[]) - // "disable" turns off reasoning entirely; "none" is a valid reasoning level. - // Both display as "None" in the UI but behave differently. // Add "disable" option only when: // 1. requiredReasoningEffort is not true, AND // 2. supportsReasoningEffort is boolean true (not an explicit array) // When the model provides an explicit array, respect those exact values. - type ReasoningEffortOption = ReasoningEffortWithMinimal | "none" | "disable" const shouldAutoAddDisable = - !modelInfo?.requiredReasoningEffort && supports === true && !baseAvailableOptions.includes("disable" as any) + !modelInfo?.requiredReasoningEffort && supports === true && !baseAvailableOptions.includes("disable") const availableOptions: ReadonlyArray = shouldAutoAddDisable - ? (["disable", ...baseAvailableOptions] as ReasoningEffortOption[]) - : (baseAvailableOptions as ReadonlyArray) + ? ["disable", ...baseAvailableOptions] + : baseAvailableOptions // 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 modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffortExtended | undefined const defaultReasoningEffort: ReasoningEffortOption = modelInfo?.requiredReasoningEffort ? modelDefaultReasoningEffort || "medium" : "disable" @@ -118,7 +114,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod 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) + setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortExtended, false) } } }, [ @@ -282,7 +278,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod } else { // "none", "minimal", "low", "medium", "high" all enable reasoning setApiConfigurationField("enableReasoningEffort", true) - setApiConfigurationField("reasoningEffort", value as ReasoningEffortWithMinimal) + setApiConfigurationField("reasoningEffort", value as ReasoningEffortExtended) } }}> diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index 904822f787..38fc6d97b5 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -1,5 +1,7 @@ // npx vitest src/components/settings/__tests__/ThinkingBudget.spec.tsx +import React from "react" + import { render, screen, fireEvent } from "@/utils/test-utils" import type { ModelInfo } from "@roo-code/types" @@ -18,16 +20,20 @@ vi.mock("@/components/ui", () => ({ onChange={(e) => onValueChange([parseInt(e.target.value)])} /> ), - Select: ({ children, value, onValueChange: _onValueChange }: any) => ( -
- {children} + Select: ({ children, value, onValueChange }: any) => ( +
+ {React.Children.map(children, (child) => React.cloneElement(child, { onValueChange }))}
), SelectTrigger: ({ children }: any) => , SelectValue: ({ placeholder }: any) => {placeholder}, - SelectContent: ({ children }: any) =>
{children}
, - SelectItem: ({ children, value }: any) => ( -
+ SelectContent: ({ children, onValueChange }: any) => ( +
+ {React.Children.map(children, (child) => React.cloneElement(child, { onValueChange }))} +
+ ), + SelectItem: ({ children, value, onValueChange }: any) => ( +
onValueChange?.(value)}> {children}
), @@ -304,6 +310,48 @@ describe("ThinkingBudget", () => { expect(screen.getByTestId("select-item-medium")).toBeInTheDocument() expect(screen.getByTestId("select-item-high")).toBeInTheDocument() }) + + it("should show 'xhigh' option when supportsReasoningEffort array includes xhigh (e.g. gpt-5.5)", () => { + render( + , + ) + + expect(screen.getByTestId("reasoning-effort")).toBeInTheDocument() + // Exactly the declared options — no unsupported tiers or auto-added "disable" + expect(screen.getByTestId("select-item-none")).toBeInTheDocument() + expect(screen.getByTestId("select-item-low")).toBeInTheDocument() + expect(screen.getByTestId("select-item-medium")).toBeInTheDocument() + expect(screen.getByTestId("select-item-high")).toBeInTheDocument() + expect(screen.getByTestId("select-item-xhigh")).toBeInTheDocument() + expect(screen.queryByTestId("select-item-disable")).not.toBeInTheDocument() + expect(screen.queryByTestId("select-item-max")).not.toBeInTheDocument() + }) + + it("should enable reasoning and persist 'xhigh' when xhigh is selected", () => { + const setApiConfigurationField = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByTestId("select-item-xhigh")) + + expect(setApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", true) + expect(setApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", "xhigh") + }) }) describe("configurable max output tokens (supportsMaxTokens)", () => { From 41ba0c31a605874ddaa9603a33342362fc83be28 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Wed, 1 Jul 2026 00:00:58 +0000 Subject: [PATCH 2/3] feat(ThinkingBudget): set dropdown to available options --- .../src/components/settings/ThinkingBudget.tsx | 8 ++++++-- .../settings/__tests__/ThinkingBudget.spec.tsx | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index a95b0a6f4b..9525ba7a95 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -105,9 +105,13 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod const defaultReasoningEffort: ReasoningEffortOption = modelInfo?.requiredReasoningEffort ? modelDefaultReasoningEffort || "medium" : "disable" - // Current reasoning effort from settings, or fall back to default + // Current reasoning effort from settings, or fall back to default. + // Clamp to availableOptions so the Select trigger always renders a valid option. const storedReasoningEffort = apiConfiguration.reasoningEffort as ReasoningEffortOption | undefined - const currentReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort + const rawReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort + const currentReasoningEffort: ReasoningEffortOption = availableOptions.includes(rawReasoningEffort) + ? rawReasoningEffort + : (availableOptions[0] ?? rawReasoningEffort) // Set default reasoning effort when model supports it and no value is set useEffect(() => { diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index 38fc6d97b5..671b1d302b 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -272,6 +272,23 @@ describe("ThinkingBudget", () => { expect(screen.getByTestId("select-item-high")).toBeInTheDocument() }) + it("should fall back to first available option when stored value is not in the explicit array", () => { + // Covers the clamp branch: defaultReasoningEffort="disable" but array omits "disable" + render( + , + ) + + // The select value should be "low" (first item), not "disable" + expect(screen.getByTestId("select")).toHaveAttribute("data-value", "low") + }) + it("should show 'disable' option when supportsReasoningEffort array explicitly includes disable", () => { render( Date: Wed, 1 Jul 2026 00:09:51 +0000 Subject: [PATCH 3/3] test(ThikningBudget): coverage --- .../settings/__tests__/ThinkingBudget.spec.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index 671b1d302b..5f9b74dfe0 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -289,6 +289,23 @@ describe("ThinkingBudget", () => { expect(screen.getByTestId("select")).toHaveAttribute("data-value", "low") }) + it("should fall back to rawReasoningEffort when availableOptions is empty", () => { + // Covers the ?? rawReasoningEffort branch when availableOptions[0] is undefined + render( + , + ) + + // With an empty options array, falls back to the stored value "medium" + expect(screen.getByTestId("select")).toHaveAttribute("data-value", "medium") + }) + it("should show 'disable' option when supportsReasoningEffort array explicitly includes disable", () => { render(