Skip to content

feat: add manual thinking budget control for supported providers/models #3892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function getModelParams({
const {
modelMaxTokens: customMaxTokens,
modelMaxThinkingTokens: customMaxThinkingTokens,
manualThinkingBudgetEnabled,
modelTemperature: customTemperature,
reasoningEffort: customReasoningEffort,
} = options
Expand All @@ -126,16 +127,22 @@ export function getModelParams({
let thinking: BetaThinkingConfigParam | undefined = undefined
let temperature = customTemperature ?? defaultTemperature
const reasoningEffort = customReasoningEffort ?? defaultReasoningEffort

if (model.thinking) {
// Only honor `customMaxTokens` for thinking models.
maxTokens = customMaxTokens ?? maxTokens

// Clamp the thinking budget to be at most 80% of max tokens and at
// least 1024 tokens.
const maxBudgetTokens = Math.floor((maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS) * 0.8)
const budgetTokens = Math.max(Math.min(customMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens), 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
if (manualThinkingBudgetEnabled) {
// Manual mode: Set specific thinking budget
// Clamp the thinking budget to be at most 80% of max tokens and at
// least 1024 tokens.
const maxBudgetTokens = Math.floor((maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS) * 0.8)
const budgetTokens = Math.max(Math.min(customMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens), 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
} else {
// The provider/model will determine the thinking token limit
// by not setting the thinking parameter (or setting it to undefined)
thinking = undefined
}

// Anthropic "Thinking" models require a temperature of 1.0.
temperature = 1.0
Expand Down
7 changes: 4 additions & 3 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
return {
id,
info,
thinkingConfig: this.options.modelMaxThinkingTokens
? { thinkingBudget: this.options.modelMaxThinkingTokens }
: undefined,
thinkingConfig:
this.options.manualThinkingBudgetEnabled && this.options.modelMaxThinkingTokens
? { thinkingBudget: this.options.modelMaxThinkingTokens }
: undefined,
maxOutputTokens: this.options.modelMaxTokens ?? info.maxTokens ?? undefined,
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/api/providers/vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand
return {
id,
info,
thinkingConfig: this.options.modelMaxThinkingTokens
? { thinkingBudget: this.options.modelMaxThinkingTokens }
: undefined,
thinkingConfig:
this.options.manualThinkingBudgetEnabled && this.options.modelMaxThinkingTokens
? { thinkingBudget: this.options.modelMaxThinkingTokens }
: undefined,
maxOutputTokens: this.options.modelMaxTokens ?? info.maxTokens ?? undefined,
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/core/config/ProviderSettingsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const providerProfilesSchema = z.object({
rateLimitSecondsMigrated: z.boolean().optional(),
diffSettingsMigrated: z.boolean().optional(),
openAiHeadersMigrated: z.boolean().optional(),
manualThinkingBudgetMigrated: z.boolean().optional(),
})
.optional(),
})
Expand All @@ -43,6 +44,7 @@ export class ProviderSettingsManager {
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
diffSettingsMigrated: true, // Mark as migrated on fresh installs
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
manualThinkingBudgetMigrated: true, // Mark as migrated on fresh installs
},
}

Expand Down Expand Up @@ -108,6 +110,7 @@ export class ProviderSettingsManager {
rateLimitSecondsMigrated: false,
diffSettingsMigrated: false,
openAiHeadersMigrated: false,
manualThinkingBudgetMigrated: false,
} // Initialize with default values
isDirty = true
}
Expand All @@ -130,6 +133,12 @@ export class ProviderSettingsManager {
isDirty = true
}

if (!providerProfiles.migrations.manualThinkingBudgetMigrated) {
await this.migrateManualThinkingBudget(providerProfiles)
providerProfiles.migrations.manualThinkingBudgetMigrated = true
isDirty = true
}

if (isDirty) {
await this.store(providerProfiles)
}
Expand Down Expand Up @@ -223,6 +232,26 @@ export class ProviderSettingsManager {
}
}

private async migrateManualThinkingBudget(providerProfiles: ProviderProfiles) {
try {
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
// For existing users who have modelMaxThinkingTokens set, enable manual control
// This maintains backward compatibility - if they were manually setting thinking tokens,
// they should continue to have manual control enabled
if (
apiConfig.modelMaxThinkingTokens !== undefined &&
apiConfig.manualThinkingBudgetEnabled === undefined
) {
apiConfig.manualThinkingBudgetEnabled = true
}
// For new users or existing users without thinking tokens set,
// default to false (automatic mode) - this is handled by the UI component's default logic
}
} catch (error) {
console.error(`[MigrateManualThinkingBudget] Failed to migrate manual thinking budget settings:`, error)
}
}

/**
* List all available configs with metadata.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ type ProviderSettings = {
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down Expand Up @@ -638,6 +639,7 @@ type IpcMessage =
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down Expand Up @@ -1115,6 +1117,7 @@ type TaskCommand =
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down
3 changes: 3 additions & 0 deletions src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ type ProviderSettings = {
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down Expand Up @@ -652,6 +653,7 @@ type IpcMessage =
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down Expand Up @@ -1131,6 +1133,7 @@ type TaskCommand =
rateLimitSeconds?: number | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
manualThinkingBudgetEnabled?: boolean | undefined
apiModelId?: string | undefined
apiKey?: string | undefined
anthropicBaseUrl?: string | undefined
Expand Down
2 changes: 2 additions & 0 deletions src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ const baseProviderSettingsSchema = z.object({
// Claude 3.7 Sonnet Thinking
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
manualThinkingBudgetEnabled: z.boolean().optional(),
})

// Several of the providers share common model config properties.
Expand Down Expand Up @@ -699,6 +700,7 @@ const providerSettingsRecord: ProviderSettingsRecord = {
// Claude 3.7 Sonnet Thinking
modelMaxTokens: undefined,
modelMaxThinkingTokens: undefined,
manualThinkingBudgetEnabled: undefined,
// Generic
includeMaxTokens: undefined,
reasoningEffort: undefined,
Expand Down
5 changes: 5 additions & 0 deletions webview-ui/.changeset/three-mammals-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": minor
---

Add setting for manual thinking budget/thinking token limit on supported providers & models
69 changes: 57 additions & 12 deletions webview-ui/src/components/settings/ThinkingBudget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"

import { Slider } from "@/components/ui"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"

import { ProviderSettings, ModelInfo } from "@roo/shared/api"

Expand All @@ -18,6 +19,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
const { t } = useAppTranslation()

const isThinkingModel = !!modelInfo && !!modelInfo.thinking && !!modelInfo.maxTokens
const supportsThinkingBudgetControl = isThinkingModel && modelInfo.maxThinkingTokens

const customMaxOutputTokens = apiConfiguration.modelMaxTokens || DEFAULT_MAX_OUTPUT_TOKENS
const customMaxThinkingTokens = apiConfiguration.modelMaxThinkingTokens || DEFAULT_MAX_THINKING_TOKENS
Expand All @@ -32,10 +34,35 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
// to the custom max output tokens being reduced then we need to shrink it
// appropriately.
useEffect(() => {
if (isThinkingModel && customMaxThinkingTokens > modelMaxThinkingTokens) {
if (
isThinkingModel &&
apiConfiguration.manualThinkingBudgetEnabled &&
customMaxThinkingTokens > modelMaxThinkingTokens
) {
setApiConfigurationField("modelMaxThinkingTokens", modelMaxThinkingTokens)
}
}, [isThinkingModel, customMaxThinkingTokens, modelMaxThinkingTokens, setApiConfigurationField])
}, [
isThinkingModel,
apiConfiguration.manualThinkingBudgetEnabled,
customMaxThinkingTokens,
modelMaxThinkingTokens,
setApiConfigurationField,
])

// Handler for toggling manual thinking budget control
const handleManualThinkingBudgetToggle = (event: any) => {
const enabled = event.target.checked
setApiConfigurationField("manualThinkingBudgetEnabled", enabled)

if (enabled) {
// Enable manual control - set to default value if not already set
if (apiConfiguration.modelMaxThinkingTokens === undefined) {
setApiConfigurationField("modelMaxThinkingTokens", DEFAULT_MAX_THINKING_TOKENS)
}
} else {
setApiConfigurationField("modelMaxThinkingTokens", undefined)
}
}

return isThinkingModel ? (
<>
Expand All @@ -53,17 +80,35 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
</div>
</div>
<div className="flex flex-col gap-1">
<div className="font-medium">{t("settings:thinkingBudget.maxThinkingTokens")}</div>
<div className="flex items-center gap-1" data-testid="thinking-budget">
<Slider
min={1024}
max={modelMaxThinkingTokens}
step={1024}
value={[customMaxThinkingTokens]}
onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
/>
<div className="w-12 text-sm text-center">{customMaxThinkingTokens}</div>
<VSCodeCheckbox
checked={apiConfiguration.manualThinkingBudgetEnabled}
onChange={handleManualThinkingBudgetToggle}
disabled={!supportsThinkingBudgetControl}
data-testid="manual-thinking-budget-checkbox">
<span className="font-medium">{t("settings:thinkingBudget.manualControl")}</span>
</VSCodeCheckbox>
<div className="text-xs text-vscode-descriptionForeground">
{!supportsThinkingBudgetControl
? t("settings:thinkingBudget.notSupportedDescription")
: apiConfiguration.manualThinkingBudgetEnabled
? t("settings:thinkingBudget.manualControlDescription")
: t("settings:thinkingBudget.autoControlDescription")}
</div>
{supportsThinkingBudgetControl && apiConfiguration.manualThinkingBudgetEnabled && (
<div className="flex flex-col gap-1">
<div className="font-medium">{t("settings:thinkingBudget.maxThinkingTokens")}</div>
<div className="flex items-center gap-1" data-testid="thinking-budget">
<Slider
min={1024}
max={modelMaxThinkingTokens}
step={1024}
value={[customMaxThinkingTokens]}
onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
/>
<div className="w-12 text-sm text-center">{customMaxThinkingTokens}</div>
</div>
</div>
)}
</div>
</>
) : null
Expand Down
Loading