mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Resolved merge conflicts in Cline.ts and ClineProvider.ts
This commit is contained in:
commit
03e0aa545b
40 changed files with 1777 additions and 269 deletions
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Handle really long text in the ChatRow similar to TaskHeader
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Support multiple files in drag-and-drop
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Better OpenRouter error handling
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -1,5 +1,25 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [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]
|
||||
|
||||
- Graduate checkpoints out of beta
|
||||
- Fix enhance prompt button when using Thinking Sonnet
|
||||
- Add tooltips to make what buttons do more obvious
|
||||
|
||||
## [3.7.6]
|
||||
|
||||
- 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 (no more "Provider Error")
|
||||
- Add slider to control max output tokens for thinking models
|
||||
|
||||
## [3.7.5]
|
||||
|
||||
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "roo-cline",
|
||||
"version": "3.7.5",
|
||||
"version": "3.7.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "roo-cline",
|
||||
"version": "3.7.5",
|
||||
"version": "3.7.8",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.8",
|
||||
"icon": "assets/icons/rocket.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
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"
|
||||
|
||||
// Mock Vertex SDK
|
||||
jest.mock("@anthropic-ai/vertex-sdk", () => ({
|
||||
|
|
@ -128,7 +130,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 +160,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 +219,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 +253,315 @@ 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("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", () => {
|
||||
|
|
@ -240,7 +572,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,
|
||||
})
|
||||
})
|
||||
|
|
@ -295,4 +633,109 @@ 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,
|
||||
modelMaxThinkingTokens: 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,
|
||||
modelMaxThinkingTokens: 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 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,
|
||||
modelMaxThinkingTokens: 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
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,21 +30,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
|
|||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
|
||||
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
|
||||
let { id: modelId, info: modelInfo } = this.getModel()
|
||||
const maxTokens = 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"
|
||||
const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 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":
|
||||
|
|
@ -194,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.modelMaxThinkingTokens ?? 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<string> {
|
||||
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 : ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.modelMaxThinkingTokens ?? 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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,97 @@
|
|||
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"
|
||||
|
||||
// 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<Anthropic.Messages.MessageParam, "content"> {
|
||||
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
|
||||
}
|
||||
| {
|
||||
type: "thinking"
|
||||
thinking: string
|
||||
}
|
||||
index?: number
|
||||
delta?:
|
||||
| {
|
||||
type: "text_delta"
|
||||
text: string
|
||||
}
|
||||
| {
|
||||
type: "thinking_delta"
|
||||
thinking: string
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class VertexHandler implements ApiHandler, SingleCompletionHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
|
@ -18,37 +106,122 @@ 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,
|
||||
temperature: this.options.modelTemperature ?? 0,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
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
|
||||
// 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: id,
|
||||
max_tokens: maxTokens,
|
||||
temperature,
|
||||
thinking,
|
||||
// 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<VertexMessageStreamEvent>
|
||||
|
||||
// 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,43 +229,124 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
|
|||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
text: chunk.content_block!.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
case "thinking": {
|
||||
if (chunk.index! > 0) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: (chunk.content_block as any).thinking,
|
||||
}
|
||||
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
|
||||
}
|
||||
case "thinking_delta": {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: (chunk.delta as any).thinking,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.modelMaxThinkingTokens ?? 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<string> {
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
temperature: this.options.modelTemperature ?? 0,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
let { id, info, temperature, maxTokens, thinking } = this.getModel()
|
||||
const useCache = info.supportsPromptCache
|
||||
|
||||
const params = {
|
||||
model: id,
|
||||
max_tokens: maxTokens,
|
||||
temperature,
|
||||
thinking,
|
||||
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") {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export class Cline {
|
|||
// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
|
||||
private isPaused: boolean = false
|
||||
private pausedModeSlug: string = defaultModeSlug
|
||||
readonly apiConfiguration: ApiConfiguration
|
||||
api: ApiHandler
|
||||
private terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
|
|
@ -120,7 +121,7 @@ export class Cline {
|
|||
isInitialized = false
|
||||
|
||||
// checkpoints
|
||||
checkpointsEnabled: boolean = false
|
||||
enableCheckpoints: boolean = false
|
||||
private checkpointService?: CheckpointService
|
||||
|
||||
// streaming
|
||||
|
|
@ -155,6 +156,7 @@ export class Cline {
|
|||
|
||||
this.taskId = crypto.randomUUID()
|
||||
this.taskNumber = -1
|
||||
this.apiConfiguration = apiConfiguration
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.urlContentFetcher = new UrlContentFetcher(provider.context)
|
||||
|
|
@ -164,7 +166,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
|
||||
|
|
@ -1018,13 +1020,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)
|
||||
|
|
@ -3436,7 +3446,7 @@ export class Cline {
|
|||
// Checkpoints
|
||||
|
||||
private async getCheckpointService() {
|
||||
if (!this.checkpointsEnabled) {
|
||||
if (!this.enableCheckpoints) {
|
||||
throw new Error("Checkpoints are disabled")
|
||||
}
|
||||
|
||||
|
|
@ -3477,7 +3487,7 @@ export class Cline {
|
|||
commitHash: string
|
||||
mode: "full" | "checkpoint"
|
||||
}) {
|
||||
if (!this.checkpointsEnabled) {
|
||||
if (!this.enableCheckpoints) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -3516,12 +3526,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
|
||||
}
|
||||
|
||||
|
|
@ -3542,7 +3552,7 @@ export class Cline {
|
|||
}
|
||||
} catch (err) {
|
||||
this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
|
||||
this.checkpointsEnabled = false
|
||||
this.enableCheckpoints = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3555,7 +3565,7 @@ export class Cline {
|
|||
commitHash: string
|
||||
mode: "preview" | "restore"
|
||||
}) {
|
||||
if (!this.checkpointsEnabled) {
|
||||
if (!this.enableCheckpoints) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -3610,7 +3620,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
172
src/core/prompts/__tests__/custom-system-prompt.test.ts
Normal file
172
src/core/prompts/__tests__/custom-system-prompt.test.ts
Normal file
|
|
@ -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<typeof fs>
|
||||
|
||||
// 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")
|
||||
})
|
||||
})
|
||||
60
src/core/prompts/sections/custom-system-prompt.ts
Normal file
60
src/core/prompts/sections/custom-system-prompt.ts
Normal file
|
|
@ -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<string> {
|
||||
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<string> {
|
||||
const filePath = getSystemPromptFilePath(cwd, mode)
|
||||
return safeReadFile(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the .roo directory exists, creating it if necessary
|
||||
*/
|
||||
export async function ensureRooDirectory(cwd: string): Promise<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
private clineStack: 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
|
||||
private lastTaskNumber = -1
|
||||
|
|
@ -402,7 +402,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customModePrompts,
|
||||
diffEnabled,
|
||||
checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
|
|
@ -417,7 +417,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customInstructions: effectiveInstructions,
|
||||
enableDiff: diffEnabled,
|
||||
enableCheckpoints: checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
images,
|
||||
|
|
@ -433,7 +433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customModePrompts,
|
||||
diffEnabled,
|
||||
checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
|
|
@ -448,7 +448,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customInstructions: effectiveInstructions,
|
||||
enableDiff: diffEnabled,
|
||||
enableCheckpoints: checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
fuzzyMatchThreshold,
|
||||
historyItem,
|
||||
experiments,
|
||||
|
|
@ -1110,9 +1110,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":
|
||||
|
|
@ -1751,7 +1751,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
anthropicThinking,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
|
|
@ -1771,6 +1770,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
modelTemperature,
|
||||
modelMaxTokens,
|
||||
modelMaxThinkingTokens,
|
||||
} = apiConfiguration
|
||||
await Promise.all([
|
||||
this.updateGlobalState("apiProvider", apiProvider),
|
||||
|
|
@ -1799,7 +1800,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
|
||||
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
|
||||
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
|
||||
this.updateGlobalState("anthropicThinking", anthropicThinking),
|
||||
this.storeSecret("geminiApiKey", geminiApiKey),
|
||||
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
|
||||
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
|
||||
|
|
@ -1819,6 +1819,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.updateGlobalState("requestyModelId", requestyModelId),
|
||||
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
|
||||
this.updateGlobalState("modelTemperature", modelTemperature),
|
||||
this.updateGlobalState("modelMaxTokens", modelMaxTokens),
|
||||
this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
|
||||
])
|
||||
if (this.getCurrentCline()) {
|
||||
this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
|
||||
|
|
@ -2023,12 +2025,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.deleteTaskFromState(id)
|
||||
|
||||
// check if checkpoints are enabled
|
||||
const { checkpointsEnabled } = await this.getState()
|
||||
const { enableCheckpoints } = await this.getState()
|
||||
// get the base directory of the project
|
||||
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
// delete checkpoints branch from project git repo
|
||||
if (checkpointsEnabled && baseDir) {
|
||||
if (enableCheckpoints && baseDir) {
|
||||
|
||||
const branchSummary = await simpleGit(baseDir)
|
||||
.branch(["-D", `roo-code-checkpoints-${id}`])
|
||||
.catch(() => undefined)
|
||||
|
|
@ -2077,7 +2080,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
alwaysAllowModeSwitch,
|
||||
soundEnabled,
|
||||
diffEnabled,
|
||||
checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
taskHistory,
|
||||
soundVolume,
|
||||
browserViewportSize,
|
||||
|
|
@ -2126,7 +2129,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,
|
||||
|
|
@ -2230,7 +2233,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
anthropicThinking,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
|
|
@ -2254,7 +2256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
allowedCommands,
|
||||
soundEnabled,
|
||||
diffEnabled,
|
||||
checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
soundVolume,
|
||||
browserViewportSize,
|
||||
fuzzyMatchThreshold,
|
||||
|
|
@ -2285,6 +2287,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
modelTemperature,
|
||||
modelMaxTokens,
|
||||
modelMaxThinkingTokens,
|
||||
maxOpenTabsContext,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
|
|
@ -2313,7 +2317,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
|
||||
this.getSecret("geminiApiKey") as Promise<string | undefined>,
|
||||
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
|
||||
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
|
||||
|
|
@ -2337,7 +2340,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
|
||||
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("checkpointsEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("enableCheckpoints") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("soundVolume") as Promise<number | undefined>,
|
||||
this.getGlobalState("browserViewportSize") as Promise<string | undefined>,
|
||||
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
|
||||
|
|
@ -2368,6 +2371,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getGlobalState("modelTemperature") as Promise<number | undefined>,
|
||||
this.getGlobalState("modelMaxTokens") as Promise<number | undefined>,
|
||||
this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
|
||||
this.getGlobalState("maxOpenTabsContext") as Promise<number | undefined>,
|
||||
])
|
||||
|
||||
|
|
@ -2413,7 +2418,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
anthropicThinking,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
|
|
@ -2433,6 +2437,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
modelTemperature,
|
||||
modelMaxTokens,
|
||||
modelMaxThinkingTokens,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
|
|
@ -2446,7 +2452,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,
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ describe("ClineProvider", () => {
|
|||
uriScheme: "vscode",
|
||||
soundEnabled: false,
|
||||
diffEnabled: false,
|
||||
checkpointsEnabled: false,
|
||||
enableCheckpoints: false,
|
||||
writeDelayMs: 1000,
|
||||
browserViewportSize: "900x600",
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
|
|
@ -712,7 +712,7 @@ describe("ClineProvider", () => {
|
|||
},
|
||||
mode: "code",
|
||||
diffEnabled: true,
|
||||
checkpointsEnabled: false,
|
||||
enableCheckpoints: false,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
|
|
|||
51
src/services/ripgrep/__tests__/index.test.ts
Normal file
51
src/services/ripgrep/__tests__/index.test.ts
Normal file
|
|
@ -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...]")
|
||||
})
|
||||
})
|
||||
|
|
@ -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<string | undefined> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export interface ExtensionState {
|
|||
soundEnabled?: boolean
|
||||
soundVolume?: number
|
||||
diffEnabled?: boolean
|
||||
checkpointsEnabled: boolean
|
||||
enableCheckpoints: boolean
|
||||
browserViewportSize?: string
|
||||
screenshotQuality?: number
|
||||
fuzzyMatchThreshold?: number
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export interface WebviewMessage {
|
|||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "checkpointsEnabled"
|
||||
| "enableCheckpoints"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "openMcpSettings"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe("checkExistKey", () => {
|
|||
apiKey: "test-key",
|
||||
apiProvider: undefined,
|
||||
anthropicBaseUrl: undefined,
|
||||
anthropicThinking: undefined,
|
||||
modelMaxThinkingTokens: undefined,
|
||||
}
|
||||
expect(checkExistKey(config)).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export interface ApiHandlerOptions {
|
|||
apiModelId?: string
|
||||
apiKey?: string // anthropic
|
||||
anthropicBaseUrl?: string
|
||||
anthropicThinking?: number
|
||||
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
|
||||
glamaModelId?: string
|
||||
glamaModelInfo?: ModelInfo
|
||||
|
|
@ -68,6 +67,8 @@ export interface ApiHandlerOptions {
|
|||
requestyModelId?: string
|
||||
requestyModelInfo?: ModelInfo
|
||||
modelTemperature?: number
|
||||
modelMaxTokens?: number
|
||||
modelMaxThinkingTokens?: 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,
|
||||
|
|
@ -441,55 +436,80 @@ 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,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
thinking: false,
|
||||
},
|
||||
"claude-3-5-sonnet-v2@20241022": {
|
||||
maxTokens: 8192,
|
||||
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<string, ModelInfo>
|
||||
|
||||
|
|
@ -671,8 +691,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,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export type GlobalStateKey =
|
|||
| "lmStudioModelId"
|
||||
| "lmStudioBaseUrl"
|
||||
| "anthropicBaseUrl"
|
||||
| "anthropicThinking"
|
||||
| "azureApiVersion"
|
||||
| "openAiStreamingEnabled"
|
||||
| "openRouterModelId"
|
||||
|
|
@ -53,7 +52,7 @@ export type GlobalStateKey =
|
|||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "checkpointsEnabled"
|
||||
| "enableCheckpoints"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "fuzzyMatchThreshold"
|
||||
|
|
@ -81,5 +80,7 @@ export type GlobalStateKey =
|
|||
| "requestyModelInfo"
|
||||
| "unboundModelInfo"
|
||||
| "modelTemperature"
|
||||
| "modelMaxTokens"
|
||||
| "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
|
||||
| "mistralCodestralUrl"
|
||||
| "maxOpenTabsContext"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -25,39 +22,42 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={hideAnnouncement}
|
||||
title="Hide announcement"
|
||||
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h2 style={{ margin: "0 0 8px" }}>🎉{" "}Introducing Roo Code 3.2</h2>
|
||||
<h2 style={{ margin: "0 0 8px" }}>🎉{" "}Automatic Checkpoints Now Enabled</h2>
|
||||
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3 style={{ margin: "12px 0 8px" }}>Custom Modes: Celebrating Our New Identity</h3>
|
||||
<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
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:
|
||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||
<li>QA Engineers who write thorough test cases and catch edge cases</li>
|
||||
<li>Product Managers who excel at user stories and feature prioritization</li>
|
||||
<li>UI/UX Designers who craft beautiful, accessible interfaces</li>
|
||||
<li>Code Reviewers who ensure quality and maintainability</li>
|
||||
<li>Peace of mind when making significant changes</li>
|
||||
<li>Ability to visually inspect changes between steps</li>
|
||||
<li>Easy rollback if you're not satisfied with certain code modifications</li>
|
||||
<li>Improved navigation through complex task execution</li>
|
||||
</ul>
|
||||
Just click the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> icon to
|
||||
get started with Custom Modes!
|
||||
</p>
|
||||
|
||||
<h3 style={{ margin: "12px 0 8px" }}>Join Us for the Next Chapter</h3>
|
||||
<h3 style={{ margin: "12px 0 8px" }}>Customize Your Experience</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
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{" "}
|
||||
<VSCodeLink href="https://www.reddit.com/r/RooCode" style={{ display: "inline" }}>
|
||||
reddit.com/r/RooCode
|
||||
</VSCodeLink>
|
||||
.
|
||||
While we recommend keeping this feature enabled, you can disable it if needed.{" "}
|
||||
<VSCodeLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.postMessage({ type: "action", action: "settingsButtonClicked" }, "*")
|
||||
}}
|
||||
style={{ display: "inline", padding: "0 2px" }}>
|
||||
Open Settings
|
||||
</VSCodeLink>{" "}
|
||||
and look for the "Enable automatic checkpoints" option in the Advanced Settings section.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -798,6 +798,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
<select
|
||||
value={mode}
|
||||
disabled={textAreaDisabled}
|
||||
title="Select mode for interaction"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "prompts-action") {
|
||||
|
|
@ -849,6 +850,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
<select
|
||||
value={currentApiConfigName || ""}
|
||||
disabled={textAreaDisabled}
|
||||
title="Select API configuration"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "settings-action") {
|
||||
|
|
@ -915,6 +917,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
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<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
className={`input-icon-button ${
|
||||
shouldDisableImages ? "disabled" : ""
|
||||
} codicon codicon-device-camera`}
|
||||
title="Add images to message"
|
||||
onClick={() => !shouldDisableImages && onSelectImages()}
|
||||
style={{ fontSize: 16.5 }}
|
||||
/>
|
||||
<span
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
|
||||
title="Send message"
|
||||
onClick={() => !textAreaDisabled && onSend()}
|
||||
style={{ fontSize: 15 }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
onClick={() => {
|
||||
scrollToBottomSmooth()
|
||||
disableAutoScrollRef.current = false
|
||||
}}>
|
||||
}}
|
||||
title="Scroll to bottom of chat">
|
||||
<span className="codicon codicon-chevron-down" style={{ fontSize: "18px" }}></span>
|
||||
</ScrollToBottomButton>
|
||||
</div>
|
||||
|
|
@ -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}
|
||||
</VSCodeButton>
|
||||
|
|
@ -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}
|
||||
</VSCodeButton>
|
||||
|
|
|
|||
|
|
@ -183,7 +183,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
${totalCost?.toFixed(4)}
|
||||
</div>
|
||||
)}
|
||||
<VSCodeButton appearance="icon" onClick={onClose} style={{ marginLeft: 6, flexShrink: 0 }}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onClose}
|
||||
style={{ marginLeft: 6, flexShrink: 0 }}
|
||||
title="Close task and start a new one">
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
|
@ -351,13 +355,18 @@ export const highlightMentions = (text?: string, withShadow = true) => {
|
|||
|
||||
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
|
||||
<div className="flex flex-row gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Export task history"
|
||||
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
|
||||
<span className="codicon codicon-cloud-download" />
|
||||
</Button>
|
||||
{!!item?.size && item.size > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Delete task from history"
|
||||
onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
|
||||
<span className="codicon codicon-trash" />
|
||||
{prettyBytes(item.size)}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
const [showConfigMenu, setShowConfigMenu] = useState(false)
|
||||
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
||||
const [activeSupportTab, setActiveSupportTab] = useState<SupportPromptType>("ENHANCE")
|
||||
const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false)
|
||||
|
||||
// Direct update functions
|
||||
const updateAgentPrompt = useCallback(
|
||||
|
|
@ -971,6 +972,45 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
<span className="codicon codicon-copy"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{/* Custom System Prompt Disclosure */}
|
||||
<div className="mb-3 mt-12">
|
||||
<button
|
||||
onClick={() => setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
|
||||
className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
|
||||
aria-expanded={isSystemPromptDisclosureOpen}>
|
||||
<span
|
||||
className={`codicon codicon-${isSystemPromptDisclosureOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
|
||||
<span>Advanced: Override System Prompt</span>
|
||||
</button>
|
||||
|
||||
{isSystemPromptDisclosureOpen && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-2 ml-5">
|
||||
You can completely replace the system prompt for this mode (aside from the role
|
||||
definition and custom instructions) by creating a file at{" "}
|
||||
<span
|
||||
className="text-vscode-textLink-foreground cursor-pointer underline"
|
||||
onClick={() => {
|
||||
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"}
|
||||
</span>{" "}
|
||||
in your workspace. This is a very advanced feature that bypasses built-in safeguards and
|
||||
consistency checks (especially around tool usage), so be careful!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import * as vscodemodels from "vscode"
|
|||
import {
|
||||
ApiConfiguration,
|
||||
ModelInfo,
|
||||
ApiProvider,
|
||||
anthropicDefaultModelId,
|
||||
anthropicModels,
|
||||
azureOpenAiDefaultApiVersion,
|
||||
|
|
@ -1380,9 +1381,11 @@ const ApiOptions = ({
|
|||
/>
|
||||
</div>
|
||||
<ThinkingBudget
|
||||
key={`${selectedProvider}-${selectedModelId}`}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
modelInfo={selectedModelInfo}
|
||||
provider={selectedProvider as ApiProvider}
|
||||
/>
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
alwaysAllowWrite,
|
||||
alwaysApproveResubmit,
|
||||
browserViewportSize,
|
||||
checkpointsEnabled,
|
||||
enableCheckpoints,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
fuzzyMatchThreshold,
|
||||
|
|
@ -143,7 +143,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={enableCheckpoints}
|
||||
onChange={(e: any) => {
|
||||
setCachedStateField("enableCheckpoints", e.target.checked)
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Enable automatic checkpoints</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
When enabled, Roo will automatically create checkpoints during task execution, making it
|
||||
easy to review changes or revert to earlier states.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={diffEnabled}
|
||||
|
|
@ -779,28 +798,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
|
||||
<VSCodeCheckbox
|
||||
checked={checkpointsEnabled}
|
||||
onChange={(e: any) => {
|
||||
setCachedStateField("checkpointsEnabled", e.target.checked)
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Enable experimental checkpoints</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(experimentConfigsMap)
|
||||
.filter((config) => config[0] !== "DIFF_STRATEGY")
|
||||
.map((config) => (
|
||||
|
|
|
|||
|
|
@ -1,29 +1,73 @@
|
|||
import { useEffect, useMemo } from "react"
|
||||
import { ApiProvider } from "../../../../src/shared/api"
|
||||
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
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
|
||||
modelInfo?: ModelInfo
|
||||
provider?: ApiProvider
|
||||
}
|
||||
|
||||
export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
|
||||
const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
|
||||
export const ThinkingBudget = ({
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
modelInfo,
|
||||
provider,
|
||||
}: ThinkingBudgetProps) => {
|
||||
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
|
||||
const tokensMin = 8192
|
||||
const tokensMax = modelInfo?.maxTokens || 64_000
|
||||
|
||||
return modelInfo && modelInfo.thinking ? (
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="font-medium">Thinking Budget</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
min={THINKING_BUDGET.min}
|
||||
max={(modelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
|
||||
step={THINKING_BUDGET.step}
|
||||
value={[budget]}
|
||||
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
|
||||
/>
|
||||
<div className="w-12 text-sm text-center">{budget}</div>
|
||||
// Get the appropriate thinking tokens based on provider
|
||||
const thinkingTokens = useMemo(() => {
|
||||
const value = apiConfiguration?.modelMaxThinkingTokens
|
||||
return value || Math.min(Math.floor(0.8 * tokens), 8192)
|
||||
}, [apiConfiguration, tokens])
|
||||
|
||||
const thinkingTokensMin = 1024
|
||||
const thinkingTokensMax = Math.floor(0.8 * tokens)
|
||||
|
||||
useEffect(() => {
|
||||
if (thinkingTokens > thinkingTokensMax) {
|
||||
setApiConfigurationField("modelMaxThinkingTokens", thinkingTokensMax)
|
||||
}
|
||||
}, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
|
||||
|
||||
if (!modelInfo?.thinking) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="font-medium">Max Tokens</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
min={tokensMin}
|
||||
max={tokensMax}
|
||||
step={1024}
|
||||
value={[tokens]}
|
||||
onValueChange={([value]) => setApiConfigurationField("modelMaxTokens", value)}
|
||||
/>
|
||||
<div className="w-12 text-sm text-center">{tokens}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="font-medium">Max Thinking Tokens</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
min={thinkingTokensMin}
|
||||
max={thinkingTokensMax}
|
||||
step={1024}
|
||||
value={[thinkingTokens]}
|
||||
onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
|
||||
/>
|
||||
<div className="w-12 text-sm text-center">{thinkingTokens}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ jest.mock("../TemperatureControl", () => ({
|
|||
),
|
||||
}))
|
||||
|
||||
// Mock ThinkingBudget component
|
||||
jest.mock("../ThinkingBudget", () => ({
|
||||
ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
|
||||
modelInfo?.thinking ? (
|
||||
<div data-testid="thinking-budget" data-provider={provider}>
|
||||
<input data-testid="thinking-tokens" value={apiConfiguration?.modelMaxThinkingTokens} />
|
||||
</div>
|
||||
) : null,
|
||||
}))
|
||||
|
||||
describe("ApiOptions", () => {
|
||||
const renderApiOptions = (props = {}) => {
|
||||
render(
|
||||
|
|
@ -72,5 +82,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.
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<input
|
||||
type="range"
|
||||
data-testid="slider"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[0]}
|
||||
onChange={(e) => 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(
|
||||
<ThinkingBudget
|
||||
{...defaultProps}
|
||||
modelInfo={{
|
||||
...mockModelInfo,
|
||||
thinking: false,
|
||||
maxTokens: 16384,
|
||||
contextWindow: 200000,
|
||||
supportsPromptCache: true,
|
||||
supportsImages: true,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("should render sliders when model supports thinking", () => {
|
||||
render(<ThinkingBudget {...defaultProps} />)
|
||||
|
||||
expect(screen.getAllByTestId("slider")).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
|
||||
const setApiConfigurationField = jest.fn()
|
||||
|
||||
render(
|
||||
<ThinkingBudget
|
||||
{...defaultProps}
|
||||
apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
provider="anthropic"
|
||||
/>,
|
||||
)
|
||||
|
||||
const sliders = screen.getAllByTestId("slider")
|
||||
fireEvent.change(sliders[1], { target: { value: "5000" } })
|
||||
|
||||
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
|
||||
})
|
||||
|
||||
it("should use modelMaxThinkingTokens field for Vertex provider", () => {
|
||||
const setApiConfigurationField = jest.fn()
|
||||
|
||||
render(
|
||||
<ThinkingBudget
|
||||
{...defaultProps}
|
||||
apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
provider="vertex"
|
||||
/>,
|
||||
)
|
||||
|
||||
const sliders = screen.getAllByTestId("slider")
|
||||
fireEvent.change(sliders[1], { target: { value: "5000" } })
|
||||
|
||||
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
|
||||
})
|
||||
|
||||
it("should cap thinking tokens at 80% of max tokens", () => {
|
||||
const setApiConfigurationField = jest.fn()
|
||||
|
||||
render(
|
||||
<ThinkingBudget
|
||||
{...defaultProps}
|
||||
apiConfiguration={{ modelMaxTokens: 10000, modelMaxThinkingTokens: 9000 }}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Effect should trigger and cap the value
|
||||
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 8000) // 80% of 10000
|
||||
})
|
||||
|
||||
it("should use default thinking tokens if not provided", () => {
|
||||
render(<ThinkingBudget {...defaultProps} apiConfiguration={{ modelMaxTokens: 10000 }} />)
|
||||
|
||||
// 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(<ThinkingBudget {...defaultProps} apiConfiguration={{ modelMaxTokens: 1000 }} />)
|
||||
|
||||
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(
|
||||
<ThinkingBudget
|
||||
{...defaultProps}
|
||||
apiConfiguration={{ modelMaxTokens: 10000 }}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
const sliders = screen.getAllByTestId("slider")
|
||||
fireEvent.change(sliders[0], { target: { value: "12000" } })
|
||||
|
||||
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 })),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue