Roo-Code/src/api/transform/reasoning.ts
Dursun Katar a1bedb3434 feat: add Adaptive Thinking support for Claude Opus 4.6 and Sonnet 4.6
- Add useAdaptiveThinking and adaptiveThinkingEffort settings to provider config

- Add supportsAdaptiveThinking and supportsAdaptiveThinkingMaxEffort model flags

- Implement getAnthropicReasoning() with {type: 'adaptive'} mode

- Implement getAnthropicOutputConfig() with effort parameter (low/medium/high/max)

- Max effort only on Opus 4.6; falls back to high on Sonnet 4.6

- Add adaptive thinking checkbox and effort selector to ThinkingBudget UI

- Fix: Max Output Tokens slider always visible (independent of adaptive thinking)

- Add i18n translations for all 17 supported locales

- Add comprehensive test coverage for reasoning transforms

Fixes #11732
2026-02-25 04:29:30 +03:00

204 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import OpenAI from "openai"
import type { GenerateContentConfig } from "@google/genai"
import type { ModelInfo, ProviderSettings, ReasoningEffortExtended } from "@roo-code/types"
import { shouldUseReasoningBudget, shouldUseReasoningEffort } from "../../shared/api"
export type OpenRouterReasoningParams = {
effort?: ReasoningEffortExtended
max_tokens?: number
exclude?: boolean
}
export type RooReasoningParams = {
enabled?: boolean
effort?: ReasoningEffortExtended
}
export type AnthropicReasoningParams = BetaThinkingConfigParam
export type AnthropicOutputConfig = { effort: "low" | "medium" | "high" | "max" }
export type OpenAiReasoningParams = { reasoning_effort: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] }
// Valid Gemini thinking levels for effort-based reasoning
const GEMINI_THINKING_LEVELS = ["minimal", "low", "medium", "high"] as const
export type GeminiThinkingLevel = (typeof GEMINI_THINKING_LEVELS)[number]
export function isGeminiThinkingLevel(value: unknown): value is GeminiThinkingLevel {
return typeof value === "string" && GEMINI_THINKING_LEVELS.includes(value as GeminiThinkingLevel)
}
export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"] & {
thinkingLevel?: GeminiThinkingLevel
}
export type GetModelReasoningOptions = {
model: ModelInfo
reasoningBudget: number | undefined
reasoningEffort: ReasoningEffortExtended | "disable" | undefined
settings: ProviderSettings
}
export const getOpenRouterReasoning = ({
model,
reasoningBudget,
reasoningEffort,
settings,
}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined =>
shouldUseReasoningBudget({ model, settings })
? { max_tokens: reasoningBudget }
: shouldUseReasoningEffort({ model, settings })
? reasoningEffort && reasoningEffort !== "disable"
? { effort: reasoningEffort as ReasoningEffortExtended }
: undefined
: undefined
export const getRooReasoning = ({
model,
reasoningEffort,
settings,
}: GetModelReasoningOptions): RooReasoningParams | undefined => {
// Check if model supports reasoning effort
if (!model.supportsReasoningEffort) {
return undefined
}
if (model.requiredReasoningEffort) {
// Honor the provided effort if it's valid, otherwise let the model choose.
if (reasoningEffort && reasoningEffort !== "disable" && reasoningEffort !== "minimal") {
return { enabled: true, effort: reasoningEffort }
} else {
return { enabled: true }
}
}
// Explicit off switch from settings: always send disabled for back-compat and to
// prevent automatic reasoning when the toggle is turned off.
if (settings.enableReasoningEffort === false) {
return { enabled: false }
}
// For Roo models that support reasoning effort, absence of a selection should be
// treated as an explicit "off" signal so that the backend does not auto-enable
// reasoning. This aligns with the default behavior in tests.
if (!reasoningEffort) {
return { enabled: false }
}
// "disable" is a legacy sentinel that means "omit the reasoning field entirely"
// and let the server decide any defaults.
if (reasoningEffort === "disable") {
return undefined
}
// For Roo, "minimal" is treated as "none" for effort-based reasoning we omit
// the reasoning field entirely instead of sending an explicit effort.
if (reasoningEffort === "minimal") {
return undefined
}
// When an effort is provided (e.g. "low" | "medium" | "high" | "none"), enable
// with the selected effort.
return { enabled: true, effort: reasoningEffort as ReasoningEffortExtended }
}
export const getAnthropicReasoning = ({
model,
reasoningBudget,
settings,
}: GetModelReasoningOptions): AnthropicReasoningParams | undefined => {
// Adaptive thinking: Claude determines dynamically when and how much to use extended thinking.
// Supported on claude-sonnet-4-6 and claude-opus-4-6 only.
if (settings?.useAdaptiveThinking && model.supportsAdaptiveThinking) {
return { type: "adaptive" as any } as any
}
// Manual mode: fixed budget_tokens
return shouldUseReasoningBudget({ model, settings })
? { type: "enabled", budget_tokens: reasoningBudget! }
: undefined
}
/**
* Returns the `output_config` top-level parameter for Anthropic API calls when adaptive thinking
* is enabled with an effort level. This is a SEPARATE top-level parameter from `thinking`.
* See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#adaptive-thinking
*/
export const getAnthropicOutputConfig = ({
model,
settings,
}: Pick<GetModelReasoningOptions, "model" | "settings">): AnthropicOutputConfig | undefined => {
if (!settings?.useAdaptiveThinking || !model.supportsAdaptiveThinking) {
return undefined
}
const effort = settings.adaptiveThinkingEffort
if (!effort) {
return undefined
}
// "max" effort is only supported on claude-opus-4-6; fall back to "high" otherwise
if (effort === "max" && !model.supportsAdaptiveThinkingMaxEffort) {
return { effort: "high" }
}
return { effort }
}
export const getOpenAiReasoning = ({
model,
reasoningEffort,
settings,
}: GetModelReasoningOptions): OpenAiReasoningParams | undefined => {
if (!shouldUseReasoningEffort({ model, settings })) return undefined
if (reasoningEffort === "disable" || !reasoningEffort) return undefined
// Include "none" | "minimal" | "low" | "medium" | "high" literally
return {
reasoning_effort: reasoningEffort as OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"],
}
}
export const getGeminiReasoning = ({
model,
reasoningBudget,
reasoningEffort,
settings,
}: GetModelReasoningOptions): GeminiReasoningParams | undefined => {
// Budget-based (2.5) models: use thinkingBudget, not thinkingLevel.
if (shouldUseReasoningBudget({ model, settings })) {
return { thinkingBudget: reasoningBudget!, includeThoughts: true }
}
// For effort-based Gemini models, rely directly on the selected effort value.
// We intentionally ignore enableReasoningEffort here so that explicitly chosen
// efforts in the UI (e.g. "High" for gemini-3-pro-preview) always translate
// into a thinkingConfig, regardless of legacy boolean flags.
const selectedEffort = (settings.reasoningEffort ?? model.reasoningEffort) as
| ReasoningEffortExtended
| "disable"
| undefined
// Respect "off" / unset semantics from the effort selector itself.
if (!selectedEffort || selectedEffort === "disable") {
return undefined
}
// Validate that the selected effort is supported by this specific model.
// e.g. gemini-3-pro-preview only supports ["low", "high"] — sending
// "medium" (carried over from a different model's settings) causes errors.
const effortToUse =
Array.isArray(model.supportsReasoningEffort) &&
isGeminiThinkingLevel(selectedEffort) &&
!model.supportsReasoningEffort.includes(selectedEffort)
? model.reasoningEffort
: selectedEffort
// Effort-based models on Google GenAI support minimal/low/medium/high levels.
if (!effortToUse || !isGeminiThinkingLevel(effortToUse)) {
return undefined
}
return { thinkingLevel: effortToUse, includeThoughts: true }
}