Compare commits

...

12 Commits

Author SHA1 Message Date
celestial-vault e39854dec0 revert useless comment 2025-02-28 18:13:10 -08:00
celestial-vault b65de92199 make change to trigger test rerun 2025-02-28 18:12:38 -08:00
celestial-vault a201864014 Merge branch 'main' into add-sonnet-reasoning 2025-02-28 17:31:54 -08:00
celestial-vault db319e4d7b merge conflicts 2025-02-28 14:12:17 -08:00
celestial-vault c0ab2a2553 merge conflicts 2025-02-25 15:45:00 -08:00
celestial-vault 17202a1b9f changeset 2025-02-25 15:43:38 -08:00
celestial-vault 917117b4d4 styling and adding reasoning level 2025-02-25 15:30:12 -08:00
celestial-vault 5c12815ccc added setting validation 2025-02-25 14:08:51 -08:00
celestial-vault 211a8af047 refactor out generic slider component; improve styling; add debounce 2025-02-25 10:08:44 -08:00
celestial-vault 56bbd6dab6 added slider for setting reasoning budget tokens 2025-02-24 18:41:01 -08:00
celestial-vault 936b6dc9c2 merge conflicts 2025-02-24 17:23:38 -08:00
celestial-vault 6240fcebc4 wip 2025-02-24 16:09:32 -08:00
10 changed files with 407 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added thinking tokens budget slider for Sonnet 3.7 with Anthropic
+5
View File
@@ -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."
}
}
}
+30 -1
View File
@@ -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 isnt 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":
+28
View File
@@ -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,
@@ -973,6 +987,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(
@@ -2039,6 +2062,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 {
@@ -2081,6 +2108,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
openRouterModelInfo,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
+1
View File
@@ -57,6 +57,7 @@ export interface WebviewMessage {
| "updateMcpTimeout"
| "fetchOpenGraphData"
| "checkIsImageUrl"
| "updateThinkingBudgetTokens"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
+1
View File
@@ -59,6 +59,7 @@ export interface ApiHandlerOptions {
o3MiniReasoningEffort?: string
qwenApiLine?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
+36
View 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
}
@@ -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}
@@ -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)