mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Sonnet, Give Me a Reason (#1961)
* wip * added slider for setting reasoning budget tokens * refactor out generic slider component; improve styling; add debounce * added setting validation * styling and adding reasoning level * changeset * make change to trigger test rerun * revert useless comment
This commit is contained in:
parent
560cefecca
commit
df7b458229
10 changed files with 407 additions and 1 deletions
5
.changeset/slimy-keys-add.md
Normal file
5
.changeset/slimy-keys-add.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added thinking tokens budget slider for Sonnet 3.7 with Anthropic
|
||||
|
|
@ -211,6 +211,11 @@
|
|||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Controls whether the MCP Marketplace is enabled."
|
||||
},
|
||||
"cline.modelSettings.anthropic.thinkingBudgetTokens": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Controls the token budget for Claude's thinking capability. Set to 0 to disable thinking. When enabled, must be ≥1024 and less than the model's max token output."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export class AnthropicHandler implements ApiHandler {
|
|||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0 ? true : false
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
const modelId = model.id
|
||||
|
|
@ -41,8 +43,11 @@ export class AnthropicHandler implements ApiHandler {
|
|||
stream = await this.client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
// "Thinking isn’t compatible with temperature, top_p, or top_k modifications as well as forced tool use."
|
||||
// (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
|
||||
temperature: reasoningOn ? 1 : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
|
|
@ -148,6 +153,20 @@ export class AnthropicHandler implements ApiHandler {
|
|||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.content_block.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
// Handle redacted thinking blocks - we still mark it as reasoning
|
||||
// but note that the content is encrypted
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "text":
|
||||
// we may receive multiple text blocks, in which case just insert a line break between them
|
||||
if (chunk.index > 0) {
|
||||
|
|
@ -165,12 +184,22 @@ export class AnthropicHandler implements ApiHandler {
|
|||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "thinking_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
case "signature_delta":
|
||||
// We don't need to do anything with the signature in the client
|
||||
// It's used when sending the thinking block back to the API
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { getNonce } from "./getNonce"
|
|||
import { getUri } from "./getUri"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { validateThinkingBudget } from "../../utils/validation"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
|
|
@ -261,6 +262,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
// Update state when marketplace tab setting changes
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
if (e && e.affectsConfiguration("cline.modelSettings.anthropic.thinkingBudgetTokens")) {
|
||||
const config = vscode.workspace.getConfiguration("cline.modelSettings.anthropic")
|
||||
const thinkingBudget = config.get<number>("thinkingBudgetTokens", 0)
|
||||
|
||||
const validatedValue = validateThinkingBudget(thinkingBudget)
|
||||
|
||||
// Only update if the value changed
|
||||
if (validatedValue !== thinkingBudget) {
|
||||
await config.update("thinkingBudgetTokens", validatedValue, true)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
|
|
@ -976,6 +990,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
break
|
||||
}
|
||||
case "updateThinkingBudgetTokens": {
|
||||
if (message.number !== undefined) {
|
||||
const validatedValue = validateThinkingBudget(message.number)
|
||||
|
||||
const config = vscode.workspace.getConfiguration("cline.modelSettings.anthropic")
|
||||
await config.update("thinkingBudgetTokens", validatedValue, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "openExtensionSettings": {
|
||||
const settingsFilter = message.text || ""
|
||||
await vscode.commands.executeCommand(
|
||||
|
|
@ -2042,6 +2065,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
|||
.getConfiguration("cline.modelSettings.o3Mini")
|
||||
.get("reasoningEffort", "medium")
|
||||
|
||||
const thinkingBudgetTokens = vscode.workspace
|
||||
.getConfiguration("cline.modelSettings.anthropic")
|
||||
.get("thinkingBudgetTokens", 0)
|
||||
|
||||
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
|
||||
|
||||
return {
|
||||
|
|
@ -2084,6 +2111,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
|||
openRouterModelInfo,
|
||||
vsCodeLmModelSelector,
|
||||
o3MiniReasoningEffort,
|
||||
thinkingBudgetTokens,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export interface WebviewMessage {
|
|||
| "updateMcpTimeout"
|
||||
| "fetchOpenGraphData"
|
||||
| "checkIsImageUrl"
|
||||
| "updateThinkingBudgetTokens"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface ApiHandlerOptions {
|
|||
o3MiniReasoningEffort?: string
|
||||
qwenApiLine?: string
|
||||
xaiApiKey?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
|
|
|
|||
36
src/utils/validation.ts
Normal file
36
src/utils/validation.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { anthropicModels } from "../shared/api"
|
||||
|
||||
/**
|
||||
* Validates the thinking budget token value according to the specified rules:
|
||||
* - If disabled (0), return as is
|
||||
* - If enabled but less than minimum (1024), set to minimum
|
||||
* - If greater than or equal to max tokens, set to max tokens - 1
|
||||
* - Otherwise, return the original value
|
||||
*
|
||||
* @param value The thinking budget token value to validate
|
||||
* @param maxTokens The maximum tokens for the current model
|
||||
* @returns The validated thinking budget token value
|
||||
*/
|
||||
export function validateThinkingBudget(
|
||||
value: number,
|
||||
maxTokens: number = anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
|
||||
): number {
|
||||
// If disabled (0), return as is
|
||||
if (value === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If enabled but less than minimum, set to minimum
|
||||
if (value > 0 && value < 1024) {
|
||||
return 1024
|
||||
}
|
||||
|
||||
// If greater than or equal to max allowed tokens (80% of max tokens), cap at that value
|
||||
const maxAllowedTokens = Math.floor(maxTokens * 0.8)
|
||||
if (value >= maxAllowedTokens) {
|
||||
return maxAllowedTokens
|
||||
}
|
||||
|
||||
// Otherwise, return the original value
|
||||
return value
|
||||
}
|
||||
203
webview-ui/src/components/common/cline-ui/ClineSlider.tsx
Normal file
203
webview-ui/src/components/common/cline-ui/ClineSlider.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { memo, useEffect, useState, useMemo } from "react"
|
||||
import debounce from "debounce"
|
||||
|
||||
interface ClineSliderProps {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
onChange: (value: number) => void
|
||||
onChangeEnd?: (value: number) => void
|
||||
description?: string
|
||||
validateValue?: (value: number) => number
|
||||
dynamicColor?: boolean
|
||||
secondaryLabel?: string
|
||||
getSecondaryLabel?: (value: number, min: number, max: number) => string | JSX.Element
|
||||
}
|
||||
|
||||
// Constants
|
||||
const THUMB_SIZE = 24
|
||||
const DEBOUNCE_DELAY = 300
|
||||
|
||||
const ClineSlider = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
onChange,
|
||||
onChangeEnd,
|
||||
description,
|
||||
validateValue,
|
||||
dynamicColor = false,
|
||||
secondaryLabel,
|
||||
getSecondaryLabel,
|
||||
}: ClineSliderProps) => {
|
||||
// State
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
// Update local value when prop value changes (if not dragging)
|
||||
useEffect(() => {
|
||||
if (!isDragging) {
|
||||
setLocalValue(value)
|
||||
}
|
||||
}, [value, isDragging])
|
||||
|
||||
// Create debounced onChange handler
|
||||
const debouncedOnChange = useMemo(() => debounce((val: number) => onChange(val), DEBOUNCE_DELAY), [onChange])
|
||||
|
||||
// Clear debounce on unmount
|
||||
useEffect(() => () => debouncedOnChange.clear(), [debouncedOnChange])
|
||||
|
||||
// Event handlers
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseInt(e.target.value, 10)
|
||||
setLocalValue(newValue)
|
||||
setIsDragging(true)
|
||||
debouncedOnChange(newValue)
|
||||
}
|
||||
|
||||
const handleSlideEnd = () => {
|
||||
if (onChangeEnd) {
|
||||
const finalValue = validateValue ? validateValue(localValue) : localValue
|
||||
setLocalValue(finalValue)
|
||||
onChangeEnd(finalValue)
|
||||
}
|
||||
setTimeout(() => setIsDragging(false), 50)
|
||||
}
|
||||
|
||||
// Calculate percentage for dynamic color
|
||||
const percentage = (localValue - min) / (max - min)
|
||||
const intensity = dynamicColor ? percentage : 0
|
||||
|
||||
// Initialize styles once
|
||||
useEffect(() => {
|
||||
if (document.getElementById("cline-slider-styles")) return
|
||||
|
||||
const styleElement = document.createElement("style")
|
||||
styleElement.id = "cline-slider-styles"
|
||||
styleElement.innerHTML = `
|
||||
.cline-slider {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
appearance: none;
|
||||
background-color: var(--track-color, var(--vscode-scrollbarSlider-background));
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cline-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: var(--thumb-size, 24px);
|
||||
height: var(--thumb-size, 24px);
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--thumb-color, var(--vscode-button-background));
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cline-slider:focus { outline: none; }
|
||||
|
||||
.cline-slider:focus::-webkit-slider-thumb,
|
||||
.cline-slider:hover::-webkit-slider-thumb {
|
||||
background: white;
|
||||
border-color: var(--thumb-color, var(--vscode-button-hoverBackground));
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.cline-slider:active::-webkit-slider-thumb {
|
||||
background: white;
|
||||
border-color: var(--thumb-color, var(--vscode-button-hoverBackground));
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleElement)
|
||||
}, [])
|
||||
|
||||
// Common styles
|
||||
const containerStyle: React.CSSProperties = { marginTop: "10px" }
|
||||
const labelContainerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "10px",
|
||||
flexWrap: "wrap",
|
||||
gap: "12px",
|
||||
}
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontWeight: 500,
|
||||
display: "block",
|
||||
marginRight: "auto",
|
||||
}
|
||||
const valueStyle: React.CSSProperties = {
|
||||
color: "var(--vscode-button-foreground)",
|
||||
backgroundColor: "var(--vscode-button-background)",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
}
|
||||
const secondaryLabelStyle: React.CSSProperties = {
|
||||
fontWeight: 500,
|
||||
textAlign: "right",
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
}
|
||||
const descriptionStyle: React.CSSProperties = {
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}
|
||||
|
||||
// Input style with dynamic color if enabled
|
||||
const inputStyle = {
|
||||
marginTop: "5px",
|
||||
"--thumb-size": `${THUMB_SIZE}px`,
|
||||
...(dynamicColor
|
||||
? {
|
||||
"--thumb-color": `var(--vscode-button-background)`,
|
||||
"--track-color": `var(--vscode-button-background)`,
|
||||
opacity: 0.5 + intensity * 0.5,
|
||||
}
|
||||
: {}),
|
||||
} as React.CSSProperties
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={labelContainerStyle}>
|
||||
<label htmlFor={id} style={labelStyle}>
|
||||
<span style={{ color: "var(--vscode-editor-foreground)" }}>{label}:</span>{" "}
|
||||
<span style={valueStyle}>{localValue}</span>
|
||||
</label>
|
||||
{(secondaryLabel || getSecondaryLabel) && (
|
||||
<div style={secondaryLabelStyle}>
|
||||
{secondaryLabel || (getSecondaryLabel && getSecondaryLabel(localValue, min, max))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
id={id}
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onMouseUp={handleSlideEnd}
|
||||
onTouchEnd={handleSlideEnd}
|
||||
className="cline-slider"
|
||||
style={inputStyle}
|
||||
/>
|
||||
{description && <p style={descriptionStyle}>{description}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ClineSlider)
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
import { useEvent, useInterval } from "react-use"
|
||||
import {
|
||||
ApiConfiguration,
|
||||
|
|
@ -1186,6 +1187,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
|||
{selectedProvider === "xai" && createDropdown(xaiModels)}
|
||||
</DropdownContainer>
|
||||
|
||||
{selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219" && (
|
||||
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
||||
)}
|
||||
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
|
|
|
|||
93
webview-ui/src/components/settings/ThinkingBudgetSlider.tsx
Normal file
93
webview-ui/src/components/settings/ThinkingBudgetSlider.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { memo } from "react"
|
||||
import { anthropicModels, ApiConfiguration } from "../../../../src/shared/api"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ClineSlider from "../common/cline-ui/ClineSlider"
|
||||
|
||||
interface ThinkingBudgetSliderProps {
|
||||
apiConfiguration: ApiConfiguration | undefined
|
||||
setApiConfiguration: (apiConfiguration: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
// Constants
|
||||
const MIN_VALID_TOKENS = 1024
|
||||
const MAX_PERCENTAGE = 0.8
|
||||
|
||||
const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration }: ThinkingBudgetSliderProps) => {
|
||||
// Calculate max tokens based on model
|
||||
const maxTokens = anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
|
||||
const maxSliderValue = Math.floor(maxTokens * MAX_PERCENTAGE)
|
||||
const currentValue = apiConfiguration?.thinkingBudgetTokens || 0
|
||||
|
||||
// Style constants for reasoning level display
|
||||
const labelStyle: React.CSSProperties = { color: "var(--vscode-editor-foreground)" }
|
||||
const valueStyle: React.CSSProperties = {
|
||||
color: "white",
|
||||
backgroundColor: "var(--vscode-button-background)",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
fontWeight: "bold",
|
||||
}
|
||||
|
||||
// Handlers
|
||||
const handleChange = (value: number) => {
|
||||
if (!apiConfiguration) return
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
thinkingBudgetTokens: value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeEnd = (value: number) => {
|
||||
if (!apiConfiguration) return
|
||||
const validValue = getValidValue(value)
|
||||
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
thinkingBudgetTokens: validValue,
|
||||
})
|
||||
|
||||
vscode.postMessage({
|
||||
type: "updateThinkingBudgetTokens",
|
||||
number: validValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const getValidValue = (value: number): number => (value === 0 ? 0 : Math.max(MIN_VALID_TOKENS, value))
|
||||
|
||||
const getReasoningLevel = (value: number, min: number, max: number): JSX.Element => {
|
||||
let levelText: string
|
||||
|
||||
if (value === 0) {
|
||||
levelText = "Off"
|
||||
} else {
|
||||
const percentage = (value - min) / (max - min)
|
||||
levelText = percentage <= 1 / 3 ? "Low" : percentage <= 2 / 3 ? "Medium" : "High"
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span style={labelStyle}>Reasoning:</span> <span style={valueStyle}>{levelText}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ClineSlider
|
||||
id="thinking-budget-slider"
|
||||
label="Thinking tokens"
|
||||
value={currentValue}
|
||||
min={0}
|
||||
max={maxSliderValue}
|
||||
step={100}
|
||||
onChange={handleChange}
|
||||
onChangeEnd={handleChangeEnd}
|
||||
validateValue={getValidValue}
|
||||
dynamicColor={true}
|
||||
getSecondaryLabel={getReasoningLevel}
|
||||
description="Set to 0 to disable extended thinking. Higher values allow Claude to think more deeply before responding."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ThinkingBudgetSlider)
|
||||
Loading…
Add table
Reference in a new issue