From 9b267e9afc8cc4308e5decfa0a9c312caaafc4bf Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Tue, 25 Feb 2025 15:30:10 +0100
Subject: [PATCH 01/28] Add Vertex AI prompt caching support and enhance
streaming handling
- Implemented comprehensive prompt caching strategy for Vertex AI models
- Added support for caching system prompts and user message text blocks
- Enhanced stream processing to handle cache-related usage metrics
- Updated model configurations to enable prompt caching
- Improved type definitions for Vertex AI message handling
---
src/api/providers/__tests__/vertex.test.ts | 215 ++++++++++++++++++-
src/api/providers/vertex.ts | 235 ++++++++++++++++++---
src/shared/api.ts | 20 +-
3 files changed, 435 insertions(+), 35 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index ebe60ba0c6..6e81fd771b 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { VertexHandler } from "../vertex"
+import { ApiStreamChunk } from "../../transform/stream"
// Mock Vertex SDK
jest.mock("@anthropic-ai/vertex-sdk", () => ({
@@ -128,7 +129,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -158,8 +159,29 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
- system: systemPrompt,
- messages: mockMessages,
+ system: [
+ {
+ type: "text",
+ text: "You are a helpful assistant",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ messages: [
+ {
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: "Hi there!",
+ },
+ ],
stream: true,
})
})
@@ -196,7 +218,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -230,6 +252,183 @@ describe("VertexHandler", () => {
}
}).rejects.toThrow("Vertex API error")
})
+
+ it("should handle prompt caching for supported models", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ cache_creation_input_tokens: 3,
+ cache_read_input_tokens: 2,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "text",
+ text: "Hello",
+ },
+ },
+ {
+ type: "content_block_delta",
+ delta: {
+ type: "text_delta",
+ text: " world!",
+ },
+ },
+ {
+ type: "message_delta",
+ usage: {
+ output_tokens: 5,
+ },
+ },
+ ]
+
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, [
+ {
+ role: "user",
+ content: "First message",
+ },
+ {
+ role: "assistant",
+ content: "Response",
+ },
+ {
+ role: "user",
+ content: "Second message",
+ },
+ ])
+
+ const chunks: ApiStreamChunk[] = []
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ // Verify usage information
+ const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+ expect(usageChunks).toHaveLength(2)
+ expect(usageChunks[0]).toEqual({
+ type: "usage",
+ inputTokens: 10,
+ outputTokens: 0,
+ cacheWriteTokens: 3,
+ cacheReadTokens: 2,
+ })
+ expect(usageChunks[1]).toEqual({
+ type: "usage",
+ inputTokens: 0,
+ outputTokens: 5,
+ })
+
+ // Verify text content
+ const textChunks = chunks.filter((chunk) => chunk.type === "text")
+ expect(textChunks).toHaveLength(2)
+ expect(textChunks[0].text).toBe("Hello")
+ expect(textChunks[1].text).toBe(" world!")
+
+ // Verify cache control was added correctly
+ expect(mockCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ system: [
+ {
+ type: "text",
+ text: "You are a helpful assistant",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ messages: [
+ expect.objectContaining({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "First message",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ }),
+ expect.objectContaining({
+ role: "assistant",
+ content: "Response",
+ }),
+ expect.objectContaining({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Second message",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ }),
+ ],
+ }),
+ )
+ })
+
+ it("should handle cache-related usage metrics", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ cache_creation_input_tokens: 5,
+ cache_read_input_tokens: 3,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "text",
+ text: "Hello",
+ },
+ },
+ ]
+
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ // Check for cache-related metrics in usage chunk
+ const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+ expect(usageChunks.length).toBeGreaterThan(0)
+ expect(usageChunks[0]).toHaveProperty("cacheWriteTokens", 5)
+ expect(usageChunks[0]).toHaveProperty("cacheReadTokens", 3)
+ })
})
describe("completePrompt", () => {
@@ -240,7 +439,13 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
- messages: [{ role: "user", content: "Test prompt" }],
+ system: "",
+ messages: [
+ {
+ role: "user",
+ content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
+ },
+ ],
stream: false,
})
})
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 0ee22e5893..70562766c3 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -1,9 +1,86 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
+// Types for Vertex SDK
+
+/**
+ * Vertex API has specific limitations for prompt caching:
+ * 1. Maximum of 4 blocks can have cache_control
+ * 2. Only text blocks can be cached (images and other content types cannot)
+ * 3. Cache control can only be applied to user messages, not assistant messages
+ *
+ * Our caching strategy:
+ * - Cache the system prompt (1 block)
+ * - Cache the last text block of the second-to-last user message (1 block)
+ * - Cache the last text block of the last user message (1 block)
+ * This ensures we stay under the 4-block limit while maintaining effective caching
+ * for the most relevant context.
+ */
+
+interface VertexTextBlock {
+ type: "text"
+ text: string
+ cache_control?: { type: "ephemeral" }
+}
+
+interface VertexImageBlock {
+ type: "image"
+ source: {
+ type: "base64"
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"
+ data: string
+ }
+}
+
+type VertexContentBlock = VertexTextBlock | VertexImageBlock
+
+interface VertexUsage {
+ input_tokens?: number
+ output_tokens?: number
+ cache_creation_input_tokens?: number
+ cache_read_input_tokens?: number
+}
+
+interface VertexMessage extends Omit {
+ content: string | VertexContentBlock[]
+}
+
+interface VertexMessageCreateParams {
+ model: string
+ max_tokens: number
+ temperature: number
+ system: string | VertexTextBlock[]
+ messages: VertexMessage[]
+ stream: boolean
+}
+
+interface VertexMessageResponse {
+ content: Array<{ type: "text"; text: string }>
+}
+
+interface VertexMessageStreamEvent {
+ type: "message_start" | "message_delta" | "content_block_start" | "content_block_delta"
+ message?: {
+ usage: VertexUsage
+ }
+ usage?: {
+ output_tokens: number
+ }
+ content_block?: {
+ type: "text"
+ text: string
+ }
+ index?: number
+ delta?: {
+ type: "text_delta"
+ text: string
+ }
+}
+
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
@@ -18,37 +95,120 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
})
}
+ private formatMessageForCache(message: Anthropic.Messages.MessageParam, shouldCache: boolean): VertexMessage {
+ // Assistant messages are kept as-is since they can't be cached
+ if (message.role === "assistant") {
+ return message as VertexMessage
+ }
+
+ // For string content, we convert to array format with optional cache control
+ if (typeof message.content === "string") {
+ return {
+ ...message,
+ content: [
+ {
+ type: "text" as const,
+ text: message.content,
+ // For string content, we only have one block so it's always the last
+ ...(shouldCache && { cache_control: { type: "ephemeral" } }),
+ },
+ ],
+ }
+ }
+
+ // For array content, find the last text block index once before mapping
+ const lastTextBlockIndex = message.content.reduce(
+ (lastIndex, content, index) => (content.type === "text" ? index : lastIndex),
+ -1,
+ )
+
+ // Then use this pre-calculated index in the map function
+ return {
+ ...message,
+ content: message.content.map((content, contentIndex) => {
+ // Images and other non-text content are passed through unchanged
+ if (content.type === "image") {
+ return content as VertexImageBlock
+ }
+
+ // Check if this is the last text block using our pre-calculated index
+ const isLastTextBlock = contentIndex === lastTextBlockIndex
+
+ return {
+ type: "text" as const,
+ text: (content as { text: string }).text,
+ ...(shouldCache && isLastTextBlock && { cache_control: { type: "ephemeral" } }),
+ }
+ }),
+ }
+ }
+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
- const stream = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ const useCache = model.info.supportsPromptCache
+
+ // Find indices of user messages that we want to cache
+ // We only cache the last two user messages to stay within the 4-block limit
+ // (1 block for system + 1 block each for last two user messages = 3 total)
+ const userMsgIndices = useCache
+ ? messages.reduce((acc, msg, i) => (msg.role === "user" ? [...acc, i] : acc), [] as number[])
+ : []
+ const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
+ const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
+
+ // Create the stream with appropriate caching configuration
+ const params = {
+ model: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- system: systemPrompt,
- messages,
+ // Cache the system prompt if caching is enabled
+ system: useCache
+ ? [
+ {
+ text: systemPrompt,
+ type: "text" as const,
+ cache_control: { type: "ephemeral" },
+ },
+ ]
+ : systemPrompt,
+ messages: messages.map((message, index) => {
+ // Only cache the last two user messages
+ const shouldCache = useCache && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)
+ return this.formatMessageForCache(message, shouldCache)
+ }),
stream: true,
- })
+ }
+
+ const stream = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsStreaming,
+ )) as unknown as AnthropicStream
+
+ // Process the stream chunks
for await (const chunk of stream) {
switch (chunk.type) {
- case "message_start":
- const usage = chunk.message.usage
+ case "message_start": {
+ const usage = chunk.message!.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
+ cacheWriteTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
}
break
- case "message_delta":
+ }
+ case "message_delta": {
yield {
type: "usage",
inputTokens: 0,
- outputTokens: chunk.usage.output_tokens || 0,
+ outputTokens: chunk.usage!.output_tokens || 0,
}
break
-
- case "content_block_start":
- switch (chunk.content_block.type) {
- case "text":
- if (chunk.index > 0) {
+ }
+ case "content_block_start": {
+ switch (chunk.content_block!.type) {
+ case "text": {
+ if (chunk.index! > 0) {
yield {
type: "text",
text: "\n",
@@ -56,21 +216,25 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
yield {
type: "text",
- text: chunk.content_block.text,
+ text: chunk.content_block!.text,
}
break
+ }
}
break
- case "content_block_delta":
- switch (chunk.delta.type) {
- case "text_delta":
+ }
+ case "content_block_delta": {
+ switch (chunk.delta!.type) {
+ case "text_delta": {
yield {
type: "text",
- text: chunk.delta.text,
+ text: chunk.delta!.text,
}
break
+ }
}
break
+ }
}
}
}
@@ -86,13 +250,34 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async completePrompt(prompt: string): Promise {
try {
- const response = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ const useCache = model.info.supportsPromptCache
+
+ const params = {
+ model: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- messages: [{ role: "user", content: prompt }],
+ system: "", // No system prompt needed for single completions
+ messages: [
+ {
+ role: "user",
+ content: useCache
+ ? [
+ {
+ type: "text" as const,
+ text: prompt,
+ cache_control: { type: "ephemeral" },
+ },
+ ]
+ : prompt,
+ },
+ ],
stream: false,
- })
+ }
+
+ const response = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsNonStreaming,
+ )) as unknown as VertexMessageResponse
const content = response.content[0]
if (content.type === "text") {
diff --git a/src/shared/api.ts b/src/shared/api.ts
index cea760c776..95399cca4a 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -435,41 +435,51 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet@20240620": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-haiku@20241022": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 5.0,
+ cacheWritesPrice: 1.25,
+ cacheReadsPrice: 0.1,
},
"claude-3-opus@20240229": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 15.0,
outputPrice: 75.0,
+ cacheWritesPrice: 18.75,
+ cacheReadsPrice: 1.5,
},
"claude-3-haiku@20240307": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 0.25,
outputPrice: 1.25,
+ cacheWritesPrice: 0.3,
+ cacheReadsPrice: 0.03,
},
} as const satisfies Record
From 5e53d00ebcf0d2adf218a07452d0e15835bf3e64 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Wed, 26 Feb 2025 14:23:11 -0800
Subject: [PATCH 02/28] 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 03/28] 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 04/28] 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 05/28] 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 06/28] 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 07/28] 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 08/28] 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(
{
const value = e.target.value
if (value === "prompts-action") {
@@ -849,6 +850,7 @@ const ChatTextArea = forwardRef(
{
const value = e.target.value
if (value === "settings-action") {
@@ -915,6 +917,7 @@ const ChatTextArea = forwardRef(
role="button"
aria-label="enhance prompt"
data-testid="enhance-prompt-button"
+ title="Enhance prompt with additional context"
className={`input-icon-button ${
textAreaDisabled ? "disabled" : ""
} codicon codicon-sparkle`}
@@ -927,11 +930,13 @@ const ChatTextArea = forwardRef(
className={`input-icon-button ${
shouldDisableImages ? "disabled" : ""
} codicon codicon-device-camera`}
+ title="Add images to message"
onClick={() => !shouldDisableImages && onSelectImages()}
style={{ fontSize: 16.5 }}
/>
!textAreaDisabled && onSend()}
style={{ fontSize: 15 }}
/>
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 98369cf095..fcd1ba9a3b 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
- }}>
+ }}
+ title="Scroll to bottom of chat">
@@ -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 }) => (
-
vscode.postMessage({ type: "exportCurrentTask" })}>
+ vscode.postMessage({ type: "exportCurrentTask" })}>
{!!item?.size && item.size > 0 && (
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
{prettyBytes(item.size)}
From 10c6f8fb67bc358a8a57e16a103205f113ec6687 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 01:31:30 -0500
Subject: [PATCH 09/28] Graduate checkpoints out of beta
---
.changeset/eighty-cheetahs-fetch.md | 5 +++
src/core/Cline.ts | 18 ++++----
src/core/webview/ClineProvider.ts | 30 ++++++-------
.../webview/__tests__/ClineProvider.test.ts | 4 +-
src/shared/ExtensionMessage.ts | 2 +-
src/shared/WebviewMessage.ts | 2 +-
src/shared/globalState.ts | 2 +-
.../src/components/chat/Announcement.tsx | 45 +++++++++----------
.../src/components/settings/SettingsView.tsx | 45 +++++++++----------
.../src/context/ExtensionStateContext.tsx | 6 +--
10 files changed, 80 insertions(+), 79 deletions(-)
create mode 100644 .changeset/eighty-cheetahs-fetch.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
new file mode 100644
index 0000000000..ca103880c8
--- /dev/null
+++ b/.changeset/eighty-cheetahs-fetch.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Graduate checkpoints out of beta
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 532b9cbe99..00897eecf4 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -115,7 +115,7 @@ export class Cline {
isInitialized = false
// checkpoints
- checkpointsEnabled: boolean = false
+ enableCheckpoints: boolean = false
private checkpointService?: CheckpointService
// streaming
@@ -159,7 +159,7 @@ export class Cline {
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
this.providerRef = new WeakRef(provider)
this.diffViewProvider = new DiffViewProvider(cwd)
- this.checkpointsEnabled = enableCheckpoints ?? false
+ this.enableCheckpoints = enableCheckpoints ?? false
if (historyItem) {
this.taskId = historyItem.id
@@ -3337,7 +3337,7 @@ export class Cline {
// Checkpoints
private async getCheckpointService() {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
throw new Error("Checkpoints are disabled")
}
@@ -3378,7 +3378,7 @@ export class Cline {
commitHash: string
mode: "full" | "checkpoint"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3417,12 +3417,12 @@ export class Cline {
)
} catch (err) {
this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
public async checkpointSave({ isFirst }: { isFirst: boolean }) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3443,7 +3443,7 @@ export class Cline {
}
} catch (err) {
this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
@@ -3456,7 +3456,7 @@ export class Cline {
commitHash: string
mode: "preview" | "restore"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3511,7 +3511,7 @@ export class Cline {
this.providerRef.deref()?.cancelTask()
} catch (err) {
this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5e6170e2ee..633c7d7293 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -64,7 +64,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
protected mcpHub?: McpHub // Change from private to protected
- private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
+ private latestAnnouncementId = "feb-27-2025-automatic-checkpoints" // update to some unique identifier when we add a new announcement
configManager: ConfigManager
customModesManager: CustomModesManager
@@ -317,7 +317,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -332,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
task,
images,
@@ -347,7 +347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -362,7 +362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
historyItem,
experiments,
@@ -1017,9 +1017,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("diffEnabled", diffEnabled)
await this.postStateToWebview()
break
- case "checkpointsEnabled":
- const checkpointsEnabled = message.bool ?? false
- await this.updateGlobalState("checkpointsEnabled", checkpointsEnabled)
+ case "enableCheckpoints":
+ const enableCheckpoints = message.bool ?? true
+ await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
await this.postStateToWebview()
break
case "browserViewportSize":
@@ -1939,11 +1939,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await fs.unlink(legacyMessagesFilePath)
}
- const { checkpointsEnabled } = await this.getState()
+ const { enableCheckpoints } = await this.getState()
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Delete checkpoints branch.
- if (checkpointsEnabled && baseDir) {
+ if (enableCheckpoints && baseDir) {
const branchSummary = await simpleGit(baseDir)
.branch(["-D", `roo-code-checkpoints-${id}`])
.catch(() => undefined)
@@ -1999,7 +1999,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowModeSwitch,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
taskHistory,
soundVolume,
browserViewportSize,
@@ -2048,7 +2048,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
soundVolume: soundVolume ?? 0.5,
@@ -2181,7 +2181,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
soundVolume,
browserViewportSize,
fuzzyMatchThreshold,
@@ -2265,7 +2265,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("allowedCommands") as Promise,
this.getGlobalState("soundEnabled") as Promise,
this.getGlobalState("diffEnabled") as Promise,
- this.getGlobalState("checkpointsEnabled") as Promise,
+ this.getGlobalState("enableCheckpoints") as Promise,
this.getGlobalState("soundVolume") as Promise,
this.getGlobalState("browserViewportSize") as Promise,
this.getGlobalState("fuzzyMatchThreshold") as Promise,
@@ -2376,7 +2376,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
soundVolume,
browserViewportSize: browserViewportSize ?? "900x600",
screenshotQuality: screenshotQuality ?? 75,
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 6449cc93be..c8742cd3f4 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -369,7 +369,7 @@ describe("ClineProvider", () => {
uriScheme: "vscode",
soundEnabled: false,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
writeDelayMs: 1000,
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
@@ -677,7 +677,7 @@ describe("ClineProvider", () => {
},
mode: "code",
diffEnabled: true,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
fuzzyMatchThreshold: 1.0,
experiments: experimentDefault,
} as any)
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index e87edffed1..34abd38dbf 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -111,7 +111,7 @@ export interface ExtensionState {
soundEnabled?: boolean
soundVolume?: number
diffEnabled?: boolean
- checkpointsEnabled: boolean
+ enableCheckpoints: boolean
browserViewportSize?: string
screenshotQuality?: number
fuzzyMatchThreshold?: number
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index fde7442cc1..8d3a114e65 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -52,7 +52,7 @@ export interface WebviewMessage {
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "openMcpSettings"
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 2cc90456a7..0863b34db2 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -53,7 +53,7 @@ export type GlobalStateKey =
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606ef..13c77fe442 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -1,8 +1,5 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
-// import VSCodeButtonLink from "./VSCodeButtonLink"
-// import { getOpenRouterAuthUrl } from "./ApiOptions"
-// import { vscode } from "../utils/vscode"
interface AnnouncementProps {
version: string
@@ -28,36 +25,38 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
style={{ position: "absolute", top: "8px", right: "8px" }}>
- 🎉{" "}Introducing Roo Code 3.2
+ 🎉{" "}Automatic Checkpoints Now Enabled
- Our biggest update yet is here - we're officially changing our name from Roo Cline to Roo Code! After
- growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to
- everyone in the Cline community who helped us reach this milestone.
+ We're thrilled to announce that our experimental Checkpoints feature is now enabled by default for all
+ users. This powerful feature automatically tracks your project changes during a task, allowing you to
+ quickly review or revert to earlier states if needed.
- Custom Modes: Celebrating Our New Identity
+ What's New
- To mark this new chapter, we're introducing the power to shape Roo Code into any role you need! Create
- specialized personas and create an entire team of agents with deeply customized prompts:
+ Automatic Checkpoints provide you with:
- QA Engineers who write thorough test cases and catch edge cases
- Product Managers who excel at user stories and feature prioritization
- UI/UX Designers who craft beautiful, accessible interfaces
- Code Reviewers who ensure quality and maintainability
+ Peace of mind when making significant changes
+ Ability to visually inspect changes between steps
+ Easy rollback if you're not satisfied with certain code modifications
+ Improved navigation through complex task execution
- Just click the icon to
- get started with Custom Modes!
- Join Us for the Next Chapter
+ Customize Your Experience
- We can't wait to see how you'll push Roo Code's potential even further! Share your custom modes and join
- the discussion at{" "}
-
- reddit.com/r/RooCode
-
- .
+ While we recommend keeping this feature enabled, you can disable it if needed.{" "}
+ {
+ e.preventDefault()
+ window.postMessage({ type: "action", action: "settingsButtonClicked" }, "*")
+ }}
+ style={{ display: "inline", padding: "0 2px" }}>
+ Open Settings
+ {" "}
+ and look for the "Enable automatic checkpoints" option in the Advanced Settings section.
)
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index d3e65a99ea..51ef4fe81d 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -52,7 +52,7 @@ const SettingsView = forwardRef(({ onDone },
alwaysAllowWrite,
alwaysApproveResubmit,
browserViewportSize,
- checkpointsEnabled,
+ enableCheckpoints,
diffEnabled,
experiments,
fuzzyMatchThreshold,
@@ -143,7 +143,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
- vscode.postMessage({ type: "checkpointsEnabled", bool: checkpointsEnabled })
+ vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -706,6 +706,25 @@ const SettingsView = forwardRef(({ onDone },
+
+
{
+ setCachedStateField("enableCheckpoints", e.target.checked)
+ }}>
+ Enable automatic checkpoints
+
+
+ When enabled, Roo will automatically create checkpoints during task execution, making it
+ easy to review changes or revert to earlier states.
+
+
+
(({ onDone },
)}
-
-
- ⚠️
- {
- setCachedStateField("checkpointsEnabled", e.target.checked)
- }}>
- Enable experimental checkpoints
-
-
-
- When enabled, Roo will save a checkpoint whenever a file in the workspace is modified,
- added or deleted, letting you easily revert to a previous state.
-
-
-
{Object.entries(experimentConfigsMap)
.filter((config) => config[0] !== "DIFF_STRATEGY")
.map((config) => (
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index ae5c5b9539..3dfc87de75 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -32,7 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setSoundEnabled: (value: boolean) => void
setSoundVolume: (value: number) => void
setDiffEnabled: (value: boolean) => void
- setCheckpointsEnabled: (value: boolean) => void
+ setEnableCheckpoints: (value: boolean) => void
setBrowserViewportSize: (value: string) => void
setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string
@@ -79,7 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: true,
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
writeDelayMs: 1000,
@@ -219,7 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
- setCheckpointsEnabled: (value) => setState((prevState) => ({ ...prevState, checkpointsEnabled: value })),
+ setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
setBrowserViewportSize: (value: string) =>
setState((prevState) => ({ ...prevState, browserViewportSize: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
From ea38d9ebbac80f4170f74b4c7d978e588b132d2d Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Thu, 27 Feb 2025 09:04:54 +0100
Subject: [PATCH 10/28] Enable prompt caching for Claude Sonnet 3.7 Vertex AI
model
---
src/shared/api.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 5cda333031..cd6aead1a5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -441,7 +441,7 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
From 0b583ed15ee80acdd853c6e994c1694f3d4f0cca Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 02:34:14 -0800
Subject: [PATCH 11/28] Fix AnthropicHandler#completePrompt
---
src/api/providers/anthropic.ts | 98 +++++++++++++++++-----------------
1 file changed, 49 insertions(+), 49 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 8c5a1795b1..eca81eab2e 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -30,29 +30,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- let { id: modelId, info: modelInfo } = this.getModel()
- const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens || 8192
- let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
- let thinking: BetaThinkingConfigParam | undefined = undefined
-
- // 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"
-
- // 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
- }
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
switch (modelId) {
case "claude-3-7-sonnet-20250219":
@@ -202,40 +180,62 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: AnthropicModelId; info: ModelInfo } {
+ getModel() {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
+ let thinking: BetaThinkingConfigParam | undefined = undefined
if (modelId && modelId in anthropicModels) {
- const id = modelId as AnthropicModelId
- return { id, info: anthropicModels[id] }
+ let id = modelId as AnthropicModelId
+ const info: ModelInfo = anthropicModels[id]
+
+ // 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.
+ if (id === "claude-3-7-sonnet-20250219:thinking") {
+ id = "claude-3-7-sonnet-20250219"
+ }
+
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ if (info.thinking) {
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ temperature = 1.0
+
+ // 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 }
+ }
+
+ return { id, info, temperature, maxTokens, thinking }
}
- return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
+ const id = anthropicDefaultModelId
+ const info: ModelInfo = anthropicModels[id]
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ return { id, info, temperature, maxTokens, thinking }
}
- async completePrompt(prompt: string): Promise {
- try {
- const response = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
- messages: [{ role: "user", content: prompt }],
- stream: false,
- })
+ async completePrompt(prompt: string) {
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
- const content = response.content[0]
+ const message = await this.client.messages.create({
+ model: modelId,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
+ messages: [{ role: "user", content: prompt }],
+ stream: false,
+ })
- if (content.type === "text") {
- return content.text
- }
-
- return ""
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(`Anthropic completion error: ${error.message}`)
- }
-
- throw error
- }
+ const content = message.content.find(({ type }) => type === "text")
+ return content?.type === "text" ? content.text : ""
}
}
From d66b5d2db62f0a6cb8650b8f465d14cf77bbcd36 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 02:40:39 -0800
Subject: [PATCH 12/28] Fix tests
---
src/api/providers/__tests__/anthropic.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/api/providers/__tests__/anthropic.test.ts b/src/api/providers/__tests__/anthropic.test.ts
index ff7bdb4054..82e098f65f 100644
--- a/src/api/providers/__tests__/anthropic.test.ts
+++ b/src/api/providers/__tests__/anthropic.test.ts
@@ -153,7 +153,7 @@ describe("AnthropicHandler", () => {
})
it("should handle API errors", async () => {
- mockCreate.mockRejectedValueOnce(new Error("API Error"))
+ mockCreate.mockRejectedValueOnce(new Error("Anthropic completion error: API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Anthropic completion error: API Error")
})
From 210afc681e799ade14fa2886e07cce0cb6aa2496 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:44:50 -0500
Subject: [PATCH 13/28] v3.7.7
---
.changeset/gorgeous-feet-dress.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
new file mode 100644
index 0000000000..fe2183052d
--- /dev/null
+++ b/.changeset/gorgeous-feet-dress.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+v3.7.7
From dc83617b4d2da06b830e848476a1c5d9179a361a Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:51:05 -0500
Subject: [PATCH 14/28] Revert "Stop removing commas from terminal output"
---
.changeset/sour-parents-hug.md | 5 -----
src/integrations/terminal/TerminalProcess.ts | 3 +++
2 files changed, 3 insertions(+), 5 deletions(-)
delete mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
deleted file mode 100644
index a24286b6bb..0000000000
--- a/.changeset/sour-parents-hug.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 4e85c10575..5597350db3 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,6 +110,9 @@ 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 8612ab574be39f863329c1b93b32d257bd67450f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 15:56:03 +0000
Subject: [PATCH 15/28] changeset version bump
---
.changeset/eighty-cheetahs-fetch.md | 5 -----
.changeset/gorgeous-feet-dress.md | 5 -----
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
5 files changed, 10 insertions(+), 13 deletions(-)
delete mode 100644 .changeset/eighty-cheetahs-fetch.md
delete mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
deleted file mode 100644
index ca103880c8..0000000000
--- a/.changeset/eighty-cheetahs-fetch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Graduate checkpoints out of beta
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
deleted file mode 100644
index fe2183052d..0000000000
--- a/.changeset/gorgeous-feet-dress.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13b0695335..ff5d8d6aaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Roo Code Changelog
+## 3.7.7
+
+### Patch Changes
+
+- Graduate checkpoints out of beta
+- v3.7.7
+
## [3.7.6]
- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
diff --git a/package-lock.json b/package-lock.json
index 808e2f2f10..c1f748983f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 463e9d597a..8441488bac 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.6",
+ "version": "3.7.7",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 4786815fe8bccf47af9202ff8cb99c6f1ef8ad16 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 11:02:13 -0500
Subject: [PATCH 16/28] Update CHANGELOG.md
---
CHANGELOG.md | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff5d8d6aaf..d0cf8f79c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,10 @@
# Roo Code Changelog
-## 3.7.7
-
-### Patch Changes
+## [3.7.7]
- Graduate checkpoints out of beta
-- v3.7.7
+- Fix enhance prompt button when using Thinking Sonnet
+- Add tooltips to make what buttons do more obvious
## [3.7.6]
From eec1769b6b5883d3179e2d1a370ed01b83078286 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 18:56:28 +0000
Subject: [PATCH 17/28] Added cache costs for Claude Sonnet 3.7 via Vertex AI
---
src/shared/api.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index e7e4c54db6..d2b4ed728f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -444,6 +444,8 @@ export const vertexModels = {
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
From 1f0211ee6418752201b7d9b34ffb12608ba4a54d Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 23 Feb 2025 20:52:10 -0600
Subject: [PATCH 18/28] Allow users to set custom system prompts
---
src/__mocks__/fs/promises.ts | 1 -
src/__mocks__/jest.setup.ts | 30 +++
.../__tests__/custom-system-prompt.test.ts | 172 ++++++++++++++++++
.../prompts/sections/custom-system-prompt.ts | 60 ++++++
src/core/prompts/system.ts | 15 ++
.../src/components/prompts/PromptsView.tsx | 40 ++++
6 files changed, 317 insertions(+), 1 deletion(-)
create mode 100644 src/core/prompts/__tests__/custom-system-prompt.test.ts
create mode 100644 src/core/prompts/sections/custom-system-prompt.ts
diff --git a/src/__mocks__/fs/promises.ts b/src/__mocks__/fs/promises.ts
index d5f076247a..e496a7fa51 100644
--- a/src/__mocks__/fs/promises.ts
+++ b/src/__mocks__/fs/promises.ts
@@ -140,7 +140,6 @@ const mockFs = {
currentPath += "/" + parts[parts.length - 1]
mockDirectories.add(currentPath)
return Promise.resolve()
- return Promise.resolve()
}),
access: jest.fn().mockImplementation(async (path: string) => {
diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts
index 6bd00e9567..836279bfe4 100644
--- a/src/__mocks__/jest.setup.ts
+++ b/src/__mocks__/jest.setup.ts
@@ -15,3 +15,33 @@ jest.mock("../utils/logging", () => ({
}),
},
}))
+
+// Add toPosix method to String prototype for all tests, mimicking src/utils/path.ts
+// This is needed because the production code expects strings to have this method
+// Note: In production, this is added via import in the entry point (extension.ts)
+export {}
+
+declare global {
+ interface String {
+ toPosix(): string
+ }
+}
+
+// Implementation that matches src/utils/path.ts
+function toPosixPath(p: string) {
+ // Extended-Length Paths in Windows start with "\\?\" to allow longer paths
+ // and bypass usual parsing. If detected, we return the path unmodified.
+ const isExtendedLengthPath = p.startsWith("\\\\?\\")
+
+ if (isExtendedLengthPath) {
+ return p
+ }
+
+ return p.replace(/\\/g, "/")
+}
+
+if (!String.prototype.toPosix) {
+ String.prototype.toPosix = function (this: string): string {
+ return toPosixPath(this)
+ }
+}
diff --git a/src/core/prompts/__tests__/custom-system-prompt.test.ts b/src/core/prompts/__tests__/custom-system-prompt.test.ts
new file mode 100644
index 0000000000..7594c13e6d
--- /dev/null
+++ b/src/core/prompts/__tests__/custom-system-prompt.test.ts
@@ -0,0 +1,172 @@
+import { SYSTEM_PROMPT } from "../system"
+import { defaultModeSlug, modes } from "../../../shared/modes"
+import * as vscode from "vscode"
+import * as fs from "fs/promises"
+
+// Mock the fs/promises module
+jest.mock("fs/promises", () => ({
+ readFile: jest.fn(),
+ mkdir: jest.fn().mockResolvedValue(undefined),
+ access: jest.fn().mockResolvedValue(undefined),
+}))
+
+// Get the mocked fs module
+const mockedFs = fs as jest.Mocked
+
+// Mock the fileExistsAtPath function
+jest.mock("../../../utils/fs", () => ({
+ fileExistsAtPath: jest.fn().mockResolvedValue(true),
+ createDirectoriesForFile: jest.fn().mockResolvedValue([]),
+}))
+
+// Create a mock ExtensionContext with relative paths instead of absolute paths
+const mockContext = {
+ extensionPath: "mock/extension/path",
+ globalStoragePath: "mock/storage/path",
+ storagePath: "mock/storage/path",
+ logPath: "mock/log/path",
+ subscriptions: [],
+ workspaceState: {
+ get: () => undefined,
+ update: () => Promise.resolve(),
+ },
+ globalState: {
+ get: () => undefined,
+ update: () => Promise.resolve(),
+ setKeysForSync: () => {},
+ },
+ extensionUri: { fsPath: "mock/extension/path" },
+ globalStorageUri: { fsPath: "mock/settings/path" },
+ asAbsolutePath: (relativePath: string) => `mock/extension/path/${relativePath}`,
+ extension: {
+ packageJSON: {
+ version: "1.0.0",
+ },
+ },
+} as unknown as vscode.ExtensionContext
+
+describe("File-Based Custom System Prompt", () => {
+ const experiments = {}
+
+ beforeEach(() => {
+ // Reset mocks before each test
+ jest.clearAllMocks()
+
+ // Default behavior: file doesn't exist
+ mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
+ })
+
+ it("should use default generation when no file-based system prompt is found", async () => {
+ const customModePrompts = {
+ [defaultModeSlug]: {
+ roleDefinition: "Test role definition",
+ },
+ }
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ customModePrompts,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain default sections
+ expect(prompt).toContain("TOOL USE")
+ expect(prompt).toContain("CAPABILITIES")
+ expect(prompt).toContain("MODES")
+ expect(prompt).toContain("Test role definition")
+ })
+
+ it("should use file-based custom system prompt when available", async () => {
+ // Mock the readFile to return content from a file
+ const fileCustomSystemPrompt = "Custom system prompt from file"
+ // When called with utf-8 encoding, return a string
+ mockedFs.readFile.mockImplementation((filePath, options) => {
+ if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+ return Promise.resolve(fileCustomSystemPrompt)
+ }
+ return Promise.reject({ code: "ENOENT" })
+ })
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain role definition and file-based system prompt
+ expect(prompt).toContain(modes[0].roleDefinition)
+ expect(prompt).toContain(fileCustomSystemPrompt)
+
+ // Should not contain any of the default sections
+ expect(prompt).not.toContain("TOOL USE")
+ expect(prompt).not.toContain("CAPABILITIES")
+ expect(prompt).not.toContain("MODES")
+ })
+
+ it("should combine file-based system prompt with role definition and custom instructions", async () => {
+ // Mock the readFile to return content from a file
+ const fileCustomSystemPrompt = "Custom system prompt from file"
+ mockedFs.readFile.mockImplementation((filePath, options) => {
+ if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+ return Promise.resolve(fileCustomSystemPrompt)
+ }
+ return Promise.reject({ code: "ENOENT" })
+ })
+
+ // Define custom role definition
+ const customRoleDefinition = "Custom role definition"
+ const customModePrompts = {
+ [defaultModeSlug]: {
+ roleDefinition: customRoleDefinition,
+ },
+ }
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ customModePrompts,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain custom role definition and file-based system prompt
+ expect(prompt).toContain(customRoleDefinition)
+ expect(prompt).toContain(fileCustomSystemPrompt)
+
+ // Should not contain any of the default sections
+ expect(prompt).not.toContain("TOOL USE")
+ expect(prompt).not.toContain("CAPABILITIES")
+ expect(prompt).not.toContain("MODES")
+ })
+})
diff --git a/src/core/prompts/sections/custom-system-prompt.ts b/src/core/prompts/sections/custom-system-prompt.ts
new file mode 100644
index 0000000000..eca2b98b8d
--- /dev/null
+++ b/src/core/prompts/sections/custom-system-prompt.ts
@@ -0,0 +1,60 @@
+import fs from "fs/promises"
+import path from "path"
+import { Mode } from "../../../shared/modes"
+import { fileExistsAtPath } from "../../../utils/fs"
+
+/**
+ * Safely reads a file, returning an empty string if the file doesn't exist
+ */
+async function safeReadFile(filePath: string): Promise {
+ try {
+ const content = await fs.readFile(filePath, "utf-8")
+ // When reading with "utf-8" encoding, content should be a string
+ return content.trim()
+ } catch (err) {
+ const errorCode = (err as NodeJS.ErrnoException).code
+ if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) {
+ throw err
+ }
+ return ""
+ }
+}
+
+/**
+ * Get the path to a system prompt file for a specific mode
+ */
+export function getSystemPromptFilePath(cwd: string, mode: Mode): string {
+ return path.join(cwd, ".roo", `system-prompt-${mode}`)
+}
+
+/**
+ * Loads custom system prompt from a file at .roo/system-prompt-[mode slug]
+ * If the file doesn't exist, returns an empty string
+ */
+export async function loadSystemPromptFile(cwd: string, mode: Mode): Promise {
+ const filePath = getSystemPromptFilePath(cwd, mode)
+ return safeReadFile(filePath)
+}
+
+/**
+ * Ensures the .roo directory exists, creating it if necessary
+ */
+export async function ensureRooDirectory(cwd: string): Promise {
+ const rooDir = path.join(cwd, ".roo")
+
+ // Check if directory already exists
+ if (await fileExistsAtPath(rooDir)) {
+ return
+ }
+
+ // Create the directory
+ try {
+ await fs.mkdir(rooDir, { recursive: true })
+ } catch (err) {
+ // If directory already exists (race condition), ignore the error
+ const errorCode = (err as NodeJS.ErrnoException).code
+ if (errorCode !== "EEXIST") {
+ throw err
+ }
+ }
+}
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index 91bbd07387..90791f6358 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -23,6 +23,7 @@ import {
getModesSection,
addCustomInstructions,
} from "./sections"
+import { loadSystemPromptFile } from "./sections/custom-system-prompt"
import fs from "fs/promises"
import path from "path"
@@ -119,11 +120,25 @@ export const SYSTEM_PROMPT = async (
return undefined
}
+ // Try to load custom system prompt from file
+ const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode)
+
// Check if it's a custom mode
const promptComponent = getPromptComponent(customModePrompts?.[mode])
+
// Get full mode config from custom modes or fall back to built-in modes
const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
+ // If a file-based custom system prompt exists, use it
+ if (fileCustomSystemPrompt) {
+ const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
+ return `${roleDefinition}
+
+${fileCustomSystemPrompt}
+
+${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}`
+ }
+
// If diff is disabled, don't pass the diffStrategy
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index 061fa789de..2bfafeff5c 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -88,6 +88,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [showConfigMenu, setShowConfigMenu] = useState(false)
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE")
+ const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false)
// Direct update functions
const updateAgentPrompt = useCallback(
@@ -971,6 +972,45 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
+ {/* Custom System Prompt Disclosure */}
+
+
setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
+ className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
+ aria-expanded={isSystemPromptDisclosureOpen}>
+
+ Advanced: Override System Prompt
+
+
+ {isSystemPromptDisclosureOpen && (
+
+ You can completely replace the system prompt for this mode (aside from the role
+ definition and custom instructions) by creating a file at{" "}
+ {
+ const currentMode = getCurrentMode()
+ if (!currentMode) return
+
+ // Open or create an empty file
+ vscode.postMessage({
+ type: "openFile",
+ text: `./.roo/system-prompt-${currentMode.slug}`,
+ values: {
+ create: true,
+ content: "",
+ },
+ })
+ }}>
+ .roo/system-prompt-{getCurrentMode()?.slug || "code"}
+ {" "}
+ in your workspace. This is a very advanced feature that bypasses built-in safeguards and
+ consistency checks (especially around tool usage), so be careful!
+
+ )}
+
Date: Thu, 27 Feb 2025 15:18:54 -0500
Subject: [PATCH 19/28] Add gpt-4.5-preview
---
.changeset/flat-avocados-carry.md | 5 +++++
src/api/providers/__tests__/openai-native.test.ts | 2 +-
src/shared/api.ts | 10 +++++++++-
3 files changed, 15 insertions(+), 2 deletions(-)
create mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
new file mode 100644
index 0000000000..f0128f21e0
--- /dev/null
+++ b/.changeset/flat-avocados-carry.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add gpt-4.5-preview
diff --git a/src/api/providers/__tests__/openai-native.test.ts b/src/api/providers/__tests__/openai-native.test.ts
index d6a855849c..eda744c335 100644
--- a/src/api/providers/__tests__/openai-native.test.ts
+++ b/src/api/providers/__tests__/openai-native.test.ts
@@ -357,7 +357,7 @@ describe("OpenAiNativeHandler", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.apiModelId)
expect(modelInfo.info).toBeDefined()
- expect(modelInfo.info.maxTokens).toBe(4096)
+ expect(modelInfo.info.maxTokens).toBe(16384)
expect(modelInfo.info.contextWindow).toBe(128_000)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..47b023ce6f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -678,8 +678,16 @@ export const openAiNativeModels = {
inputPrice: 1.1,
outputPrice: 4.4,
},
+ "gpt-4.5-preview": {
+ maxTokens: 16_384,
+ contextWindow: 128_000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 75,
+ outputPrice: 150,
+ },
"gpt-4o": {
- maxTokens: 4_096,
+ maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
From 820ebc97c5251e04194b014bd60ab51e08de1481 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 20:41:23 +0000
Subject: [PATCH 20/28] changeset version bump
---
.changeset/flat-avocados-carry.md | 5 -----
CHANGELOG.md | 6 ++++++
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
deleted file mode 100644
index f0128f21e0..0000000000
--- a/.changeset/flat-avocados-carry.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add gpt-4.5-preview
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0cf8f79c3..e3aa95d448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Roo Code Changelog
+## 3.7.8
+
+### Patch Changes
+
+- Add gpt-4.5-preview
+
## [3.7.7]
- Graduate checkpoints out of beta
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..e7d7718b75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 8441488bac..a4a2298a48 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.7",
+ "version": "3.7.8",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From ca7d746990ace8208b5f444ae6f16ea0f25525a1 Mon Sep 17 00:00:00 2001
From: R00-B0T
Date: Thu, 27 Feb 2025 20:41:50 +0000
Subject: [PATCH 21/28] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3aa95d448..fe8156abf7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,6 @@
# Roo Code Changelog
-## 3.7.8
-
-### Patch Changes
+## [3.7.8]
- Add gpt-4.5-preview
From 75e7ef728d0c8512a2f8835a3139bba194f8b327 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 16:09:28 -0500
Subject: [PATCH 22/28] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe8156abf7..9622ce0c99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
## [3.7.8]
+- Add Vertex AI prompt caching support for Claude models (thanks @aitoroses and @lupuletic!)
- Add gpt-4.5-preview
+- Add an advanced feature to customize the system prompt
## [3.7.7]
From 3514f6506b5b0e24919bad29e65b8eba11afced5 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 21:56:56 +0000
Subject: [PATCH 23/28] Added support for Claude Sonnet 3.7 thinking via Vertex
AI
---
package-lock.json | 10 +-
package.json | 2 +-
src/api/providers/vertex.ts | 110 +++++++++++++++---
src/core/webview/ClineProvider.ts | 5 +
src/shared/api.ts | 14 +++
src/shared/globalState.ts | 2 +
.../src/components/settings/ApiOptions.tsx | 3 +
.../components/settings/ThinkingBudget.tsx | 30 +++--
8 files changed, 143 insertions(+), 33 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..547f20a930 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
- "@anthropic-ai/vertex-sdk": "^0.4.1",
+ "@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
@@ -150,11 +150,11 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/@anthropic-ai/vertex-sdk": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.3.tgz",
- "integrity": "sha512-2Uef0C5P2Hx+T88RnUSRA3u4aZqmqnrRSOb2N64ozgKPiSUPTM5JlggAq2b32yWMj5d3MLYa6spJXKMmHXOcoA==",
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.7.0.tgz",
+ "integrity": "sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==",
"dependencies": {
- "@anthropic-ai/sdk": ">=0.14 <1",
+ "@anthropic-ai/sdk": ">=0.35 <1",
"google-auth-library": "^9.4.2"
}
},
diff --git a/package.json b/package.json
index 8441488bac..35db01621a 100644
--- a/package.json
+++ b/package.json
@@ -305,7 +305,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
- "@anthropic-ai/vertex-sdk": "^0.4.1",
+ "@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 70562766c3..69fb7d26f7 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler, SingleCompletionHandler } from "../"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
@@ -70,15 +71,25 @@ interface VertexMessageStreamEvent {
usage?: {
output_tokens: number
}
- content_block?: {
- type: "text"
- text: string
- }
+ content_block?:
+ | {
+ type: "text"
+ text: string
+ }
+ | {
+ type: "thinking"
+ thinking: string
+ }
index?: number
- delta?: {
- type: "text_delta"
- text: string
- }
+ delta?:
+ | {
+ type: "text_delta"
+ text: string
+ }
+ | {
+ type: "thinking_delta"
+ thinking: string
+ }
}
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
@@ -145,6 +156,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
+ let { id, info, temperature, maxTokens, thinking } = model
const useCache = model.info.supportsPromptCache
// Find indices of user messages that we want to cache
@@ -158,9 +170,10 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
// Create the stream with appropriate caching configuration
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
// Cache the system prompt if caching is enabled
system: useCache
? [
@@ -220,6 +233,19 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking": {
+ if (chunk.index! > 0) {
+ yield {
+ type: "reasoning",
+ text: "\n",
+ }
+ }
+ yield {
+ type: "reasoning",
+ text: (chunk.content_block as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -232,6 +258,13 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking_delta": {
+ yield {
+ type: "reasoning",
+ text: (chunk.delta as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -239,24 +272,63 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: VertexModelId; info: ModelInfo } {
+ getModel(): {
+ id: VertexModelId
+ info: ModelInfo
+ temperature: number
+ maxTokens: number
+ thinking?: BetaThinkingConfigParam
+ } {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? 0
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
if (modelId && modelId in vertexModels) {
const id = modelId as VertexModelId
- return { id, info: vertexModels[id] }
+ const info: ModelInfo = vertexModels[id]
+
+ // The `:thinking` variant is a virtual identifier for thinking-enabled models
+ // Similar to how it's handled in the Anthropic provider
+ let actualId = id
+ if (id.endsWith(":thinking")) {
+ actualId = id.replace(":thinking", "") as VertexModelId
+ }
+
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ if (info.thinking) {
+ temperature = 1.0 // Thinking requires temperature 1.0
+ const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(
+ this.options.vertexThinking ?? this.options.anthropicThinking ?? maxBudgetTokens,
+ maxBudgetTokens,
+ ),
+ 1024,
+ )
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
+ }
+
+ return { id: actualId, info, temperature, maxTokens, thinking }
}
- return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
+
+ const id = vertexDefaultModelId
+ const info = vertexModels[id]
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ return { id, info, temperature, maxTokens, thinking }
}
async completePrompt(prompt: string): Promise {
try {
- const model = this.getModel()
- const useCache = model.info.supportsPromptCache
+ let { id, info, temperature, maxTokens, thinking } = this.getModel()
+ const useCache = info.supportsPromptCache
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
system: "", // No system prompt needed for single completions
messages: [
{
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 633c7d7293..5417e54ff7 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1652,6 +1652,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1701,6 +1702,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
this.updateGlobalState("anthropicThinking", anthropicThinking),
+ this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -2158,6 +2160,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2242,6 +2245,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
this.getGlobalState("anthropicThinking") as Promise,
+ this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2343,6 +2347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..f048761d0f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -41,6 +41,7 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
+ vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -436,6 +437,18 @@ export const openRouterDefaultModelInfo: ModelInfo = {
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
export const vertexModels = {
+ "claude-3-7-sonnet@20250219:thinking": {
+ maxTokens: 64000,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0,
+ outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
+ thinking: true,
+ },
"claude-3-7-sonnet@20250219": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -446,6 +459,7 @@ export const vertexModels = {
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
+ thinking: false,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 0863b34db2..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,6 +24,7 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
+ | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c30035cef0..42ac5cdcb3 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -7,6 +7,7 @@ import * as vscodemodels from "vscode"
import {
ApiConfiguration,
ModelInfo,
+ ApiProvider,
anthropicDefaultModelId,
anthropicModels,
azureOpenAiDefaultApiVersion,
@@ -1380,9 +1381,11 @@ const ApiOptions = ({
/>
(field: K, value: ApiConfiguration[K]) => void
modelInfo?: ModelInfo
+ provider?: ApiProvider
}
-export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+export const ThinkingBudget = ({
+ apiConfiguration,
+ setApiConfigurationField,
+ modelInfo,
+ provider,
+}: ThinkingBudgetProps) => {
+ const isVertexProvider = provider === "vertex"
+ const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
+
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
- const thinkingTokens = apiConfiguration?.anthropicThinking || 8192
+ // Get the appropriate thinking tokens based on provider
+ const thinkingTokens = useMemo(() => {
+ const value = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ return value || Math.min(Math.floor(0.8 * tokens), 8192)
+ }, [apiConfiguration, isVertexProvider, tokens])
+
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField("anthropicThinking", thinkingTokensMax)
+ setApiConfigurationField(budgetField, thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
- if (!modelInfo || !modelInfo.thinking) {
+ if (!modelInfo?.thinking) {
return null
}
@@ -52,7 +66,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField("anthropicThinking", value)}
+ onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
/>
{thinkingTokens}
From 5eba1d53fbeef6f71f027d8317d9f99d120e8026 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:15:17 +0000
Subject: [PATCH 24/28] Added tests for Claude Sonnet Thinking
---
src/api/providers/__tests__/vertex.test.ts | 250 ++++++++++++++++++
.../settings/__tests__/ApiOptions.test.tsx | 57 +++-
.../__tests__/ThinkingBudget.test.tsx | 145 ++++++++++
3 files changed, 451 insertions(+), 1 deletion(-)
create mode 100644 webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 6e81fd771b..076f902ca2 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -2,6 +2,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { VertexHandler } from "../vertex"
import { ApiStreamChunk } from "../../transform/stream"
@@ -431,6 +432,138 @@ describe("VertexHandler", () => {
})
})
+ describe("thinking functionality", () => {
+ const mockMessages: Anthropic.Messages.MessageParam[] = [
+ {
+ role: "user",
+ content: "Hello",
+ },
+ ]
+
+ const systemPrompt = "You are a helpful assistant"
+
+ it("should handle thinking content blocks and deltas", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "thinking",
+ thinking: "Let me think about this...",
+ },
+ },
+ {
+ type: "content_block_delta",
+ delta: {
+ type: "thinking_delta",
+ thinking: " I need to consider all options.",
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 1,
+ content_block: {
+ type: "text",
+ text: "Here's my answer:",
+ },
+ },
+ ]
+
+ // Setup async iterator for mock stream
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ // Verify thinking content is processed correctly
+ const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
+ expect(reasoningChunks).toHaveLength(2)
+ expect(reasoningChunks[0].text).toBe("Let me think about this...")
+ expect(reasoningChunks[1].text).toBe(" I need to consider all options.")
+
+ // Verify text content is processed correctly
+ const textChunks = chunks.filter((chunk) => chunk.type === "text")
+ expect(textChunks).toHaveLength(2) // One for the text block, one for the newline
+ expect(textChunks[0].text).toBe("\n")
+ expect(textChunks[1].text).toBe("Here's my answer:")
+ })
+
+ it("should handle multiple thinking blocks with line breaks", async () => {
+ const mockStream = [
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "thinking",
+ thinking: "First thinking block",
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 1,
+ content_block: {
+ type: "thinking",
+ thinking: "Second thinking block",
+ },
+ },
+ ]
+
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ expect(chunks.length).toBe(3)
+ expect(chunks[0]).toEqual({
+ type: "reasoning",
+ text: "First thinking block",
+ })
+ expect(chunks[1]).toEqual({
+ type: "reasoning",
+ text: "\n",
+ })
+ expect(chunks[2]).toEqual({
+ type: "reasoning",
+ text: "Second thinking block",
+ })
+ })
+ })
+
describe("completePrompt", () => {
it("should complete prompt successfully", async () => {
const result = await handler.completePrompt("Test prompt")
@@ -500,4 +633,121 @@ describe("VertexHandler", () => {
expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model
})
})
+
+ describe("thinking model configuration", () => {
+ it("should configure thinking for models with :thinking suffix", () => {
+ const thinkingHandler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 4096,
+ })
+
+ const modelInfo = thinkingHandler.getModel()
+
+ // Verify thinking configuration
+ expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219")
+ expect(modelInfo.thinking).toBeDefined()
+ const thinkingConfig = modelInfo.thinking as { type: "enabled"; budget_tokens: number }
+ expect(thinkingConfig.type).toBe("enabled")
+ expect(thinkingConfig.budget_tokens).toBe(4096)
+ expect(modelInfo.temperature).toBe(1.0) // Thinking requires temperature 1.0
+ })
+
+ it("should calculate thinking budget correctly", () => {
+ // Test with explicit thinking budget
+ const handlerWithBudget = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 5000,
+ })
+
+ expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
+
+ // Test with default thinking budget (80% of max tokens)
+ const handlerWithDefaultBudget = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 10000,
+ })
+
+ expect((handlerWithDefaultBudget.getModel().thinking as any).budget_tokens).toBe(8000) // 80% of 10000
+
+ // Test with minimum thinking budget (should be at least 1024)
+ const handlerWithSmallMaxTokens = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024
+ })
+
+ expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
+ })
+
+ it("should use anthropicThinking value if vertexThinking is not provided", () => {
+ const handler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ anthropicThinking: 6000, // Should be used as fallback
+ })
+
+ expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
+ })
+
+ it("should pass thinking configuration to API", async () => {
+ const thinkingHandler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 4096,
+ })
+
+ const mockCreate = jest.fn().mockImplementation(async (options) => {
+ if (!options.stream) {
+ return {
+ id: "test-completion",
+ content: [{ type: "text", text: "Test response" }],
+ role: "assistant",
+ model: options.model,
+ usage: {
+ input_tokens: 10,
+ output_tokens: 5,
+ },
+ }
+ }
+ return {
+ async *[Symbol.asyncIterator]() {
+ yield {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 5,
+ },
+ },
+ }
+ },
+ }
+ })
+ ;(thinkingHandler["client"].messages as any).create = mockCreate
+
+ await thinkingHandler
+ .createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }])
+ .next()
+
+ expect(mockCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ thinking: { type: "enabled", budget_tokens: 4096 },
+ temperature: 1.0, // Thinking requires temperature 1.0
+ }),
+ )
+ })
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 73394bae10..65ae137003 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -46,6 +46,21 @@ jest.mock("../TemperatureControl", () => ({
),
}))
+// Mock ThinkingBudget component
+jest.mock("../ThinkingBudget", () => ({
+ ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
+ modelInfo?.thinking ? (
+
+
+
+ ) : null,
+}))
+
describe("ApiOptions", () => {
const renderApiOptions = (props = {}) => {
render(
@@ -72,5 +87,45 @@ describe("ApiOptions", () => {
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
})
- //TODO: More test cases needed
+ describe("thinking functionality", () => {
+ it("should show ThinkingBudget for Anthropic models that support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "anthropic",
+ apiModelId: "claude-3-7-sonnet-20250219:thinking",
+ },
+ })
+
+ expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+ expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "anthropic")
+ })
+
+ it("should show ThinkingBudget for Vertex models that support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "vertex",
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ },
+ })
+
+ expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+ expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "vertex")
+ })
+
+ it("should not show ThinkingBudget for models that don't support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "anthropic",
+ apiModelId: "claude-3-opus-20240229",
+ modelInfo: { thinking: false }, // Non-thinking model
+ },
+ })
+
+ expect(screen.queryByTestId("thinking-budget")).not.toBeInTheDocument()
+ })
+
+ // Note: We don't need to test the actual ThinkingBudget component functionality here
+ // since we have separate tests for that component. We just need to verify that
+ // it's included in the ApiOptions component when appropriate.
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
new file mode 100644
index 0000000000..54f6b1037b
--- /dev/null
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -0,0 +1,145 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react"
+import { ThinkingBudget } from "../ThinkingBudget"
+import { ApiProvider, ModelInfo } from "../../../../../src/shared/api"
+
+// Mock Slider component
+jest.mock("@/components/ui", () => ({
+ Slider: ({ value, onValueChange, min, max }: any) => (
+ onValueChange([parseInt(e.target.value)])}
+ />
+ ),
+}))
+
+describe("ThinkingBudget", () => {
+ const mockModelInfo: ModelInfo = {
+ thinking: true,
+ maxTokens: 16384,
+ contextWindow: 200000,
+ supportsPromptCache: true,
+ supportsImages: true,
+ }
+ const defaultProps = {
+ apiConfiguration: {},
+ setApiConfigurationField: jest.fn(),
+ modelInfo: mockModelInfo,
+ provider: "anthropic" as ApiProvider,
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("should render nothing when model doesn't support thinking", () => {
+ const { container } = render(
+ ,
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it("should render sliders when model supports thinking", () => {
+ render( )
+
+ expect(screen.getAllByTestId("slider")).toHaveLength(2)
+ })
+
+ it("should use anthropicThinking field for Anthropic provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ })
+
+ it("should use vertexThinking field for Vertex provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ })
+
+ it("should cap thinking tokens at 80% of max tokens", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ // Effect should trigger and cap the value
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ })
+
+ it("should use default thinking tokens if not provided", () => {
+ render( )
+
+ // Default is 80% of max tokens, capped at 8192
+ const sliders = screen.getAllByTestId("slider")
+ expect(sliders[1]).toHaveValue("8000") // 80% of 10000
+ })
+
+ it("should use min thinking tokens of 1024", () => {
+ render( )
+
+ const sliders = screen.getAllByTestId("slider")
+ expect(sliders[1].getAttribute("min")).toBe("1024")
+ })
+
+ it("should update max tokens when slider changes", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[0], { target: { value: "12000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
+ })
+})
From 2b3d23ebd750bfaf19efd6fbcc5bbcc8f1cb3aef Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:17:09 +0000
Subject: [PATCH 25/28] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 87b70cef83bafcf8ea4751de165ba41c35b38ba2 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:20:35 +0000
Subject: [PATCH 26/28] Removed unnecessary comment
---
src/shared/globalState.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From dd4fb6b3097430f98345e85d4c563e29baade089 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:45:12 +0000
Subject: [PATCH 27/28] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 8cbce2ded08e107454dee7d2eec256973d3e85e0 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 16:06:47 -0800
Subject: [PATCH 28/28] Add provider-agnostic modelMaxThinkingTokens setting
---
src/api/providers/__tests__/vertex.test.ts | 18 +++---------------
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 2 +-
src/api/providers/vertex.ts | 5 +----
src/core/webview/ClineProvider.ts | 15 +++++----------
.../__tests__/checkExistApiConfig.test.ts | 2 +-
src/shared/api.ts | 3 +--
src/shared/globalState.ts | 3 +--
.../src/components/settings/ThinkingBudget.tsx | 13 +++++--------
.../settings/__tests__/ApiOptions.test.tsx | 7 +------
.../settings/__tests__/ThinkingBudget.test.tsx | 16 ++++++++--------
11 files changed, 28 insertions(+), 58 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 076f902ca2..9cf92f0a16 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -641,7 +641,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const modelInfo = thinkingHandler.getModel()
@@ -662,7 +662,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 5000,
+ modelMaxThinkingTokens: 5000,
})
expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
@@ -688,25 +688,13 @@ describe("VertexHandler", () => {
expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
})
- it("should use anthropicThinking value if vertexThinking is not provided", () => {
- const handler = new VertexHandler({
- apiModelId: "claude-3-7-sonnet@20250219:thinking",
- vertexProjectId: "test-project",
- vertexRegion: "us-central1",
- modelMaxTokens: 16384,
- anthropicThinking: 6000, // Should be used as fallback
- })
-
- expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
- })
-
it("should pass thinking configuration to API", async () => {
const thinkingHandler = new VertexHandler({
apiModelId: "claude-3-7-sonnet@20250219:thinking",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const mockCreate = jest.fn().mockImplementation(async (options) => {
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index eca81eab2e..fc0b99c59b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -206,7 +206,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69bcb0074c..82c02e20a7 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -117,7 +117,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 69fb7d26f7..a25fad07ee 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -300,10 +300,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
temperature = 1.0 // Thinking requires temperature 1.0
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(
- this.options.vertexThinking ?? this.options.anthropicThinking ?? maxBudgetTokens,
- maxBudgetTokens,
- ),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
thinking = { type: "enabled", budget_tokens: budgetTokens }
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5417e54ff7..7b6f2c8971 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1651,8 +1651,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1673,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
} = apiConfiguration
await Promise.all([
this.updateGlobalState("apiProvider", apiProvider),
@@ -1701,8 +1700,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
- this.updateGlobalState("anthropicThinking", anthropicThinking),
- this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -1723,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
this.updateGlobalState("modelMaxTokens", modelMaxTokens),
+ this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
])
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2159,8 +2157,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2216,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
maxOpenTabsContext,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
@@ -2244,8 +2241,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioModelId") as Promise,
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
- this.getGlobalState("anthropicThinking") as Promise,
- this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2301,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestyModelInfo") as Promise,
this.getGlobalState("modelTemperature") as Promise,
this.getGlobalState("modelMaxTokens") as Promise,
+ this.getGlobalState("anthropicThinking") as Promise,
this.getGlobalState("maxOpenTabsContext") as Promise,
])
@@ -2346,8 +2342,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2368,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
},
lastShownAnnouncementId,
customInstructions,
diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts
index 62517d6958..c99ddddbc4 100644
--- a/src/shared/__tests__/checkExistApiConfig.test.ts
+++ b/src/shared/__tests__/checkExistApiConfig.test.ts
@@ -32,7 +32,7 @@ describe("checkExistKey", () => {
apiKey: "test-key",
apiProvider: undefined,
anthropicBaseUrl: undefined,
- anthropicThinking: undefined,
+ modelMaxThinkingTokens: undefined,
}
expect(checkExistKey(config)).toBe(true)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index b36781d630..f88bb5e8b5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -21,7 +21,6 @@ export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
anthropicBaseUrl?: string
- anthropicThinking?: number
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
glamaModelId?: string
glamaModelInfo?: ModelInfo
@@ -41,7 +40,6 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
- vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -70,6 +68,7 @@ export interface ApiHandlerOptions {
requestyModelInfo?: ModelInfo
modelTemperature?: number
modelMaxTokens?: number
+ modelMaxThinkingTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..aabc77cc01 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,7 +24,6 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
- | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,7 +42,6 @@ export type GlobalStateKey =
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
- | "anthropicThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
@@ -83,5 +81,6 @@ export type GlobalStateKey =
| "unboundModelInfo"
| "modelTemperature"
| "modelMaxTokens"
+ | "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
| "mistralCodestralUrl"
| "maxOpenTabsContext"
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
index d21e1fb7ea..557a69538d 100644
--- a/webview-ui/src/components/settings/ThinkingBudget.tsx
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -17,27 +17,24 @@ export const ThinkingBudget = ({
modelInfo,
provider,
}: ThinkingBudgetProps) => {
- const isVertexProvider = provider === "vertex"
- const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
-
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
// Get the appropriate thinking tokens based on provider
const thinkingTokens = useMemo(() => {
- const value = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ const value = apiConfiguration?.modelMaxThinkingTokens
return value || Math.min(Math.floor(0.8 * tokens), 8192)
- }, [apiConfiguration, isVertexProvider, tokens])
+ }, [apiConfiguration, tokens])
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField(budgetField, thinkingTokensMax)
+ setApiConfigurationField("modelMaxThinkingTokens", thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
if (!modelInfo?.thinking) {
return null
@@ -66,7 +63,7 @@ export const ThinkingBudget = ({
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
+ onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
/>
{thinkingTokens}
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 65ae137003..06ed95585a 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,12 +51,7 @@ jest.mock("../ThinkingBudget", () => ({
ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
modelInfo?.thinking ? (
-
+
) : null,
}))
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
index 54f6b1037b..212316ea9a 100644
--- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -60,13 +60,13 @@ describe("ThinkingBudget", () => {
expect(screen.getAllByTestId("slider")).toHaveLength(2)
})
- it("should use anthropicThinking field for Anthropic provider", () => {
+ it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -75,16 +75,16 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
- it("should use vertexThinking field for Vertex provider", () => {
+ it("should use modelMaxThinkingTokens field for Vertex provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -93,7 +93,7 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
it("should cap thinking tokens at 80% of max tokens", () => {
@@ -102,13 +102,13 @@ describe("ThinkingBudget", () => {
render(
,
)
// Effect should trigger and cap the value
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 8000) // 80% of 10000
})
it("should use default thinking tokens if not provided", () => {