mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-23 00:43:14 +00:00
- 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
204 lines
7 KiB
TypeScript
204 lines
7 KiB
TypeScript
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 }
|
||
}
|