+
No model found.
@@ -85,13 +93,30 @@ export const ModelPicker = ({
- {selectedModelId && selectedModelInfo && (
-
+ {errorMessage ? (
+
+
+
+ Note: Roo Code uses complex prompts and works best
+ with Claude models. Less capable models may not work as expected.
+
+
+
+ ) : (
+ selectedModelId &&
+ selectedModelInfo && (
+
+ )
)}
The extension automatically fetches the latest list of models available on{" "}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 75ba11107c..ee032c3ee0 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -1,6 +1,6 @@
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { Dropdown, type DropdownOption } from "vscrui"
+import { Button, Dropdown, type DropdownOption } from "vscrui"
import {
AlertDialog,
@@ -14,7 +14,6 @@ import {
} from "@/components/ui"
import { vscode } from "../../utils/vscode"
-import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
import { ApiConfiguration } from "../../../../src/shared/api"
@@ -33,14 +32,13 @@ export interface SettingsViewRef {
const SettingsView = forwardRef(({ onDone }, ref) => {
const extensionState = useExtensionState()
- const [apiErrorMessage, setApiErrorMessage] = useState(undefined)
- const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined)
const [commandInput, setCommandInput] = useState("")
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
const [cachedState, setCachedState] = useState(extensionState)
const [isChangeDetected, setChangeDetected] = useState(false)
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
const confirmDialogHandler = useRef<() => void>()
+ const [errorMessage, setErrorMessage] = useState(undefined)
// TODO: Reduce WebviewMessage/ExtensionState complexity
const { currentApiConfigName } = extensionState
@@ -135,20 +133,9 @@ const SettingsView = forwardRef(({ onDone },
}
})
}, [])
-
+ const isSettingValid = !errorMessage
const handleSubmit = () => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
-
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
-
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
-
- if (!apiValidationResult && !modelIdValidationResult) {
+ if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -177,23 +164,6 @@ const SettingsView = forwardRef(({ onDone },
}
}
- useEffect(() => {
- setApiErrorMessage(undefined)
- setModelIdErrorMessage(undefined)
- }, [apiConfiguration])
-
- // Initial validation on mount
- useEffect(() => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
- }, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
-
const checkUnsaveChanges = useCallback(
(then: () => void) => {
if (isChangeDetected) {
@@ -287,13 +257,14 @@ const SettingsView = forwardRef(({ onDone },
justifyContent: "space-between",
gap: "6px",
}}>
-
+ disabled={!isChangeDetected || !isSettingValid}>
Save
-
+
(({ onDone },
uriScheme={extensionState.uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
- apiErrorMessage={apiErrorMessage}
- modelIdErrorMessage={modelIdErrorMessage}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 8f2d0dff89..73394bae10 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,6 +51,8 @@ describe("ApiOptions", () => {
render(
{}}
uriScheme={undefined}
apiConfiguration={{}}
setApiConfigurationField={() => {}}
@@ -69,4 +71,6 @@ describe("ApiOptions", () => {
renderApiOptions({ fromWelcomeView: true })
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
})
+
+ //TODO: More test cases needed
})
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 858d2622f3..5d880efc0b 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -42,6 +42,8 @@ const WelcomeView = () => {
apiConfiguration={apiConfiguration || {}}
uriScheme={uriScheme}
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
From 48975003afe593c975476acfd348ceb6110d7ced Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 06:50:11 +0800
Subject: [PATCH 18/38] Remove ModelInfo related exports from
ExtensionStateContext
---
.../src/context/ExtensionStateContext.tsx | 84 +------------------
1 file changed, 1 insertion(+), 83 deletions(-)
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index 3dca8d5f51..c2c4d181e4 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -1,18 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
-import {
- ApiConfiguration,
- ModelInfo,
- glamaDefaultModelId,
- glamaDefaultModelInfo,
- openRouterDefaultModelId,
- openRouterDefaultModelInfo,
- unboundDefaultModelId,
- unboundDefaultModelInfo,
- requestyDefaultModelId,
- requestyDefaultModelInfo,
-} from "../../../src/shared/api"
+import { ApiConfiguration } from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
@@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: any
- glamaModels: Record
- requestyModels: Record
- openRouterModels: Record
- unboundModels: Record
- openAiModels: string[]
mcpServers: McpServer[]
currentCheckpoint?: string
filePaths: string[]
@@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setRateLimitSeconds: (value: number) => void
setCurrentApiConfigName: (value: string) => void
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
- onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
mode: Mode
setMode: (value: Mode) => void
setCustomModePrompts: (value: CustomModePrompts) => void
@@ -124,21 +107,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [showWelcome, setShowWelcome] = useState(false)
const [theme, setTheme] = useState(undefined)
const [filePaths, setFilePaths] = useState([])
- const [glamaModels, setGlamaModels] = useState>({
- [glamaDefaultModelId]: glamaDefaultModelInfo,
- })
const [openedTabs, setOpenedTabs] = useState>([])
- const [openRouterModels, setOpenRouterModels] = useState>({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo,
- })
- const [unboundModels, setUnboundModels] = useState>({
- [unboundDefaultModelId]: unboundDefaultModelInfo,
- })
- const [requestyModels, setRequestyModels] = useState>({
- [requestyDefaultModelId]: requestyDefaultModelInfo,
- })
- const [openAiModels, setOpenAiModels] = useState([])
const [mcpServers, setMcpServers] = useState([])
const [currentCheckpoint, setCurrentCheckpoint] = useState()
@@ -146,18 +116,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
[],
)
-
- const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
- setState((currentState) => {
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentState.currentApiConfigName,
- apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig },
- })
- return currentState // No state update needed
- })
- }, [])
-
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -202,40 +160,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
})
break
}
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(updatedModels)
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
@@ -264,11 +188,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
didHydrateState,
showWelcome,
theme,
- glamaModels,
- requestyModels,
- openRouterModels,
- openAiModels,
- unboundModels,
mcpServers,
currentCheckpoint,
filePaths,
@@ -316,7 +235,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
setListApiConfigMeta,
- onUpdateApiConfig,
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
From 05151ed2254e8698d4c380ceb8daa36e019f82aa Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Tue, 25 Feb 2025 18:30:37 -0500
Subject: [PATCH 19/38] Update package.json
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 84bec2645a..28045436e6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "roo-cline",
"displayName": "Roo Code (prev. Roo Cline)",
- "description": "An AI-powered autonomous coding agent that lives in your editor.",
+ "description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
"version": "3.7.4",
"icon": "assets/icons/rocket.png",
From e56908f6c1b800bd2a2b4edd85f725c1a0055920 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:48:20 -0800
Subject: [PATCH 20/38] Thinking settings tweaks
---
src/api/providers/anthropic.ts | 23 +++++----
src/api/providers/openrouter.ts | 12 ++++-
src/shared/api.ts | 14 +++++-
.../src/components/settings/ApiOptions.tsx | 50 ++++++-------------
4 files changed, 51 insertions(+), 48 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 2d1f07f833..c907350607 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -14,8 +14,6 @@ import { ApiStream } from "../transform/stream"
const ANTHROPIC_DEFAULT_TEMPERATURE = 0
-const THINKING_MODELS = ["claude-3-7-sonnet-20250219"]
-
export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private client: Anthropic
@@ -32,16 +30,19 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- const modelId = this.getModel().id
- const maxTokens = this.getModel().info.maxTokens || 8192
+ let { id: modelId, info: modelInfo } = this.getModel()
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
- if (THINKING_MODELS.includes(modelId)) {
- thinking = this.options.anthropicThinking
- ? { type: "enabled", budget_tokens: this.options.anthropicThinking }
- : { type: "disabled" }
-
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ if (modelId === "claude-3-7-sonnet-20250219:thinking") {
+ // The `:thinking` variant is a virtual identifier for the
+ // `claude-3-7-sonnet-20250219` model with a thinking budget.
+ // We can handle this more elegantly in the future.
+ modelId = "claude-3-7-sonnet-20250219"
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -114,8 +115,8 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
default: {
stream = (await this.client.messages.create({
model: modelId,
- max_tokens: this.getModel().info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
+ max_tokens: maxTokens,
+ temperature,
system: [{ text: systemPrompt, type: "text" }],
messages,
// tools,
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69c55b8e71..6bf4fa4a8c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -1,4 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import axios from "axios"
import OpenAI from "openai"
import delay from "delay"
@@ -17,6 +18,7 @@ const OPENROUTER_DEFAULT_TEMPERATURE = 0
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
+ thinking?: BetaThinkingConfigParam
}
// Add custom interface for OpenRouter usage chunk.
@@ -57,7 +59,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (true) {
- case this.getModel().id.startsWith("anthropic/"):
+ case modelId.startsWith("anthropic/"):
openAiMessages[0] = {
role: "system",
content: [
@@ -108,8 +110,13 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
let temperature = this.options.modelTemperature ?? defaultTemperature
+ const maxTokens = modelInfo.maxTokens
+ const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
// Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -118,8 +125,9 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: modelInfo.maxTokens,
+ max_tokens: maxTokens,
temperature,
+ thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
messages: openAiMessages,
stream: true,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 63707e52b4..5d4b8b120d 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -103,7 +103,7 @@ export const THINKING_BUDGET = {
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicModels = {
- "claude-3-7-sonnet-20250219": {
+ "claude-3-7-sonnet-20250219:thinking": {
maxTokens: 16384,
contextWindow: 200_000,
supportsImages: true,
@@ -115,6 +115,18 @@ export const anthropicModels = {
cacheReadsPrice: 0.3, // $0.30 per million tokens
thinking: true,
},
+ "claude-3-7-sonnet-20250219": {
+ maxTokens: 16384,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million input tokens
+ outputPrice: 15.0, // $15 per million output tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ thinking: false,
+ },
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200_000,
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 9d17cae4fa..73dc4fd41f 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -73,7 +73,7 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking
+ const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
@@ -1272,39 +1272,21 @@ const ApiOptions = ({
)}
{selectedModelInfo && selectedModelInfo.thinking && (
-
-
- setApiConfigurationField(
- "anthropicThinking",
- checked
- ? Math.min(
- THINKING_BUDGET.default,
- selectedModelInfo.maxTokens ?? THINKING_BUDGET.default,
- )
- : undefined,
- )
- }>
- Thinking?
-
- {anthropicThinkingBudget && (
- <>
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- >
- )}
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {anthropicThinkingBudget}
+
+
+ Number of tokens Claude is allowed to use for its internal reasoning process.
+
)}
From 8971e47b96ec7ae3a6de4ec5a95a2acab4cba1b7 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:53:32 -0800
Subject: [PATCH 21/38] Add changeset
---
.changeset/swift-kings-attack.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
new file mode 100644
index 0000000000..8a8a425611
--- /dev/null
+++ b/.changeset/swift-kings-attack.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Pass "thinking" params to OpenRouter
From 33fd3bd6b35caafce66fcd53b9070f60279fcc0d Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 16:26:40 -0800
Subject: [PATCH 22/38] Fix budgetTokens
---
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 8 +++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index c907350607..ad58a1cf6b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -32,7 +32,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, info: modelInfo } = this.getModel()
const maxTokens = modelInfo.maxTokens || 8192
- const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
@@ -42,6 +41,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// `claude-3-7-sonnet-20250219` model with a thinking budget.
// We can handle this more elegantly in the future.
modelId = "claude-3-7-sonnet-20250219"
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 6bf4fa4a8c..0a9488e816 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -109,13 +109,11 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}
let temperature = this.options.modelTemperature ?? defaultTemperature
-
- const maxTokens = modelInfo.maxTokens
- const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
let thinking: BetaThinkingConfigParam | undefined = undefined
- // Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -125,7 +123,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: maxTokens,
+ max_tokens: modelInfo.maxTokens,
temperature,
thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
From f3d02030ac47420d8a9735c0d02f70e561475dae Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 08:37:28 +0800
Subject: [PATCH 23/38] Fix: Input/output prices should be parsed using
parseFloat
---
webview-ui/src/components/settings/ApiOptions.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 73dc4fd41f..bfcf93256e 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -900,7 +900,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ??
openAiModelInfoSaneDefaults),
@@ -945,7 +945,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
From 41e75bc9890036674cd31ce2d9da23fcd5127956 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:02:43 -0800
Subject: [PATCH 24/38] Model picker fixes
---
.changeset/real-ties-destroy.md | 5 +
src/api/providers/requesty.ts | 36 +-
src/core/webview/ClineProvider.ts | 22 +-
src/shared/ExtensionMessage.ts | 13 +-
src/shared/WebviewMessage.ts | 5 +-
webview-ui/package-lock.json | 2 +
.../components/settings/ApiErrorMessage.tsx | 18 +-
.../src/components/settings/ApiOptions.tsx | 445 ++++++++----------
.../src/components/settings/ModelPicker.tsx | 91 ++--
.../src/components/settings/SettingsView.tsx | 48 +-
.../components/settings/ThinkingBudget.tsx | 29 ++
webview-ui/src/components/ui/alert-dialog.tsx | 151 +++---
webview-ui/src/components/ui/dialog.tsx | 158 ++++---
.../src/components/welcome/WelcomeView.tsx | 8 +-
webview-ui/src/utils/validate.ts | 254 ++++++----
15 files changed, 664 insertions(+), 621 deletions(-)
create mode 100644 .changeset/real-ties-destroy.md
create mode 100644 webview-ui/src/components/settings/ThinkingBudget.tsx
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
new file mode 100644
index 0000000000..a2e9ba8eb0
--- /dev/null
+++ b/.changeset/real-ties-destroy.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Fix model picker
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 2151a7172d..5e570ca2a2 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler {
}
}
-export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
+export async function getRequestyModels() {
const models: Record = {}
- if (!apiKey) {
- return models
- }
-
try {
- const config: Record = {}
- config["headers"] = { Authorization: `Bearer ${apiKey}` }
-
- const response = await axios.get("https://router.requesty.ai/v1/models", config)
+ const response = await axios.get("https://router.requesty.ai/v1/models")
const rawModels = response.data.data
for (const rawModel of rawModels) {
+ // {
+ // id: "anthropic/claude-3-5-sonnet-20240620",
+ // object: "model",
+ // created: 1740552655,
+ // owned_by: "system",
+ // input_price: 0.0000028,
+ // caching_price: 0.00000375,
+ // cached_price: 3e-7,
+ // output_price: 0.000015,
+ // max_output_tokens: 8192,
+ // context_window: 200000,
+ // supports_caching: true,
+ // description:
+ // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.",
+ // }
+
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_output_tokens,
contextWindow: rawModel.context_window,
- supportsImages: rawModel.support_image,
- supportsComputerUse: rawModel.support_computer_use,
supportsPromptCache: rawModel.supports_caching,
inputPrice: parseApiPrice(rawModel.input_price),
outputPrice: parseApiPrice(rawModel.output_price),
@@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
modelInfo.maxTokens = 16384
break
+ case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
+ modelInfo.maxTokens = 8192
+ break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 118bbddcf5..bc6f457868 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
- const requestyApiKey = await this.getSecret("requestyApiKey")
-
- getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => {
+ getRequestyModels().then(async (requestyModels) => {
if (Object.keys(requestyModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
@@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "refreshRequestyModels":
- if (message?.values?.apiKey) {
- const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey })
+ const requestyModels = await getRequestyModels()
- if (Object.keys(requestyModels).length > 0) {
- const cacheDir = await this.ensureCacheDirectoryExists()
- await fs.writeFile(
- path.join(cacheDir, GlobalFileNames.requestyModels),
- JSON.stringify(requestyModels),
- )
- await this.postMessageToWebview({ type: "requestyModels", requestyModels })
- }
+ if (Object.keys(requestyModels).length > 0) {
+ const cacheDir = await this.ensureCacheDirectoryExists()
+ await fs.writeFile(
+ path.join(cacheDir, GlobalFileNames.requestyModels),
+ JSON.stringify(requestyModels),
+ )
+ await this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
break
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 8f64a9ba05..e87edffed1 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -27,10 +27,11 @@ export interface ExtensionMessage {
| "workspaceUpdated"
| "invoke"
| "partialMessage"
- | "glamaModels"
| "openRouterModels"
- | "openAiModels"
+ | "glamaModels"
+ | "unboundModels"
| "requestyModels"
+ | "openAiModels"
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
@@ -43,8 +44,6 @@ export interface ExtensionMessage {
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
- | "unboundModels"
- | "refreshUnboundModels"
| "currentCheckpointUpdated"
text?: string
action?:
@@ -67,11 +66,11 @@ export interface ExtensionMessage {
path?: string
}>
partialMessage?: ClineMessage
- glamaModels?: Record
- requestyModels?: Record
openRouterModels?: Record
- openAiModels?: string[]
+ glamaModels?: Record
unboundModels?: Record
+ requestyModels?: Record
+ openAiModels?: string[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 106e6d243b..fde7442cc1 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -40,11 +40,11 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
- | "refreshGlamaModels"
| "refreshOpenRouterModels"
- | "refreshOpenAiModels"
+ | "refreshGlamaModels"
| "refreshUnboundModels"
| "refreshRequestyModels"
+ | "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
@@ -71,7 +71,6 @@ export interface WebviewMessage {
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
- | "refreshGlamaModels"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 1d64f934dc..22564d01a6 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -3674,6 +3674,7 @@
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
@@ -4719,6 +4720,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx
index 4b419957b6..06764a1bfa 100644
--- a/webview-ui/src/components/settings/ApiErrorMessage.tsx
+++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx
@@ -4,13 +4,13 @@ interface ApiErrorMessageProps {
errorMessage: string | undefined
children?: React.ReactNode
}
-const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
- return (
-
-
- {errorMessage}
- {children}
+
+export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => (
+
+
- )
-}
-export default ApiErrorMessage
+ {children}
+
+)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 107f2a483a..c30035cef0 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import * as vscodemodels from "vscode"
-import { Slider } from "@/components/ui"
-
import {
ApiConfiguration,
ModelInfo,
@@ -33,7 +31,6 @@ import {
unboundDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
- THINKING_BUDGET,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
-import ApiErrorMessage from "./ApiErrorMessage"
+import { ApiErrorMessage } from "./ApiErrorMessage"
+import { ThinkingBudget } from "./ThinkingBudget"
+
+const modelsByProvider: Record
> = {
+ anthropic: anthropicModels,
+ bedrock: bedrockModels,
+ vertex: vertexModels,
+ gemini: geminiModels,
+ "openai-native": openAiNativeModels,
+ deepseek: deepSeekModels,
+ mistral: mistralModels,
+}
interface ApiOptionsProps {
uriScheme: string | undefined
@@ -66,18 +74,23 @@ const ApiOptions = ({
const [ollamaModels, setOllamaModels] = useState([])
const [lmStudioModels, setLmStudioModels] = useState([])
const [vsCodeLmModels, setVsCodeLmModels] = useState([])
+
const [openRouterModels, setOpenRouterModels] = useState>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
+
const [glamaModels, setGlamaModels] = useState>({
[glamaDefaultModelId]: glamaDefaultModelInfo,
})
+
const [unboundModels, setUnboundModels] = useState>({
[unboundDefaultModelId]: unboundDefaultModelInfo,
})
+
const [requestyModels, setRequestyModels] = useState>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
+
const [openAiModels, setOpenAiModels] = useState | null>(null)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
@@ -85,8 +98,6 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
-
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
const dropdownEventTransform = (event: DropdownOption | string | undefined) =>
@@ -103,62 +114,87 @@ const ApiOptions = ({
[setApiConfigurationField],
)
- const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
- return normalizeApiConfiguration(apiConfiguration)
- }, [apiConfiguration])
+ const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(
+ () => normalizeApiConfiguration(apiConfiguration),
+ [apiConfiguration],
+ )
- // Pull ollama/lmstudio models
- // Debounced model updates, only executed 250ms after the user stops typing
+ // Debounced refresh model updates, only executed 250ms after the user
+ // stops typing.
useDebounce(
() => {
- if (selectedProvider === "ollama") {
+ if (selectedProvider === "openrouter") {
+ vscode.postMessage({ type: "refreshOpenRouterModels" })
+ } else if (selectedProvider === "glama") {
+ vscode.postMessage({ type: "refreshGlamaModels" })
+ } else if (selectedProvider === "unbound") {
+ vscode.postMessage({ type: "refreshUnboundModels" })
+ } else if (selectedProvider === "requesty") {
+ vscode.postMessage({
+ type: "refreshRequestyModels",
+ values: { apiKey: apiConfiguration?.requestyApiKey },
+ })
+ } else if (selectedProvider === "openai") {
+ vscode.postMessage({
+ type: "refreshOpenAiModels",
+ values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey },
+ })
+ } else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
- } else if (selectedProvider === "openai") {
- vscode.postMessage({
- type: "refreshOpenAiModels",
- values: {
- baseUrl: apiConfiguration?.openAiBaseUrl,
- apiKey: apiConfiguration?.openAiApiKey,
- },
- })
- } else if (selectedProvider === "openrouter") {
- vscode.postMessage({ type: "refreshOpenRouterModels", values: {} })
- } else if (selectedProvider === "glama") {
- vscode.postMessage({ type: "refreshGlamaModels", values: {} })
- } else if (selectedProvider === "requesty") {
- vscode.postMessage({
- type: "refreshRequestyModels",
- values: {
- apiKey: apiConfiguration?.requestyApiKey,
- },
- })
}
},
250,
[
selectedProvider,
- apiConfiguration?.ollamaBaseUrl,
- apiConfiguration?.lmStudioBaseUrl,
+ apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
- apiConfiguration?.requestyApiKey,
+ apiConfiguration?.ollamaBaseUrl,
+ apiConfiguration?.lmStudioBaseUrl,
],
)
useEffect(() => {
const apiValidationResult =
validateApiConfiguration(apiConfiguration) ||
- validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
- setErrorMessage(apiValidationResult)
- }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
+ validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels)
- const handleMessage = useCallback((event: MessageEvent) => {
+ setErrorMessage(apiValidationResult)
+ }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
+
+ const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
+
switch (message.type) {
+ case "openRouterModels": {
+ const updatedModels = message.openRouterModels ?? {}
+ setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "glamaModels": {
+ const updatedModels = message.glamaModels ?? {}
+ setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "unboundModels": {
+ const updatedModels = message.unboundModels ?? {}
+ setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "requestyModels": {
+ const updatedModels = message.requestyModels ?? {}
+ setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "openAiModels": {
+ const updatedModels = message.openAiModels ?? []
+ setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
+ break
+ }
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
@@ -177,72 +213,30 @@ const ApiOptions = ({
setVsCodeLmModels(newModels)
}
break
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
}
}, [])
- useEvent("message", handleMessage)
+ useEvent("message", onMessage)
- const createDropdown = (models: Record) => {
- const options: DropdownOption[] = [
- { value: "", label: "Select a model..." },
- ...Object.keys(models).map((modelId) => ({
- value: modelId,
- label: modelId,
- })),
- ]
-
- return (
- {
- setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
- }}
- style={{ width: "100%" }}
- options={options}
- />
- )
- }
+ const selectedProviderModelOptions: DropdownOption[] = useMemo(
+ () =>
+ modelsByProvider[selectedProvider]
+ ? [
+ { value: "", label: "Select a model..." },
+ ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({
+ value: modelId,
+ label: modelId,
+ })),
+ ]
+ : [],
+ [selectedProvider],
+ )
return (
-
+ {errorMessage &&
}
+
{selectedProvider === "anthropic" && (
- Anthropic API Key
+ Anthropic API Key
- Glama API Key
+ Glama API Key
{!apiConfiguration?.glamaApiKey && (
- Requesty API Key
+ Requesty API Key
- OpenAI API Key
+ OpenAI API Key
- Mistral API Key
+ Mistral API Key
- Codestral Base URL (Optional)
+ Codestral Base URL (Optional)
- OpenRouter API Key
+ OpenRouter API Key
{!apiConfiguration?.openRouterApiKey && (
@@ -530,7 +526,7 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name">
- AWS Profile Name
+ AWS Profile Name
) : (
<>
@@ -541,7 +537,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
- AWS Access Key
+ AWS Access Key
- AWS Secret Key
+ AWS Secret Key
- AWS Session Token
+ AWS Session Token
>
)}
- AWS Region
+ AWS Region
- Google Cloud Project ID
+ Google Cloud Project ID
- Google Cloud Region
+ Google Cloud Region
- {errorMessage && }
- Gemini API Key
+ Gemini API Key
- Base URL
+ Base URL
- API Key
+ API Key
)}
-
-
+
- Max Output Tokens
+ Max Output Tokens
-
Context Window Size
+
Context Window Size
-
Image Support
+
Image Support
- Computer Use
+ Computer Use
-
Input Price
+
Input Price
-
Output Price
+
Output Price
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
- {errorMessage && }
-
{lmStudioModels.length > 0 && (
{" "}
feature to use it with this extension.{" "}
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1154,7 +1141,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
- DeepSeek API Key
+ DeepSeek API Key
- Language Model
+ Language Model
{vsCodeLmModels.length > 0 ? (
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
{errorMessage && (
@@ -1284,7 +1271,7 @@ const ApiOptions = ({
quickstart guide.
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1299,7 +1286,7 @@ const ApiOptions = ({
type="password"
onChange={handleInputChange("unboundApiKey")}
placeholder="Enter API Key...">
- Unbound API Key
+ Unbound API Key
{!apiConfiguration?.unboundApiKey && (
This key is stored locally and only used to make API requests from this extension.
-
)}
- {selectedProvider === "glama" && (
-
- )}
-
{selectedProvider === "openrouter" && (
)}
+
+ {selectedProvider === "glama" && (
+
+ )}
+
+ {selectedProvider === "unbound" && (
+
+ )}
+
{selectedProvider === "requesty" && (
)}
- {selectedProvider !== "glama" &&
- selectedProvider !== "openrouter" &&
- selectedProvider !== "requesty" &&
- selectedProvider !== "openai" &&
- selectedProvider !== "ollama" &&
- selectedProvider !== "lmstudio" &&
- selectedProvider !== "unbound" && (
- <>
-
-
- Model
-
- {selectedProvider === "anthropic" && createDropdown(anthropicModels)}
- {selectedProvider === "bedrock" && createDropdown(bedrockModels)}
- {selectedProvider === "vertex" && createDropdown(vertexModels)}
- {selectedProvider === "gemini" && createDropdown(geminiModels)}
- {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
- {selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
- {selectedProvider === "mistral" && createDropdown(mistralModels)}
-
- {errorMessage && }
- 0 && (
+ <>
+
+
+ Model
+
+
{
+ setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
+ }}
+ options={selectedProviderModelOptions}
+ className="w-full"
/>
- >
- )}
-
- {selectedModelInfo && selectedModelInfo.thinking && (
-
-
Thinking Budget
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
+
+
+ >
)}
{!fromWelcomeView && (
@@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const getProviderData = (models: Record, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
+
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
@@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
+
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
+
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
@@ -1481,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
- case "glama":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
- selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
- }
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@@ -1495,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
+ case "glama":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
+ selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
+ }
+ case "unbound":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
+ selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
+ }
+ case "requesty":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
+ selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
+ }
case "openai":
return {
selectedProvider: provider,
@@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
- supportsImages: false, // VSCode LM API currently doesn't support images
+ supportsImages: false, // VSCode LM API currently doesn't support images.
},
}
- case "unbound":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
- selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
- }
- case "requesty":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
- selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
- }
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index fd62bfb97b..5a7737edd5 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -1,11 +1,13 @@
+import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { useMemo, useState, useCallback, useEffect } from "react"
+
+import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox"
+
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { normalizeApiConfiguration } from "./ApiOptions"
+import { ThinkingBudget } from "./ThinkingBudget"
import { ModelInfoView } from "./ModelInfoView"
-import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
-import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
-import ApiErrorMessage from "./ApiErrorMessage"
type ExtractType = NonNullable<
{ [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration]
@@ -14,24 +16,17 @@ type ExtractType = NonNullable<
type ModelIdKeys = NonNullable<
{ [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
>
-declare module "react" {
- interface CSSProperties {
- // Allow CSS variables
- [key: `--${string}`]: string | number
- }
-}
+
interface ModelPickerProps {
- defaultModelId?: string
+ defaultModelId: string
+ defaultModelInfo?: ModelInfo
models: Record | null
modelIdKey: ModelIdKeys
modelInfoKey: ExtractType
serviceName: string
serviceUrl: string
- recommendedModel: string
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
- defaultModelInfo?: ModelInfo
- errorMessage?: string
}
export const ModelPicker = ({
@@ -41,13 +36,12 @@ export const ModelPicker = ({
modelInfoKey,
serviceName,
serviceUrl,
- recommendedModel,
apiConfiguration,
setApiConfigurationField,
defaultModelInfo,
- errorMessage,
}: ModelPickerProps) => {
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
+ const isInitialized = useRef(false)
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
@@ -55,6 +49,7 @@ export const ModelPicker = ({
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
+
const onSelect = useCallback(
(modelId: string) => {
const modelInfo = models?.[modelId]
@@ -63,26 +58,23 @@ export const ModelPicker = ({
},
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)
+
+ const inputValue = apiConfiguration[modelIdKey]
+
useEffect(() => {
- if (apiConfiguration[modelIdKey] == null && defaultModelId) {
- onSelect(defaultModelId)
+ if (!inputValue && !isInitialized.current) {
+ const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId
+ setApiConfigurationField(modelIdKey, initialValue)
}
- }, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
+
+ isInitialized.current = true
+ }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId])
return (
<>
Model
-
-
+
+
No model found.
{modelIds.map((model) => (
@@ -92,31 +84,18 @@ export const ModelPicker = ({
))}
-
- {errorMessage ? (
-
-
-
- Note: Roo Code uses complex prompts and works best
- with Claude models. Less capable models may not work as expected.
-
-
-
- ) : (
- selectedModelId &&
- selectedModelInfo && (
-
- )
+
+ {selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
+
)}
The extension automatically fetches the latest list of models available on{" "}
@@ -124,7 +103,7 @@ export const ModelPicker = ({
{serviceName}.
If you're unsure which model to choose, Roo Code works best with{" "}
- onSelect(recommendedModel)}>{recommendedModel}.
+ onSelect(defaultModelId)}>{defaultModelId}.
You can also try searching "free" for no-cost options currently available.
>
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index ee032c3ee0..d3e65a99ea 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -66,21 +66,20 @@ const SettingsView = forwardRef(({ onDone },
terminalOutputLineLimit,
writeDelayMs,
} = cachedState
-
+
//Make sure apiConfiguration is initialized and managed by SettingsView
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
- // Update only when currentApiConfigName is changed
- // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration
+ // Update only when currentApiConfigName is changed.
+ // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
if (prevApiConfigName.current === currentApiConfigName) {
return
}
- setCachedState((prevCachedState) => ({
- ...prevCachedState,
- ...extensionState,
- }))
+
+ setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
+ // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
setChangeDetected(false)
}, [currentApiConfigName, extensionState, isChangeDetected])
@@ -90,11 +89,10 @@ const SettingsView = forwardRef(({ onDone },
if (prevState[field] === value) {
return prevState
}
+
+ // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- [field]: value,
- }
+ return { ...prevState, [field]: value }
})
},
[],
@@ -107,15 +105,10 @@ const SettingsView = forwardRef(({ onDone },
return prevState
}
+ // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- apiConfiguration: {
- ...prevState.apiConfiguration,
- [field]: value,
- },
- }
+ return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
})
},
[],
@@ -126,14 +119,19 @@ const SettingsView = forwardRef(({ onDone },
if (prevState.experiments?.[id] === enabled) {
return prevState
}
+
+ // console.log("setExperimentEnabled: setChangeDetected -> true")
setChangeDetected(true)
+
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
})
}, [])
+
const isSettingValid = !errorMessage
+
const handleSubmit = () => {
if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
@@ -160,6 +158,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "updateExperimental", values: experiments })
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
+ // console.log("handleSubmit: setChangeDetected -> false")
setChangeDetected(false)
}
}
@@ -176,13 +175,7 @@ const SettingsView = forwardRef(({ onDone },
[isChangeDetected],
)
- useImperativeHandle(
- ref,
- () => ({
- checkUnsaveChanges,
- }),
- [checkUnsaveChanges],
- )
+ useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
@@ -200,10 +193,7 @@ const SettingsView = forwardRef(({ onDone },
const newCommands = [...currentCommands, commandInput]
setCachedStateField("allowedCommands", newCommands)
setCommandInput("")
- vscode.postMessage({
- type: "allowedCommands",
- commands: newCommands,
- })
+ vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}
}
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
new file mode 100644
index 0000000000..efaa90dc39
--- /dev/null
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -0,0 +1,29 @@
+import { Slider } from "@/components/ui"
+
+import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+
+interface ThinkingBudgetProps {
+ apiConfiguration: ApiConfiguration
+ setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
+ modelInfo?: ModelInfo
+}
+
+export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+ const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
+
+ return modelInfo && modelInfo.thinking ? (
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {budget}
+
+
+ ) : null
+}
diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx
index 7530cae54d..82a25bf8f7 100644
--- a/webview-ui/src/components/ui/alert-dialog.tsx
+++ b/webview-ui/src/components/ui/alert-dialog.tsx
@@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
-const AlertDialog = AlertDialogPrimitive.Root
+function AlertDialog({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+function AlertDialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogPortal = AlertDialogPrimitive.Portal
+function AlertDialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
- ) {
+ return (
+
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+ )
+}
-const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
+function AlertDialogContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
-const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
+function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
+function AlertDialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+function AlertDialogAction({ className, ...props }: React.ComponentProps) {
+ return
+}
+
+function AlertDialogCancel({ className, ...props }: React.ComponentProps) {
+ return
+}
export {
AlertDialog,
diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx
index 11d5e2d3b0..ed3160f692 100644
--- a/webview-ui/src/components/ui/dialog.tsx
+++ b/webview-ui/src/components/ui/dialog.tsx
@@ -1,96 +1,108 @@
-"use client"
-
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { Cross2Icon } from "@radix-ui/react-icons"
+import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
-const Dialog = DialogPrimitive.Root
+function Dialog({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogTrigger = DialogPrimitive.Trigger
+function DialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogPortal = DialogPrimitive.Portal
+function DialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogClose = DialogPrimitive.Close
+function DialogClose({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
-
-const DialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
- ) {
+ return (
+
- {children}
-
-
- Close
-
-
-
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
+ {...props}
+ />
+ )
+}
-const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogHeader.displayName = "DialogHeader"
+function DialogContent({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
-const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogFooter.displayName = "DialogFooter"
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
+function DialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
export {
Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
DialogClose,
DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
}
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 5d880efc0b..ae674c895f 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -12,16 +12,14 @@ const WelcomeView = () => {
const handleSubmit = useCallback(() => {
const error = validateApiConfiguration(apiConfiguration)
+
if (error) {
setErrorMessage(error)
return
}
+
setErrorMessage(undefined)
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentApiConfigName,
- apiConfiguration,
- })
+ vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
}, [apiConfiguration, currentApiConfigName])
return (
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts
index 97c702637c..82af23ab49 100644
--- a/webview-ui/src/utils/validate.ts
+++ b/webview-ui/src/utils/validate.ts
@@ -1,74 +1,83 @@
-import { ApiConfiguration } from "../../../src/shared/api"
-import { ModelInfo } from "../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../src/shared/api"
+
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "anthropic":
- if (!apiConfiguration.apiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "glama":
- if (!apiConfiguration.glamaApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "bedrock":
- if (!apiConfiguration.awsRegion) {
- return "You must choose a region to use with AWS Bedrock."
- }
- break
- case "openrouter":
- if (!apiConfiguration.openRouterApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "vertex":
- if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
- return "You must provide a valid Google Cloud Project ID and Region."
- }
- break
- case "gemini":
- if (!apiConfiguration.geminiApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "openai-native":
- if (!apiConfiguration.openAiNativeApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "mistral":
- if (!apiConfiguration.mistralApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "openai":
- if (
- !apiConfiguration.openAiBaseUrl ||
- !apiConfiguration.openAiApiKey ||
- !apiConfiguration.openAiModelId
- ) {
- return "You must provide a valid base URL, API key, and model ID."
- }
- break
- case "ollama":
- if (!apiConfiguration.ollamaModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "lmstudio":
- if (!apiConfiguration.lmStudioModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "vscode-lm":
- if (!apiConfiguration.vsCodeLmModelSelector) {
- return "You must provide a valid model selector."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ if (!apiConfiguration.openRouterApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "glama":
+ if (!apiConfiguration.glamaApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "unbound":
+ if (!apiConfiguration.unboundApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "requesty":
+ if (!apiConfiguration.requestyApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "anthropic":
+ if (!apiConfiguration.apiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "bedrock":
+ if (!apiConfiguration.awsRegion) {
+ return "You must choose a region to use with AWS Bedrock."
+ }
+ break
+ case "vertex":
+ if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
+ return "You must provide a valid Google Cloud Project ID and Region."
+ }
+ break
+ case "gemini":
+ if (!apiConfiguration.geminiApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai-native":
+ if (!apiConfiguration.openAiNativeApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "mistral":
+ if (!apiConfiguration.mistralApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai":
+ if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
+ return "You must provide a valid base URL, API key, and model ID."
+ }
+ break
+ case "ollama":
+ if (!apiConfiguration.ollamaModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "lmstudio":
+ if (!apiConfiguration.lmStudioModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "vscode-lm":
+ if (!apiConfiguration.vsCodeLmModelSelector) {
+ return "You must provide a valid model selector."
+ }
+ break
+ }
+
return undefined
}
@@ -77,40 +86,81 @@ export function validateModelId(
glamaModels?: Record,
openRouterModels?: Record,
unboundModels?: Record,
+ requestyModels?: Record,
): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "glama":
- const glamaModelId = apiConfiguration.glamaModelId
- if (!glamaModelId) {
- return "You must provide a model ID."
- }
- if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "openrouter":
- const modelId = apiConfiguration.openRouterModelId
- if (!modelId) {
- return "You must provide a model ID."
- }
- if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "unbound":
- const unboundModelId = apiConfiguration.unboundModelId
- if (!unboundModelId) {
- return "You must provide a model ID."
- }
- if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ const modelId = apiConfiguration.openRouterModelId
+
+ if (!modelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ openRouterModels &&
+ Object.keys(openRouterModels).length > 1 &&
+ !Object.keys(openRouterModels).includes(modelId)
+ ) {
+ return `The model ID (${modelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "glama":
+ const glamaModelId = apiConfiguration.glamaModelId
+
+ if (!glamaModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ glamaModels &&
+ Object.keys(glamaModels).length > 1 &&
+ !Object.keys(glamaModels).includes(glamaModelId)
+ ) {
+ return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "unbound":
+ const unboundModelId = apiConfiguration.unboundModelId
+
+ if (!unboundModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ unboundModels &&
+ Object.keys(unboundModels).length > 1 &&
+ !Object.keys(unboundModels).includes(unboundModelId)
+ ) {
+ return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "requesty":
+ const requestyModelId = apiConfiguration.requestyModelId
+
+ if (!requestyModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ requestyModels &&
+ Object.keys(requestyModels).length > 1 &&
+ !Object.keys(requestyModels).includes(requestyModelId)
+ ) {
+ return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+ }
+
return undefined
}
From 44724e5881ab8daefa7b80bb6c52e5f1bb8a828e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 26 Feb 2025 07:06:25 +0000
Subject: [PATCH 25/38] changeset version bump
---
.changeset/cold-poems-change.md | 5 -----
.changeset/real-ties-destroy.md | 5 -----
.changeset/shaggy-spies-kneel.md | 5 -----
.changeset/swift-kings-attack.md | 5 -----
CHANGELOG.md | 9 +++++++++
package-lock.json | 4 ++--
package.json | 2 +-
7 files changed, 12 insertions(+), 23 deletions(-)
delete mode 100644 .changeset/cold-poems-change.md
delete mode 100644 .changeset/real-ties-destroy.md
delete mode 100644 .changeset/shaggy-spies-kneel.md
delete mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/cold-poems-change.md b/.changeset/cold-poems-change.md
deleted file mode 100644
index 41693ccdfc..0000000000
--- a/.changeset/cold-poems-change.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.5
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
deleted file mode 100644
index a2e9ba8eb0..0000000000
--- a/.changeset/real-ties-destroy.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Fix model picker
diff --git a/.changeset/shaggy-spies-kneel.md b/.changeset/shaggy-spies-kneel.md
deleted file mode 100644
index d137cf85ef..0000000000
--- a/.changeset/shaggy-spies-kneel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add drag-and-drop for files
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
deleted file mode 100644
index 8a8a425611..0000000000
--- a/.changeset/swift-kings-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Pass "thinking" params to OpenRouter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52fb754097..7a9b4b57bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Roo Code Changelog
+## 3.7.5
+
+### Patch Changes
+
+- v3.7.5
+- Fix model picker
+- Add drag-and-drop for files
+- Pass "thinking" params to OpenRouter
+
## [3.7.4]
- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
diff --git a/package-lock.json b/package-lock.json
index 4bcdf8136d..a6c75bd69b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 28045436e6..40bb6a545d 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.4",
+ "version": "3.7.5",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 319f16eb0aabb8033ef94d006b983bdbb45a1eec Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:11:42 -0800
Subject: [PATCH 26/38] Update CHANGELOG
---
CHANGELOG.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a9b4b57bf..ee107be67a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,11 @@
# Roo Code Changelog
-## 3.7.5
+## [3.7.5]
-### Patch Changes
-
-- v3.7.5
-- Fix model picker
+- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
+- Fix various issues with the model picker
- Add drag-and-drop for files
-- Pass "thinking" params to OpenRouter
+- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter
## [3.7.4]
From da1b31765ed05cab6c1d0e25cbae6748aa7ce89a Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:13:52 -0800
Subject: [PATCH 27/38] Update CHANGELOG
---
CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee107be67a..02a4a30cbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,8 @@
## [3.7.5]
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
-- Fix various issues with the model picker
+- Fix various issues with the model picker (thanks @System233!)
+- Fix model input / output cost parsing (thanks @System233!)
- Add drag-and-drop for files
- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter
From 78d5af491aed08505afcfb80bd02a96cb0d287ae Mon Sep 17 00:00:00 2001
From: Joe Manley
Date: Wed, 26 Feb 2025 09:32:13 -0800
Subject: [PATCH 28/38] Fix long strings correctly in ChatRow
---
webview-ui/src/components/chat/ChatRow.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index b139c68f96..4017ccf318 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -617,8 +617,10 @@ export const ChatRowContent = ({
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
- whiteSpace: "pre-line",
- wordWrap: "break-word",
+ overflow: "hidden",
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ overflowWrap: "anywhere",
}}>
Date: Wed, 26 Feb 2025 09:41:57 -0800
Subject: [PATCH 29/38] Add changeset
---
.changeset/fluffy-apples-attack.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/fluffy-apples-attack.md
diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md
new file mode 100644
index 0000000000..924a1b2505
--- /dev/null
+++ b/.changeset/fluffy-apples-attack.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Handle really long text in the ChatRow similar to TaskHeader
From d7266be3feb018c5bf207135cb54f74c94d198e5 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 13:52:38 -0500
Subject: [PATCH 30/38] Better OpenRouter error handling
---
.changeset/tender-cycles-help.md | 5 +++++
src/core/Cline.ts | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 .changeset/tender-cycles-help.md
diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md
new file mode 100644
index 0000000000..d43e423ee6
--- /dev/null
+++ b/.changeset/tender-cycles-help.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Better OpenRouter error handling
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 073bd10911..2e29ad453c 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -1010,7 +1010,7 @@ export class Cline {
} catch (error) {
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (alwaysApproveResubmit) {
- const errorMsg = error.message ?? "Unknown error"
+ const errorMsg = error.error?.metadata?.raw ?? error.message ?? "Unknown error"
const baseDelay = requestDelaySeconds || 5
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
// Wait for the greater of the exponential delay or the rate limit delay
From a4e58700ce7527ed486a96df6cce9709fb145074 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 16:04:32 -0500
Subject: [PATCH 31/38] Support multiple files in drag-and-drop
---
.changeset/orange-zoos-train.md | 5 +
.../src/components/chat/ChatTextArea.tsx | 37 ++-
.../chat/__tests__/ChatTextArea.test.tsx | 238 ++++++++++++++++++
3 files changed, 272 insertions(+), 8 deletions(-)
create mode 100644 .changeset/orange-zoos-train.md
diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md
new file mode 100644
index 0000000000..76c16f4567
--- /dev/null
+++ b/.changeset/orange-zoos-train.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Support multiple files in drag-and-drop
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index dc78a3fdb3..be2b2a9798 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -590,15 +590,36 @@ const ChatTextArea = forwardRef(
const files = Array.from(e.dataTransfer.files)
const text = e.dataTransfer.getData("text")
if (text) {
- // Convert the path to a mention-friendly format
- const mentionText = convertToMentionPath(text, cwd)
+ // Split text on newlines to handle multiple files
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
- const newValue =
- inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
- setInputValue(newValue)
- const newCursorPosition = cursorPosition + mentionText.length + 1
- setCursorPosition(newCursorPosition)
- setIntendedCursorPosition(newCursorPosition)
+ if (lines.length > 0) {
+ // Process each line as a separate file path
+ let newValue = inputValue.slice(0, cursorPosition)
+ let totalLength = 0
+
+ lines.forEach((line, index) => {
+ // Convert each path to a mention-friendly format
+ const mentionText = convertToMentionPath(line, cwd)
+ newValue += mentionText
+ totalLength += mentionText.length
+
+ // Add space after each mention except the last one
+ if (index < lines.length - 1) {
+ newValue += " "
+ totalLength += 1
+ }
+ })
+
+ // Add space after the last mention and append the rest of the input
+ newValue += " " + inputValue.slice(cursorPosition)
+ totalLength += 1
+
+ setInputValue(newValue)
+ const newCursorPosition = cursorPosition + totalLength
+ setCursorPosition(newCursorPosition)
+ setIntendedCursorPosition(newCursorPosition)
+ }
return
}
diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
index 205912fc15..3241010e88 100644
--- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
+++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
@@ -3,6 +3,7 @@ import ChatTextArea from "../ChatTextArea"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import { defaultModeSlug } from "../../../../../src/shared/modes"
+import * as pathMentions from "../../../utils/path-mentions"
// Mock modules
jest.mock("../../../utils/vscode", () => ({
@@ -12,9 +13,20 @@ jest.mock("../../../utils/vscode", () => ({
}))
jest.mock("../../../components/common/CodeBlock")
jest.mock("../../../components/common/MarkdownBlock")
+jest.mock("../../../utils/path-mentions", () => ({
+ convertToMentionPath: jest.fn((path, cwd) => {
+ // Simple mock implementation that mimics the real function's behavior
+ if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) {
+ const relativePath = path.substring(cwd.length)
+ return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
+ }
+ return path
+ }),
+}))
// Get the mocked postMessage function
const mockPostMessage = vscode.postMessage as jest.Mock
+const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock
// Mock ExtensionStateContext
jest.mock("../../../context/ExtensionStateContext")
@@ -160,4 +172,230 @@ describe("ChatTextArea", () => {
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
})
})
+
+ describe("multi-file drag and drop", () => {
+ const mockCwd = "/Users/test/project"
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ ;(useExtensionState as jest.Mock).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ cwd: mockCwd,
+ })
+ mockConvertToMentionPath.mockClear()
+ })
+
+ it("should process multiple file paths separated by newlines", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with text data containing multiple file paths
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called for each file path
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd)
+
+ // Verify setInputValue was called with the correct value
+ // The mock implementation of convertToMentionPath will convert the paths to @/file1.js and @/file2.js
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
+ })
+
+ it("should filter out empty lines in the dragged text", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with text data containing empty lines
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n\n/Users/test/project/file2.js\n\n"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called only for non-empty lines
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
+
+ // Verify setInputValue was called with the correct value
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
+ })
+
+ it("should correctly update cursor position after adding multiple mentions", () => {
+ const setInputValue = jest.fn()
+ const initialCursorPosition = 5
+
+ const { container } = render(
+ ,
+ )
+
+ // Set the cursor position manually
+ const textArea = container.querySelector("textarea")
+ if (textArea) {
+ textArea.selectionStart = initialCursorPosition
+ textArea.selectionEnd = initialCursorPosition
+ }
+
+ // Create a mock dataTransfer object with text data
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // The cursor position should be updated based on the implementation in the component
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Hello world")
+ })
+
+ it("should handle very long file paths correctly", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create a very long file path
+ const longPath =
+ "/Users/test/project/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript"
+
+ // Create a mock dataTransfer object with the long path
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(longPath),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called with the long path
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd)
+
+ // The mock implementation will convert it to @/very/long/path/...
+ expect(setInputValue).toHaveBeenCalledWith(
+ "@/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript ",
+ )
+ })
+
+ it("should handle paths with special characters correctly", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create paths with special characters
+ const specialPath1 = "/Users/test/project/file with spaces.js"
+ const specialPath2 = "/Users/test/project/file-with-dashes.js"
+ const specialPath3 = "/Users/test/project/file_with_underscores.js"
+ const specialPath4 = "/Users/test/project/file.with.dots.js"
+
+ // Create a mock dataTransfer object with the special paths
+ const dataTransfer = {
+ getData: jest
+ .fn()
+ .mockReturnValue(`${specialPath1}\n${specialPath2}\n${specialPath3}\n${specialPath4}`),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called for each path
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd)
+
+ // Verify setInputValue was called with the correct value
+ expect(setInputValue).toHaveBeenCalledWith(
+ "@/file with spaces.js @/file-with-dashes.js @/file_with_underscores.js @/file.with.dots.js ",
+ )
+ })
+
+ it("should handle paths outside the current working directory", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create paths outside the current working directory
+ const outsidePath = "/Users/other/project/file.js"
+
+ // Mock the convertToMentionPath function to return the original path for paths outside cwd
+ mockConvertToMentionPath.mockImplementationOnce((path, cwd) => {
+ return path // Return original path for this test
+ })
+
+ // Create a mock dataTransfer object with the outside path
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(outsidePath),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called with the outside path
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd)
+
+ // Verify setInputValue was called with the original path
+ expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ")
+ })
+
+ it("should do nothing when dropped text is empty", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with empty text
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(""),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was not called
+ expect(mockConvertToMentionPath).not.toHaveBeenCalled()
+
+ // Verify setInputValue was not called
+ expect(setInputValue).not.toHaveBeenCalled()
+ })
+ })
})
From 5e53d00ebcf0d2adf218a07452d0e15835bf3e64 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Wed, 26 Feb 2025 14:23:11 -0800
Subject: [PATCH 32/38] Allow control over maxTokens for thinking models
---
src/api/providers/anthropic.ts | 12 ++-
src/api/providers/openrouter.ts | 13 ++-
src/core/Cline.ts | 18 +++-
.../__tests__/sliding-window.test.ts | 102 +++++++++++++++---
src/core/sliding-window/index.ts | 37 ++++---
src/core/webview/ClineProvider.ts | 5 +
src/shared/api.ts | 11 +-
src/shared/globalState.ts | 1 +
.../components/settings/ThinkingBudget.tsx | 63 ++++++++---
9 files changed, 197 insertions(+), 65 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index ad58a1cf6b..8c5a1795b1 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -31,7 +31,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, info: modelInfo } = this.getModel()
- const maxTokens = modelInfo.maxTokens || 8192
+ const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens || 8192
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
@@ -41,7 +41,15 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// `claude-3-7-sonnet-20250219` model with a thinking budget.
// We can handle this more elegantly in the future.
modelId = "claude-3-7-sonnet-20250219"
- const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
+
+ // Clamp the thinking budget to be at most 80% of max tokens and at
+ // least 1024 tokens.
+ const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 0a9488e816..69bcb0074c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -108,12 +108,19 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
topP = 0.95
}
+ const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens
let temperature = this.options.modelTemperature ?? defaultTemperature
let thinking: BetaThinkingConfigParam | undefined = undefined
if (modelInfo.thinking) {
- const maxTokens = modelInfo.maxTokens || 8192
- const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
+ // Clamp the thinking budget to be at most 80% of max tokens and at
+ // least 1024 tokens.
+ const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -271,7 +278,7 @@ export async function getOpenRouterModels() {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
- modelInfo.maxTokens = 16384
+ modelInfo.maxTokens = 64_000
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
modelInfo.supportsPromptCache = true
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 073bd10911..fb123e0584 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -87,6 +87,7 @@ export type ClineOptions = {
export class Cline {
readonly taskId: string
+ readonly apiConfiguration: ApiConfiguration
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -148,6 +149,7 @@ export class Cline {
}
this.taskId = crypto.randomUUID()
+ this.apiConfiguration = apiConfiguration
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -961,13 +963,21 @@ export class Cline {
cacheWrites = 0,
cacheReads = 0,
}: ClineApiReqInfo = JSON.parse(previousRequest)
+
const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
- const trimmedMessages = truncateConversationIfNeeded(
- this.apiConversationHistory,
+ const modelInfo = this.api.getModel().info
+ const maxTokens = modelInfo.thinking
+ ? this.apiConfiguration.modelMaxTokens || modelInfo.maxTokens
+ : modelInfo.maxTokens
+ const contextWindow = modelInfo.contextWindow
+
+ const trimmedMessages = truncateConversationIfNeeded({
+ messages: this.apiConversationHistory,
totalTokens,
- this.api.getModel().info,
- )
+ maxTokens,
+ contextWindow,
+ })
if (trimmedMessages !== this.apiConversationHistory) {
await this.overwriteApiConversationHistory(trimmedMessages)
diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts
index 3dcf9e5fd2..cb897aa8cb 100644
--- a/src/core/sliding-window/__tests__/sliding-window.test.ts
+++ b/src/core/sliding-window/__tests__/sliding-window.test.ts
@@ -119,11 +119,21 @@ describe("getMaxTokens", () => {
// Max tokens = 100000 - 50000 = 50000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 49999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 49999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 50001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 50001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -133,11 +143,21 @@ describe("getMaxTokens", () => {
// Max tokens = 100000 - (100000 * 0.2) = 80000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 79999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 79999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 80001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 80001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -147,11 +167,21 @@ describe("getMaxTokens", () => {
// Max tokens = 50000 - 10000 = 40000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 39999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 39999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 40001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 40001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -161,11 +191,21 @@ describe("getMaxTokens", () => {
// Max tokens = 200000 - 30000 = 170000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 169999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 169999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 170001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 170001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -194,7 +234,12 @@ describe("truncateConversationIfNeeded", () => {
const maxTokens = 100000 - 30000 // 70000
const totalTokens = 69999 // Below threshold
- const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+ const result = truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result).toEqual(messages) // No truncation occurs
})
@@ -207,7 +252,12 @@ describe("truncateConversationIfNeeded", () => {
// With 4 messages after the first, 0.5 fraction means remove 2 messages
const expectedResult = [messages[0], messages[3], messages[4]]
- const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+ const result = truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result).toEqual(expectedResult)
})
@@ -218,14 +268,38 @@ describe("truncateConversationIfNeeded", () => {
// Test below threshold
const belowThreshold = 69999
- expect(truncateConversationIfNeeded(messages, belowThreshold, modelInfo1)).toEqual(
- truncateConversationIfNeeded(messages, belowThreshold, modelInfo2),
+ expect(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: belowThreshold,
+ contextWindow: modelInfo1.contextWindow,
+ maxTokens: modelInfo1.maxTokens,
+ }),
+ ).toEqual(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: belowThreshold,
+ contextWindow: modelInfo2.contextWindow,
+ maxTokens: modelInfo2.maxTokens,
+ }),
)
// Test above threshold
const aboveThreshold = 70001
- expect(truncateConversationIfNeeded(messages, aboveThreshold, modelInfo1)).toEqual(
- truncateConversationIfNeeded(messages, aboveThreshold, modelInfo2),
+ expect(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: aboveThreshold,
+ contextWindow: modelInfo1.contextWindow,
+ maxTokens: modelInfo1.maxTokens,
+ }),
+ ).toEqual(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: aboveThreshold,
+ contextWindow: modelInfo2.contextWindow,
+ maxTokens: modelInfo2.maxTokens,
+ }),
)
})
})
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index a0fff05ea5..8b646f933b 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -1,7 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
-import { ModelInfo } from "../../shared/api"
-
/**
* Truncates a conversation by removing a fraction of the messages.
*
@@ -26,28 +24,29 @@ export function truncateConversation(
}
/**
- * Conditionally truncates the conversation messages if the total token count exceeds the model's limit.
+ * Conditionally truncates the conversation messages if the total token count
+ * exceeds the model's limit.
*
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
* @param {number} totalTokens - The total number of tokens in the conversation.
- * @param {ModelInfo} modelInfo - Model metadata including context window size.
+ * @param {number} contextWindow - The context window size.
+ * @param {number} maxTokens - The maximum number of tokens allowed.
* @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
*/
-export function truncateConversationIfNeeded(
- messages: Anthropic.Messages.MessageParam[],
- totalTokens: number,
- modelInfo: ModelInfo,
-): Anthropic.Messages.MessageParam[] {
- return totalTokens < getMaxTokens(modelInfo) ? messages : truncateConversation(messages, 0.5)
+
+type TruncateOptions = {
+ messages: Anthropic.Messages.MessageParam[]
+ totalTokens: number
+ contextWindow: number
+ maxTokens?: number
}
-/**
- * Calculates the maximum allowed tokens
- *
- * @param {ModelInfo} modelInfo - The model information containing the context window size.
- * @returns {number} The maximum number of tokens allowed
- */
-function getMaxTokens(modelInfo: ModelInfo): number {
- // The buffer needs to be at least as large as `modelInfo.maxTokens`, or 20% of the context window if for some reason it's not set.
- return modelInfo.contextWindow - (modelInfo.maxTokens || modelInfo.contextWindow * 0.2)
+export function truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow,
+ maxTokens,
+}: TruncateOptions): Anthropic.Messages.MessageParam[] {
+ const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2)
+ return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index bc6f457868..5e6170e2ee 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1671,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
} = apiConfiguration
await Promise.all([
this.updateGlobalState("apiProvider", apiProvider),
@@ -1719,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelId", requestyModelId),
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
+ this.updateGlobalState("modelMaxTokens", modelMaxTokens),
])
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2210,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
maxOpenTabsContext,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
@@ -2293,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestyModelId") as Promise,
this.getGlobalState("requestyModelInfo") as Promise,
this.getGlobalState("modelTemperature") as Promise,
+ this.getGlobalState("modelMaxTokens") as Promise,
this.getGlobalState("maxOpenTabsContext") as Promise,
])
@@ -2358,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
},
lastShownAnnouncementId,
customInstructions,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 5d4b8b120d..e7e4c54db6 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -68,6 +68,7 @@ export interface ApiHandlerOptions {
requestyModelId?: string
requestyModelInfo?: ModelInfo
modelTemperature?: number
+ modelMaxTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -92,19 +93,13 @@ export interface ModelInfo {
thinking?: boolean
}
-export const THINKING_BUDGET = {
- step: 1024,
- min: 1024,
- default: 8 * 1024,
-}
-
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicModels = {
"claude-3-7-sonnet-20250219:thinking": {
- maxTokens: 16384,
+ maxTokens: 64_000,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
@@ -116,7 +111,7 @@ export const anthropicModels = {
thinking: true,
},
"claude-3-7-sonnet-20250219": {
- maxTokens: 16384,
+ maxTokens: 64_000,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 7b6b4f8274..2cc90456a7 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -81,5 +81,6 @@ export type GlobalStateKey =
| "requestyModelInfo"
| "unboundModelInfo"
| "modelTemperature"
+ | "modelMaxTokens"
| "mistralCodestralUrl"
| "maxOpenTabsContext"
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
index efaa90dc39..5b67874410 100644
--- a/webview-ui/src/components/settings/ThinkingBudget.tsx
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -1,6 +1,8 @@
+import { useEffect } from "react"
+
import { Slider } from "@/components/ui"
-import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
interface ThinkingBudgetProps {
apiConfiguration: ApiConfiguration
@@ -9,21 +11,52 @@ interface ThinkingBudgetProps {
}
export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
- const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
+ const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
+ const tokensMin = 8192
+ const tokensMax = modelInfo?.maxTokens || 64_000
- return modelInfo && modelInfo.thinking ? (
-
-
Thinking Budget
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {budget}
+ const thinkingTokens = apiConfiguration?.anthropicThinking || 8192
+ const thinkingTokensMin = 1024
+ const thinkingTokensMax = Math.floor(0.8 * tokens)
+
+ useEffect(() => {
+ if (thinkingTokens > thinkingTokensMax) {
+ setApiConfigurationField("anthropicThinking", thinkingTokensMax)
+ }
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+
+ if (!modelInfo || !modelInfo.thinking) {
+ return null
+ }
+
+ return (
+
+
+
Max Tokens
+
+
setApiConfigurationField("modelMaxTokens", value)}
+ />
+ {tokens}
+
+
+
+
Max Thinking Tokens
+
+
setApiConfigurationField("anthropicThinking", value)}
+ />
+ {thinkingTokens}
+
- ) : null
+ )
}
From cf69b0fff92e8b1cff23cf4b37d8e8a2f10a34dc Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Wed, 26 Feb 2025 14:26:27 -0800
Subject: [PATCH 33/38] Add changeset
---
.changeset/wild-emus-dream.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/wild-emus-dream.md
diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md
new file mode 100644
index 0000000000..19e5a4626b
--- /dev/null
+++ b/.changeset/wild-emus-dream.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Allow control over maxTokens for thinking models
From dfa019e7f443bf9997ed2ac2556f597a59e4bf77 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 17:34:39 -0500
Subject: [PATCH 34/38] Truncate results from search_files to 500 chars max
---
.changeset/stale-cooks-help.md | 5 ++
src/services/ripgrep/__tests__/index.test.ts | 51 ++++++++++++++++++++
src/services/ripgrep/index.ts | 30 ++++++++++--
3 files changed, 82 insertions(+), 4 deletions(-)
create mode 100644 .changeset/stale-cooks-help.md
create mode 100644 src/services/ripgrep/__tests__/index.test.ts
diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md
new file mode 100644
index 0000000000..8c9c714738
--- /dev/null
+++ b/.changeset/stale-cooks-help.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Truncate search_file output to avoid crashing the extension
diff --git a/src/services/ripgrep/__tests__/index.test.ts b/src/services/ripgrep/__tests__/index.test.ts
new file mode 100644
index 0000000000..7c3549a827
--- /dev/null
+++ b/src/services/ripgrep/__tests__/index.test.ts
@@ -0,0 +1,51 @@
+// npx jest src/services/ripgrep/__tests__/index.test.ts
+
+import { describe, expect, it } from "@jest/globals"
+import { truncateLine } from "../index"
+
+describe("Ripgrep line truncation", () => {
+ // The default MAX_LINE_LENGTH is 500 in the implementation
+ const MAX_LINE_LENGTH = 500
+
+ it("should truncate lines longer than MAX_LINE_LENGTH", () => {
+ const longLine = "a".repeat(600) // Line longer than MAX_LINE_LENGTH
+ const truncated = truncateLine(longLine)
+
+ expect(truncated).toContain("[truncated...]")
+ expect(truncated.length).toBeLessThan(longLine.length)
+ expect(truncated.length).toEqual(MAX_LINE_LENGTH + " [truncated...]".length)
+ })
+
+ it("should not truncate lines shorter than MAX_LINE_LENGTH", () => {
+ const shortLine = "Short line of text"
+ const truncated = truncateLine(shortLine)
+
+ expect(truncated).toEqual(shortLine)
+ expect(truncated).not.toContain("[truncated...]")
+ })
+
+ it("should correctly truncate a line at exactly MAX_LINE_LENGTH characters", () => {
+ const exactLine = "a".repeat(MAX_LINE_LENGTH)
+ const exactPlusOne = exactLine + "x"
+
+ // Should not truncate when exactly MAX_LINE_LENGTH
+ expect(truncateLine(exactLine)).toEqual(exactLine)
+
+ // Should truncate when exceeding MAX_LINE_LENGTH by even 1 character
+ expect(truncateLine(exactPlusOne)).toContain("[truncated...]")
+ })
+
+ it("should handle empty lines without errors", () => {
+ expect(truncateLine("")).toEqual("")
+ })
+
+ it("should allow custom maximum length", () => {
+ const customLength = 100
+ const line = "a".repeat(customLength + 50)
+
+ const truncated = truncateLine(line, customLength)
+
+ expect(truncated.length).toEqual(customLength + " [truncated...]".length)
+ expect(truncated).toContain("[truncated...]")
+ })
+})
diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts
index b48c60b5b2..770c897e52 100644
--- a/src/services/ripgrep/index.ts
+++ b/src/services/ripgrep/index.ts
@@ -58,7 +58,19 @@ interface SearchResult {
afterContext: string[]
}
+// Constants
const MAX_RESULTS = 300
+const MAX_LINE_LENGTH = 500
+
+/**
+ * Truncates a line if it exceeds the maximum length
+ * @param line The line to truncate
+ * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH)
+ * @returns The truncated line, or the original line if it's shorter than maxLength
+ */
+export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string {
+ return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
+}
async function getBinPath(vscodeAppRoot: string): Promise {
const checkPath = async (pkgFolder: string) => {
@@ -140,7 +152,8 @@ export async function regexSearchFiles(
let output: string
try {
output = await execRipgrep(rgPath, args)
- } catch {
+ } catch (error) {
+ console.error("Error executing ripgrep:", error)
return "No results found"
}
const results: SearchResult[] = []
@@ -154,19 +167,28 @@ export async function regexSearchFiles(
if (currentResult) {
results.push(currentResult as SearchResult)
}
+
+ // Safety check: truncate extremely long lines to prevent excessive output
+ const matchText = parsed.data.lines.text
+ const truncatedMatch = truncateLine(matchText)
+
currentResult = {
file: parsed.data.path.text,
line: parsed.data.line_number,
column: parsed.data.submatches[0].start,
- match: parsed.data.lines.text,
+ match: truncatedMatch,
beforeContext: [],
afterContext: [],
}
} else if (parsed.type === "context" && currentResult) {
+ // Apply the same truncation logic to context lines
+ const contextText = parsed.data.lines.text
+ const truncatedContext = truncateLine(contextText)
+
if (parsed.data.line_number < currentResult.line!) {
- currentResult.beforeContext!.push(parsed.data.lines.text)
+ currentResult.beforeContext!.push(truncatedContext)
} else {
- currentResult.afterContext!.push(parsed.data.lines.text)
+ currentResult.afterContext!.push(truncatedContext)
}
}
} catch (error) {
From 247a50a6dcbb29512d20796b29a3240dc4b84463 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 26 Feb 2025 23:17:23 +0000
Subject: [PATCH 35/38] changeset version bump
---
.changeset/fluffy-apples-attack.md | 5 -----
.changeset/orange-zoos-train.md | 5 -----
.changeset/stale-cooks-help.md | 5 -----
.changeset/tender-cycles-help.md | 5 -----
.changeset/wild-emus-dream.md | 5 -----
CHANGELOG.md | 10 ++++++++++
package-lock.json | 4 ++--
package.json | 2 +-
8 files changed, 13 insertions(+), 28 deletions(-)
delete mode 100644 .changeset/fluffy-apples-attack.md
delete mode 100644 .changeset/orange-zoos-train.md
delete mode 100644 .changeset/stale-cooks-help.md
delete mode 100644 .changeset/tender-cycles-help.md
delete mode 100644 .changeset/wild-emus-dream.md
diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md
deleted file mode 100644
index 924a1b2505..0000000000
--- a/.changeset/fluffy-apples-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Handle really long text in the ChatRow similar to TaskHeader
diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md
deleted file mode 100644
index 76c16f4567..0000000000
--- a/.changeset/orange-zoos-train.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Support multiple files in drag-and-drop
diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md
deleted file mode 100644
index 8c9c714738..0000000000
--- a/.changeset/stale-cooks-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Truncate search_file output to avoid crashing the extension
diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md
deleted file mode 100644
index d43e423ee6..0000000000
--- a/.changeset/tender-cycles-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Better OpenRouter error handling
diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md
deleted file mode 100644
index 19e5a4626b..0000000000
--- a/.changeset/wild-emus-dream.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Allow control over maxTokens for thinking models
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02a4a30cbd..0e5223231a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Roo Code Changelog
+## 3.7.6
+
+### Patch Changes
+
+- Handle really long text in the ChatRow similar to TaskHeader
+- Support multiple files in drag-and-drop
+- Truncate search_file output to avoid crashing the extension
+- Better OpenRouter error handling
+- Allow control over maxTokens for thinking models
+
## [3.7.5]
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
diff --git a/package-lock.json b/package-lock.json
index a6c75bd69b..808e2f2f10 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.5",
+ "version": "3.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.5",
+ "version": "3.7.6",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 40bb6a545d..463e9d597a 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.5",
+ "version": "3.7.6",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 4f578dc8262e03a2a665abcd6784610cc092cdb2 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 18:47:22 -0500
Subject: [PATCH 36/38] Update CHANGELOG.md
---
CHANGELOG.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e5223231a..13b0695335 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,12 @@
# Roo Code Changelog
-## 3.7.6
+## [3.7.6]
-### Patch Changes
-
-- Handle really long text in the ChatRow similar to TaskHeader
+- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
- Support multiple files in drag-and-drop
- Truncate search_file output to avoid crashing the extension
-- Better OpenRouter error handling
-- Allow control over maxTokens for thinking models
+- Better OpenRouter error handling (no more "Provider Error")
+- Add slider to control max output tokens for thinking models
## [3.7.5]
From 5c5bf8502094fb87397eebadda89acd3512dcf84 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 21:36:19 -0500
Subject: [PATCH 37/38] Stop removing commas from terminal output
---
.changeset/sour-parents-hug.md | 5 +++++
src/integrations/terminal/TerminalProcess.ts | 3 ---
2 files changed, 5 insertions(+), 3 deletions(-)
create mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
new file mode 100644
index 0000000000..a24286b6bb
--- /dev/null
+++ b/.changeset/sour-parents-hug.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 5597350db3..4e85c10575 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,9 +110,6 @@ export class TerminalProcess extends EventEmitter {
data = lines.join("\n")
}
- // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
- data = data.replace(/,/g, "")
-
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
From 4806ab5420048af6526348e5b128dd4724c9fcc8 Mon Sep 17 00:00:00 2001
From: dleffel
Date: Wed, 26 Feb 2025 21:34:56 -0800
Subject: [PATCH 38/38] Fix missing tooltips in several components.
---
.../src/components/chat/Announcement.tsx | 1 +
.../src/components/chat/ChatTextArea.tsx | 5 +++
webview-ui/src/components/chat/ChatView.tsx | 33 ++++++++++++++++++-
webview-ui/src/components/chat/TaskHeader.tsx | 13 ++++++--
4 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606ef..93d0c9d750 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -25,6 +25,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index be2b2a9798..dcbe085147 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -798,6 +798,7 @@ const ChatTextArea = forwardRef(
@@ -1101,6 +1102,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
+ title={
+ primaryButtonText === "Retry"
+ ? "Try the operation again"
+ : primaryButtonText === "Save"
+ ? "Save the file changes"
+ : primaryButtonText === "Approve"
+ ? "Approve this action"
+ : primaryButtonText === "Run Command"
+ ? "Execute this command"
+ : primaryButtonText === "Start New Task"
+ ? "Begin a new task"
+ : primaryButtonText === "Resume Task"
+ ? "Continue the current task"
+ : primaryButtonText === "Proceed Anyways"
+ ? "Continue despite warnings"
+ : primaryButtonText === "Proceed While Running"
+ ? "Continue while command executes"
+ : undefined
+ }
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
@@ -1113,6 +1133,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
+ title={
+ isStreaming
+ ? "Cancel the current operation"
+ : secondaryButtonText === "Start New Task"
+ ? "Begin a new task"
+ : secondaryButtonText === "Reject"
+ ? "Reject this action"
+ : secondaryButtonText === "Terminate"
+ ? "End the current task"
+ : undefined
+ }
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index 341855f796..fb7db6f617 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -180,7 +180,11 @@ const TaskHeader: React.FC
= ({
${totalCost?.toFixed(4)}
)}
-
+
@@ -348,13 +352,18 @@ export const highlightMentions = (text?: string, withShadow = true) => {
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
-