From b2b135c05c47b46cb223c1e448741a67a262a04e Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 15:45:26 -0800 Subject: [PATCH 01/11] [WIP] Claude 3.7 Sonnet (Thinking) --- package-lock.json | 9 ++--- package.json | 2 +- src/api/providers/anthropic.ts | 65 +++++++++++++++++++++------------- src/shared/api.ts | 1 + 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8c65fd8e8..52b1e0bcb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.7.1", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", - "@anthropic-ai/sdk": "^0.26.0", + "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@google/generative-ai": "^0.18.0", @@ -122,9 +122,10 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.26.1.tgz", - "integrity": "sha512-HeMJP1bDFfQPQS3XTJAmfXkFBdZ88wvfkE05+vsoA9zGn5dHqEaHOPsqkazf/i0gXYg2XlLxxZrf6rUAarSqzw==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz", + "integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==", + "license": "MIT", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", diff --git a/package.json b/package.json index 76d1273e5c..ced42da9f0 100644 --- a/package.json +++ b/package.json @@ -304,7 +304,7 @@ }, "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", - "@anthropic-ai/sdk": "^0.26.0", + "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@google/generative-ai": "^0.18.0", diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 4c62238f46..4bde475e80 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -1,5 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" +import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources" +import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta" import { anthropicDefaultModelId, AnthropicModelId, @@ -12,12 +14,15 @@ import { ApiStream } from "../transform/stream" const ANTHROPIC_DEFAULT_TEMPERATURE = 0 +const THINKING_MODELS = ["claude-3-7-sonnet-20250219"] + export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic constructor(options: ApiHandlerOptions) { this.options = options + this.client = new Anthropic({ apiKey: this.options.apiKey, baseURL: this.options.anthropicBaseUrl || undefined, @@ -25,26 +30,36 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { - let stream: AnthropicStream + let stream: AnthropicStream + const cacheControl: CacheControlEphemeral = { type: "ephemeral" } const modelId = this.getModel().id + let thinking: BetaThinkingConfigParam | undefined = undefined + + if (THINKING_MODELS.includes(modelId)) { + thinking = this.options.anthropicThinking + ? { type: "enabled", budget_tokens: this.options.anthropicThinking } + : { type: "disabled" } + } switch (modelId) { - // 'latest' alias does not support cache_control case "claude-3-7-sonnet-20250219": case "claude-3-5-sonnet-20241022": case "claude-3-5-haiku-20241022": case "claude-3-opus-20240229": case "claude-3-haiku-20240307": { - /* - The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request.. - */ + /** + * The latest message will be the new user message, one before will + * be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request.. + */ const userMsgIndices = messages.reduce( (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[], ) + const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 - stream = await this.client.beta.promptCaching.messages.create( + + stream = await this.client.messages.create( { model: modelId, max_tokens: this.getModel().info.maxTokens || 8192, @@ -60,12 +75,12 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { { type: "text", text: message.content, - cache_control: { type: "ephemeral" }, + cache_control: cacheControl, }, ] : message.content.map((content, contentIndex) => contentIndex === message.content.length - 1 - ? { ...content, cache_control: { type: "ephemeral" } } + ? { ...content, cache_control: cacheControl } : content, ), } @@ -76,6 +91,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { // tool_choice: { type: "auto" }, // tools: tools, stream: true, + thinking, }, (() => { // prompt caching: https://x.com/alexalbert__/status/1823751995901272068 @@ -114,8 +130,9 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { for await (const chunk of stream) { switch (chunk.type) { case "message_start": - // tells us cache reads/writes/input/output + // Tells us cache reads/writes/input/output. const usage = chunk.message.usage + yield { type: "usage", inputTokens: usage.input_tokens || 0, @@ -123,43 +140,41 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { cacheWriteTokens: usage.cache_creation_input_tokens || undefined, cacheReadTokens: usage.cache_read_input_tokens || undefined, } + break case "message_delta": - // tells us stop_reason, stop_sequence, and output tokens along the way and at the end of the message - + // Tells us stop_reason, stop_sequence, and output tokens + // along the way and at the end of the message. yield { type: "usage", inputTokens: 0, outputTokens: chunk.usage.output_tokens || 0, } + break case "message_stop": - // no usage data, just an indicator that the message is done + // No usage data, just an indicator that the message is done. break case "content_block_start": switch (chunk.content_block.type) { + case "thinking": + yield { type: "reasoning", text: chunk.content_block.thinking } + break case "text": - // we may receive multiple text blocks, in which case just insert a line break between them + // We may receive multiple text blocks, in which + // case just insert a line break between them. if (chunk.index > 0) { - yield { - type: "text", - text: "\n", - } - } - yield { - type: "text", - text: chunk.content_block.text, + yield { type: "text", text: "\n" } } + + yield { type: "text", text: chunk.content_block.text } break } break case "content_block_delta": switch (chunk.delta.type) { case "text_delta": - yield { - type: "text", - text: chunk.delta.text, - } + yield { type: "text", text: chunk.delta.text } break } break diff --git a/src/shared/api.ts b/src/shared/api.ts index 9c91f8b76d..cea760c776 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -21,6 +21,7 @@ export interface ApiHandlerOptions { apiModelId?: string apiKey?: string // anthropic anthropicBaseUrl?: string + anthropicThinking?: number vsCodeLmModelSelector?: vscode.LanguageModelChatSelector glamaModelId?: string glamaModelInfo?: ModelInfo From 22391e5011c3dfbcfaa4dc1d5620e63fa9b2bdd3 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 15:55:04 -0800 Subject: [PATCH 02/11] Fix type errors --- src/api/transform/bedrock-converse-format.ts | 4 +++- src/api/transform/gemini-format.ts | 15 ++++----------- src/api/transform/openai-format.ts | 3 +++ src/api/transform/simple-format.ts | 11 +---------- src/api/transform/vscode-lm-format.ts | 3 +++ src/core/Cline.ts | 4 +--- src/integrations/misc/export-markdown.ts | 9 +-------- 7 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index 07529db1bc..e4dc9eecc8 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -193,6 +193,8 @@ export function convertToAnthropicMessage( usage: { input_tokens: streamEvent.metadata.usage.inputTokens || 0, output_tokens: streamEvent.metadata.usage.outputTokens || 0, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }, } } @@ -203,7 +205,7 @@ export function convertToAnthropicMessage( return { type: "message", role: "assistant", - content: [{ type: "text", text: text }], + content: [{ type: "text", text: text, citations: null }], model: modelId, } } diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 935e47147a..3a149dc149 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -11,16 +11,7 @@ import { TextPart, } from "@google/generative-ai" -export function convertAnthropicContentToGemini( - content: - | string - | Array< - | Anthropic.Messages.TextBlockParam - | Anthropic.Messages.ImageBlockParam - | Anthropic.Messages.ToolUseBlockParam - | Anthropic.Messages.ToolResultBlockParam - >, -): Part[] { +export function convertAnthropicContentToGemini(content: Anthropic.Messages.MessageParam["content"]): Part[] { if (typeof content === "string") { return [{ text: content } as TextPart] } @@ -140,7 +131,7 @@ export function convertGeminiResponseToAnthropic( // Add the main text response const text = response.text() if (text) { - content.push({ type: "text", text }) + content.push({ type: "text", text, citations: null }) } // Add function calls as tool_use blocks @@ -190,6 +181,8 @@ export function convertGeminiResponseToAnthropic( usage: { input_tokens: response.usageMetadata?.promptTokenCount ?? 0, output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }, } } diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index fe23b9b2ff..f421769054 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -158,6 +158,7 @@ export function convertToAnthropicMessage( { type: "text", text: openAiMessage.content || "", + citations: null, }, ], model: completion.model, @@ -178,6 +179,8 @@ export function convertToAnthropicMessage( usage: { input_tokens: completion.usage?.prompt_tokens || 0, output_tokens: completion.usage?.completion_tokens || 0, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }, } diff --git a/src/api/transform/simple-format.ts b/src/api/transform/simple-format.ts index c1e4895bba..39049f76c2 100644 --- a/src/api/transform/simple-format.ts +++ b/src/api/transform/simple-format.ts @@ -3,16 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk" /** * Convert complex content blocks to simple string content */ -export function convertToSimpleContent( - content: - | string - | Array< - | Anthropic.Messages.TextBlockParam - | Anthropic.Messages.ImageBlockParam - | Anthropic.Messages.ToolUseBlockParam - | Anthropic.Messages.ToolResultBlockParam - >, -): string { +export function convertToSimpleContent(content: Anthropic.Messages.MessageParam["content"]): string { if (typeof content === "string") { return content } diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index 6d7bea92ba..f258dd1732 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -175,6 +175,7 @@ export async function convertToAnthropicMessage( return { type: "text", text: part.value, + citations: null, } } @@ -195,6 +196,8 @@ export async function convertToAnthropicMessage( usage: { input_tokens: 0, output_tokens: 0, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }, } } diff --git a/src/core/Cline.ts b/src/core/Cline.ts index f1f5e41b33..549cec5eea 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -69,9 +69,7 @@ const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution type ToolResponse = string | Array -type UserContent = Array< - Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam -> +type UserContent = Array export type ClineOptions = { provider: ClineProvider diff --git a/src/integrations/misc/export-markdown.ts b/src/integrations/misc/export-markdown.ts index 2aa9d7b6ed..05b31671d8 100644 --- a/src/integrations/misc/export-markdown.ts +++ b/src/integrations/misc/export-markdown.ts @@ -41,14 +41,7 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi } } -export function formatContentBlockToMarkdown( - block: - | Anthropic.TextBlockParam - | Anthropic.ImageBlockParam - | Anthropic.ToolUseBlockParam - | Anthropic.ToolResultBlockParam, - // messages: Anthropic.MessageParam[] -): string { +export function formatContentBlockToMarkdown(block: Anthropic.Messages.ContentBlockParam): string { switch (block.type) { case "text": return block.text From 543b68f785f98f62976655d5f4510bccfd0ffb42 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 16:09:31 -0800 Subject: [PATCH 03/11] Fix tests --- src/api/providers/__tests__/anthropic.test.ts | 76 +++++-------------- .../__tests__/bedrock-converse-format.test.ts | 8 +- .../transform/__tests__/openai-format.test.ts | 6 ++ .../__tests__/vscode-lm-format.test.ts | 3 + 4 files changed, 33 insertions(+), 60 deletions(-) diff --git a/src/api/providers/__tests__/anthropic.test.ts b/src/api/providers/__tests__/anthropic.test.ts index df0050ab9c..ff7bdb4054 100644 --- a/src/api/providers/__tests__/anthropic.test.ts +++ b/src/api/providers/__tests__/anthropic.test.ts @@ -1,50 +1,13 @@ +// npx jest src/api/providers/__tests__/anthropic.test.ts + import { AnthropicHandler } from "../anthropic" import { ApiHandlerOptions } from "../../../shared/api" -import { ApiStream } from "../../transform/stream" -import { Anthropic } from "@anthropic-ai/sdk" -// Mock Anthropic client -const mockBetaCreate = jest.fn() const mockCreate = jest.fn() + jest.mock("@anthropic-ai/sdk", () => { return { Anthropic: jest.fn().mockImplementation(() => ({ - beta: { - promptCaching: { - messages: { - create: mockBetaCreate.mockImplementation(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { - usage: { - input_tokens: 100, - output_tokens: 50, - cache_creation_input_tokens: 20, - cache_read_input_tokens: 10, - }, - }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "Hello", - }, - } - yield { - type: "content_block_delta", - delta: { - type: "text_delta", - text: " world", - }, - } - }, - })), - }, - }, - }, messages: { create: mockCreate.mockImplementation(async (options) => { if (!options.stream) { @@ -65,16 +28,26 @@ jest.mock("@anthropic-ai/sdk", () => { type: "message_start", message: { usage: { - input_tokens: 10, - output_tokens: 5, + input_tokens: 100, + output_tokens: 50, + cache_creation_input_tokens: 20, + cache_read_input_tokens: 10, }, }, } yield { type: "content_block_start", + index: 0, content_block: { type: "text", - text: "Test response", + text: "Hello", + }, + } + yield { + type: "content_block_delta", + delta: { + type: "text_delta", + text: " world", }, } }, @@ -95,7 +68,6 @@ describe("AnthropicHandler", () => { apiModelId: "claude-3-5-sonnet-20241022", } handler = new AnthropicHandler(mockOptions) - mockBetaCreate.mockClear() mockCreate.mockClear() }) @@ -126,17 +98,6 @@ describe("AnthropicHandler", () => { describe("createMessage", () => { const systemPrompt = "You are a helpful assistant." - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "Hello!", - }, - ], - }, - ] it("should handle prompt caching for supported models", async () => { const stream = handler.createMessage(systemPrompt, [ @@ -173,9 +134,8 @@ describe("AnthropicHandler", () => { expect(textChunks[0].text).toBe("Hello") expect(textChunks[1].text).toBe(" world") - // Verify beta API was used - expect(mockBetaCreate).toHaveBeenCalled() - expect(mockCreate).not.toHaveBeenCalled() + // Verify API + expect(mockCreate).toHaveBeenCalled() }) }) diff --git a/src/api/transform/__tests__/bedrock-converse-format.test.ts b/src/api/transform/__tests__/bedrock-converse-format.test.ts index c46eb94a2e..fdd29c75bf 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.test.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.test.ts @@ -1,3 +1,5 @@ +// npx jest src/api/transform/__tests__/bedrock-converse-format.test.ts + import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../bedrock-converse-format" import { Anthropic } from "@anthropic-ai/sdk" import { ContentBlock, ToolResultContentBlock } from "@aws-sdk/client-bedrock-runtime" @@ -187,6 +189,8 @@ describe("bedrock-converse-format", () => { usage: { input_tokens: 10, output_tokens: 20, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }, }) }) @@ -205,7 +209,7 @@ describe("bedrock-converse-format", () => { expect(result).toEqual({ type: "message", role: "assistant", - content: [{ type: "text", text: "Hello" }], + content: [{ type: "text", text: "Hello", citations: null }], model: "test-model", }) }) @@ -224,7 +228,7 @@ describe("bedrock-converse-format", () => { expect(result).toEqual({ type: "message", role: "assistant", - content: [{ type: "text", text: " world" }], + content: [{ type: "text", text: " world", citations: null }], model: "test-model", }) }) diff --git a/src/api/transform/__tests__/openai-format.test.ts b/src/api/transform/__tests__/openai-format.test.ts index f37d369d70..812208acd1 100644 --- a/src/api/transform/__tests__/openai-format.test.ts +++ b/src/api/transform/__tests__/openai-format.test.ts @@ -1,3 +1,5 @@ +// npx jest src/api/transform/__tests__/openai-format.test.ts + import { convertToOpenAiMessages, convertToAnthropicMessage } from "../openai-format" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" @@ -172,11 +174,14 @@ describe("OpenAI Format Transformations", () => { expect(anthropicMessage.content[0]).toEqual({ type: "text", text: "Hello there!", + citations: null, }) expect(anthropicMessage.stop_reason).toBe("end_turn") expect(anthropicMessage.usage).toEqual({ input_tokens: 10, output_tokens: 5, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, }) }) @@ -221,6 +226,7 @@ describe("OpenAI Format Transformations", () => { expect(anthropicMessage.content[0]).toEqual({ type: "text", text: "Let me check the weather.", + citations: null, }) expect(anthropicMessage.content[1]).toEqual({ type: "tool_use", diff --git a/src/api/transform/__tests__/vscode-lm-format.test.ts b/src/api/transform/__tests__/vscode-lm-format.test.ts index b27097fd17..eb800e2b7a 100644 --- a/src/api/transform/__tests__/vscode-lm-format.test.ts +++ b/src/api/transform/__tests__/vscode-lm-format.test.ts @@ -1,3 +1,5 @@ +// npx jest src/api/transform/__tests__/vscode-lm-format.test.ts + import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" import { convertToVsCodeLmMessages, convertToAnthropicRole, convertToAnthropicMessage } from "../vscode-lm-format" @@ -216,6 +218,7 @@ describe("vscode-lm-format", () => { expect(result.content[0]).toEqual({ type: "text", text: "Hello", + citations: null, }) expect(result.id).toBe("test-uuid") }) From bf1aa4c7b266325d7dee86dd290dbff39f32f608 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 16:47:27 -0800 Subject: [PATCH 04/11] Add settings --- src/api/providers/anthropic.ts | 37 +++++++++++++------ .../src/components/settings/ApiOptions.tsx | 31 +++++++++++++++- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 4bde475e80..0d4b67b067 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -33,12 +33,16 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { let stream: AnthropicStream const cacheControl: CacheControlEphemeral = { type: "ephemeral" } const modelId = this.getModel().id + const maxTokens = this.getModel().info.maxTokens || 8192 + let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE let thinking: BetaThinkingConfigParam | undefined = undefined if (THINKING_MODELS.includes(modelId)) { thinking = this.options.anthropicThinking ? { type: "enabled", budget_tokens: this.options.anthropicThinking } : { type: "disabled" } + + temperature = 1.0 } switch (modelId) { @@ -62,22 +66,18 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { stream = await this.client.messages.create( { model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, - temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, - system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], // setting cache breakpoint for system prompt so new tasks can reuse it + max_tokens: maxTokens, + temperature, + thinking, + // Setting cache breakpoint for system prompt so new tasks can reuse it. + system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }], messages: messages.map((message, index) => { if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { return { ...message, content: typeof message.content === "string" - ? [ - { - type: "text", - text: message.content, - cache_control: cacheControl, - }, - ] + ? [{ type: "text", text: message.content, cache_control: cacheControl }] : message.content.map((content, contentIndex) => contentIndex === message.content.length - 1 ? { ...content, cache_control: cacheControl } @@ -91,7 +91,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { // tool_choice: { type: "auto" }, // tools: tools, stream: true, - thinking, }, (() => { // prompt caching: https://x.com/alexalbert__/status/1823751995901272068 @@ -132,6 +131,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { case "message_start": // Tells us cache reads/writes/input/output. const usage = chunk.message.usage + console.log("usage", usage) yield { type: "usage", @@ -158,6 +158,12 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { case "content_block_start": switch (chunk.content_block.type) { case "thinking": + // We may receive multiple text blocks, in which + // case just insert a line break between them. + if (chunk.index > 0) { + yield { type: "reasoning", text: "\n" } + } + yield { type: "reasoning", text: chunk.content_block.thinking } break case "text": @@ -173,10 +179,14 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { break case "content_block_delta": switch (chunk.delta.type) { + case "thinking_delta": + yield { type: "reasoning", text: chunk.delta.thinking } + break case "text_delta": yield { type: "text", text: chunk.delta.text } break } + break case "content_block_stop": break @@ -186,10 +196,12 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { getModel(): { id: AnthropicModelId; info: ModelInfo } { const modelId = this.options.apiModelId + if (modelId && modelId in anthropicModels) { const id = modelId as AnthropicModelId return { id, info: anthropicModels[id] } } + return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] } } @@ -204,14 +216,17 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { }) const content = response.content[0] + if (content.type === "text") { return content.text } + return "" } catch (error) { if (error instanceof Error) { throw new Error(`Anthropic completion error: ${error.message}`) } + throw error } } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 1303e79c7a..48529d4852 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -43,6 +43,7 @@ import { UnboundModelPicker } from "./UnboundModelPicker" import { ModelInfoView } from "./ModelInfoView" import { DROPDOWN_Z_INDEX } from "./styles" import { RequestyModelPicker } from "./RequestyModelPicker" +import { Slider } from "../ui" interface ApiOptionsProps { uriScheme: string | undefined @@ -65,6 +66,8 @@ const ApiOptions = ({ const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [anthropicThinkingEnabled, setAnthropicThinkingEnabled] = useState(!!apiConfiguration?.anthropicThinking) + const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking ?? 1024) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) @@ -1224,7 +1227,6 @@ const ApiOptions = ({ )} {selectedProvider === "glama" && } - {selectedProvider === "openrouter" && } {selectedProvider === "requesty" && } @@ -1258,8 +1260,33 @@ const ApiOptions = ({ )} + {selectedProvider === "anthropic" && ( +
+ + Thinking? + + {anthropicThinkingEnabled && ( + <> +
+ Number of tokens Claude is allowed use for its internal reasoning process +
+
+ setAnthropicThinkingBudget(value[0])} + /> +
{anthropicThinkingBudget}
+
+ + )} +
+ )} + {!fromWelcomeView && ( -
+
Date: Mon, 24 Feb 2025 21:08:08 -0800 Subject: [PATCH 05/11] Add tests, remove unused code --- .../transform/__tests__/gemini-format.test.ts | 338 ++++++++++++++++++ src/api/transform/gemini-format.ts | 111 +----- src/api/transform/vscode-lm-format.ts | 1 + 3 files changed, 342 insertions(+), 108 deletions(-) create mode 100644 src/api/transform/__tests__/gemini-format.test.ts diff --git a/src/api/transform/__tests__/gemini-format.test.ts b/src/api/transform/__tests__/gemini-format.test.ts new file mode 100644 index 0000000000..fe6b256404 --- /dev/null +++ b/src/api/transform/__tests__/gemini-format.test.ts @@ -0,0 +1,338 @@ +// npx jest src/api/transform/__tests__/gemini-format.test.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { convertAnthropicMessageToGemini } from "../gemini-format" + +describe("convertAnthropicMessageToGemini", () => { + it("should convert a simple text message", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: "Hello, world!", + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [{ text: "Hello, world!" }], + }) + }) + + it("should convert assistant role to model role", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: "I'm an assistant", + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "model", + parts: [{ text: "I'm an assistant" }], + }) + }) + + it("should convert a message with text blocks", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "First paragraph" }, + { type: "text", text: "Second paragraph" }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [{ text: "First paragraph" }, { text: "Second paragraph" }], + }) + }) + + it("should convert a message with an image", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "Check out this image:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64encodeddata", + }, + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { text: "Check out this image:" }, + { + inlineData: { + data: "base64encodeddata", + mimeType: "image/jpeg", + }, + }, + ], + }) + }) + + it("should throw an error for unsupported image source type", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "image", + source: { + type: "url", // Not supported + url: "https://example.com/image.jpg", + } as any, + }, + ], + } + + expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow("Unsupported image source type") + }) + + it("should convert a message with tool use", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: [ + { type: "text", text: "Let me calculate that for you." }, + { + type: "tool_use", + id: "calc-123", + name: "calculator", + input: { operation: "add", numbers: [2, 3] }, + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "model", + parts: [ + { text: "Let me calculate that for you." }, + { + functionCall: { + name: "calculator", + args: { operation: "add", numbers: [2, 3] }, + }, + }, + ], + }) + }) + + it("should convert a message with tool result as string", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "Here's the result:" }, + { + type: "tool_result", + tool_use_id: "calculator-123", + content: "The result is 5", + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { text: "Here's the result:" }, + { + functionResponse: { + name: "calculator", + response: { + name: "calculator", + content: "The result is 5", + }, + }, + }, + ], + }) + }) + + it("should handle empty tool result content", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "calculator-123", + content: null as any, // Empty content + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + // Should skip the empty tool result + expect(result).toEqual({ + role: "user", + parts: [], + }) + }) + + it("should convert a message with tool result as array with text only", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: [ + { type: "text", text: "First result" }, + { type: "text", text: "Second result" }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "search", + response: { + name: "search", + content: "First result\n\nSecond result", + }, + }, + }, + ], + }) + }) + + it("should convert a message with tool result as array with text and images", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: [ + { type: "text", text: "Search results:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "image1data", + }, + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "image2data", + }, + }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "search", + response: { + name: "search", + content: "Search results:\n\n(See next part for image)", + }, + }, + }, + { + inlineData: { + data: "image1data", + mimeType: "image/png", + }, + }, + { + inlineData: { + data: "image2data", + mimeType: "image/jpeg", + }, + }, + ], + }) + }) + + it("should convert a message with tool result containing only images", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "imagesearch-123", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "onlyimagedata", + }, + }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "imagesearch", + response: { + name: "imagesearch", + content: "\n\n(See next part for image)", + }, + }, + }, + { + inlineData: { + data: "onlyimagedata", + mimeType: "image/png", + }, + }, + ], + }) + }) + + it("should throw an error for unsupported content block type", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "unknown_type", // Unsupported type + data: "some data", + } as any, + ], + } + + expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow( + "Unsupported content block type: unknown_type", + ) + }) +}) diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 3a149dc149..c8fc80d769 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -1,20 +1,11 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { - Content, - EnhancedGenerateContentResponse, - FunctionCallPart, - FunctionDeclaration, - FunctionResponsePart, - InlineDataPart, - Part, - SchemaType, - TextPart, -} from "@google/generative-ai" +import { Content, FunctionCallPart, FunctionResponsePart, InlineDataPart, Part, TextPart } from "@google/generative-ai" -export function convertAnthropicContentToGemini(content: Anthropic.Messages.MessageParam["content"]): Part[] { +function convertAnthropicContentToGemini(content: Anthropic.Messages.MessageParam["content"]): Part[] { if (typeof content === "string") { return [{ text: content } as TextPart] } + return content.flatMap((block) => { switch (block.type) { case "text": @@ -90,99 +81,3 @@ export function convertAnthropicMessageToGemini(message: Anthropic.Messages.Mess parts: convertAnthropicContentToGemini(message.content), } } - -export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration { - return { - name: tool.name, - description: tool.description || "", - parameters: { - type: SchemaType.OBJECT, - properties: Object.fromEntries( - Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [ - key, - { - type: (value as any).type.toUpperCase(), - description: (value as any).description || "", - }, - ]), - ), - required: (tool.input_schema.required as string[]) || [], - }, - } -} - -/* -It looks like gemini likes to double escape certain characters when writing file contents: https://discuss.ai.google.dev/t/function-call-string-property-is-double-escaped/37867 -*/ -export function unescapeGeminiContent(content: string) { - return content - .replace(/\\n/g, "\n") - .replace(/\\'/g, "'") - .replace(/\\"/g, '"') - .replace(/\\r/g, "\r") - .replace(/\\t/g, "\t") -} - -export function convertGeminiResponseToAnthropic( - response: EnhancedGenerateContentResponse, -): Anthropic.Messages.Message { - const content: Anthropic.Messages.ContentBlock[] = [] - - // Add the main text response - const text = response.text() - if (text) { - content.push({ type: "text", text, citations: null }) - } - - // Add function calls as tool_use blocks - const functionCalls = response.functionCalls() - if (functionCalls) { - functionCalls.forEach((call, index) => { - if ("content" in call.args && typeof call.args.content === "string") { - call.args.content = unescapeGeminiContent(call.args.content) - } - content.push({ - type: "tool_use", - id: `${call.name}-${index}-${Date.now()}`, - name: call.name, - input: call.args, - }) - }) - } - - // Determine stop reason - let stop_reason: Anthropic.Messages.Message["stop_reason"] = null - const finishReason = response.candidates?.[0]?.finishReason - if (finishReason) { - switch (finishReason) { - case "STOP": - stop_reason = "end_turn" - break - case "MAX_TOKENS": - stop_reason = "max_tokens" - break - case "SAFETY": - case "RECITATION": - case "OTHER": - stop_reason = "stop_sequence" - break - // Add more cases if needed - } - } - - return { - id: `msg_${Date.now()}`, // Generate a unique ID - type: "message", - role: "assistant", - content, - model: "", - stop_reason, - stop_sequence: null, // Gemini doesn't provide this information - usage: { - input_tokens: response.usageMetadata?.promptTokenCount ?? 0, - output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }, - } -} diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index f258dd1732..85c2fc7ba5 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -160,6 +160,7 @@ export async function convertToAnthropicMessage( vsCodeLmMessage: vscode.LanguageModelChatMessage, ): Promise { const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role) + if (anthropicRole !== "assistant") { throw new Error("Roo Code : Only assistant messages are supported.") } From 849f8bf8e814d9321324c7d19f3b6063b880b67a Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 21:13:23 -0800 Subject: [PATCH 06/11] Add tests --- .../__tests__/mistral-format.test.ts | 301 ++++++++++++++++++ src/api/transform/mistral-format.ts | 2 +- 2 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 src/api/transform/__tests__/mistral-format.test.ts diff --git a/src/api/transform/__tests__/mistral-format.test.ts b/src/api/transform/__tests__/mistral-format.test.ts new file mode 100644 index 0000000000..b8e9412eda --- /dev/null +++ b/src/api/transform/__tests__/mistral-format.test.ts @@ -0,0 +1,301 @@ +// npx jest src/api/transform/__tests__/mistral-format.test.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { convertToMistralMessages } from "../mistral-format" + +describe("convertToMistralMessages", () => { + it("should convert simple text messages for user and assistant roles", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello", + }, + { + role: "assistant", + content: "Hi there!", + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(2) + expect(mistralMessages[0]).toEqual({ + role: "user", + content: "Hello", + }) + expect(mistralMessages[1]).toEqual({ + role: "assistant", + content: "Hi there!", + }) + }) + + it("should handle user messages with image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "What is in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", + }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("user") + + const content = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + + expect(Array.isArray(content)).toBe(true) + expect(content).toHaveLength(2) + expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(content[1]).toEqual({ + type: "image_url", + imageUrl: { url: "data:image/jpeg;base64,base64data" }, + }) + }) + + it("should handle user messages with only tool results", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", + }, + ], + }, + ] + + // Based on the implementation, tool results without accompanying text/image + // don't generate any messages + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(0) + }) + + it("should handle user messages with mixed content (text, image, and tool results)", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Here's the weather data and an image:", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "imagedata123", + }, + }, + { + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + // Based on the implementation, only the text and image content is included + // Tool results are not converted to separate messages + expect(mistralMessages).toHaveLength(1) + + // Message should be the user message with text and image + expect(mistralMessages[0].role).toBe("user") + const userContent = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + expect(Array.isArray(userContent)).toBe(true) + expect(userContent).toHaveLength(2) + expect(userContent[0]).toEqual({ type: "text", text: "Here's the weather data and an image:" }) + expect(userContent[1]).toEqual({ + type: "image_url", + imageUrl: { url: "data:image/png;base64,imagedata123" }, + }) + }) + + it("should handle assistant messages with text content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "I'll help you with that question.", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("I'll help you with that question.") + }) + + it("should handle assistant messages with tool use", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "Let me check the weather for you.", + }, + { + type: "tool_use", + id: "weather-123", + name: "get_weather", + input: { city: "London" }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("Let me check the weather for you.") + }) + + it("should handle multiple text blocks in assistant messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "First paragraph of information.", + }, + { + type: "text", + text: "Second paragraph with more details.", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("First paragraph of information.\nSecond paragraph with more details.") + }) + + it("should handle a conversation with mixed message types", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "What's in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "imagedata", + }, + }, + ], + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "This image shows a landscape with mountains.", + }, + { + type: "tool_use", + id: "search-123", + name: "search_info", + input: { query: "mountain types" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: "Found information about different mountain types.", + }, + ], + }, + { + role: "assistant", + content: "Based on the search results, I can tell you more about the mountains in the image.", + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + // Based on the implementation, user messages with only tool results don't generate messages + expect(mistralMessages).toHaveLength(3) + + // User message with image + expect(mistralMessages[0].role).toBe("user") + const userContent = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + expect(Array.isArray(userContent)).toBe(true) + expect(userContent).toHaveLength(2) + + // Assistant message with text (tool_use is not included in Mistral format) + expect(mistralMessages[1].role).toBe("assistant") + expect(mistralMessages[1].content).toBe("This image shows a landscape with mountains.") + + // Final assistant message + expect(mistralMessages[2]).toEqual({ + role: "assistant", + content: "Based on the search results, I can tell you more about the mountains in the image.", + }) + }) + + it("should handle empty content in assistant messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "search-123", + name: "search_info", + input: { query: "test query" }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBeUndefined() + }) +}) diff --git a/src/api/transform/mistral-format.ts b/src/api/transform/mistral-format.ts index 16c6aaf238..baf81ef24d 100644 --- a/src/api/transform/mistral-format.ts +++ b/src/api/transform/mistral-format.ts @@ -1,5 +1,4 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { Mistral } from "@mistralai/mistralai" import { AssistantMessage } from "@mistralai/mistralai/models/components/assistantmessage" import { SystemMessage } from "@mistralai/mistralai/models/components/systemmessage" import { ToolMessage } from "@mistralai/mistralai/models/components/toolmessage" @@ -13,6 +12,7 @@ export type MistralMessage = export function convertToMistralMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): MistralMessage[] { const mistralMessages: MistralMessage[] = [] + for (const anthropicMessage of anthropicMessages) { if (typeof anthropicMessage.content === "string") { mistralMessages.push({ From 09902904f371a89b980d68c757023808f363be7c Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 22:08:14 -0800 Subject: [PATCH 07/11] Persist anthropicThinking --- src/core/webview/ClineProvider.ts | 6 +++ .../__tests__/checkExistApiConfig.test.ts | 1 + .../src/components/settings/ApiOptions.tsx | 38 +++++++++++----- .../src/components/settings/SettingsView.tsx | 44 +++++++++---------- webview-ui/src/components/ui/index.ts | 1 + 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b4819d9683..99caceffc2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -89,6 +89,7 @@ type GlobalStateKey = | "lmStudioModelId" | "lmStudioBaseUrl" | "anthropicBaseUrl" + | "anthropicThinking" | "azureApiVersion" | "openAiStreamingEnabled" | "openRouterModelId" @@ -1654,6 +1655,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, @@ -1701,6 +1703,7 @@ 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), @@ -2510,6 +2513,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, @@ -2592,6 +2596,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("lmStudioModelId") as Promise, this.getGlobalState("lmStudioBaseUrl") as Promise, this.getGlobalState("anthropicBaseUrl") as Promise, + this.getGlobalState("anthropicThinking") as Promise, this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, @@ -2691,6 +2696,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts index 914f4933d6..62517d6958 100644 --- a/src/shared/__tests__/checkExistApiConfig.test.ts +++ b/src/shared/__tests__/checkExistApiConfig.test.ts @@ -32,6 +32,7 @@ describe("checkExistKey", () => { apiKey: "test-key", apiProvider: undefined, anthropicBaseUrl: undefined, + anthropicThinking: undefined, } expect(checkExistKey(config)).toBe(true) }) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 48529d4852..3761ddce50 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -2,9 +2,10 @@ import { memo, useCallback, useMemo, useState } from "react" import { useDebounce, useEvent } from "react-use" import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { TemperatureControl } from "./TemperatureControl" import * as vscodemodels from "vscode" +import { Slider } from "@/components/ui" + import { ApiConfiguration, ModelInfo, @@ -34,6 +35,7 @@ import { requestyDefaultModelInfo, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" + import { vscode } from "../../utils/vscode" import VSCodeButtonLink from "../common/VSCodeButtonLink" import { OpenRouterModelPicker } from "./OpenRouterModelPicker" @@ -43,7 +45,7 @@ import { UnboundModelPicker } from "./UnboundModelPicker" import { ModelInfoView } from "./ModelInfoView" import { DROPDOWN_Z_INDEX } from "./styles" import { RequestyModelPicker } from "./RequestyModelPicker" -import { Slider } from "../ui" +import { TemperatureControl } from "./TemperatureControl" interface ApiOptionsProps { uriScheme: string | undefined @@ -66,8 +68,7 @@ const ApiOptions = ({ const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) - const [anthropicThinkingEnabled, setAnthropicThinkingEnabled] = useState(!!apiConfiguration?.anthropicThinking) - const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking ?? 1024) + const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) @@ -188,6 +189,7 @@ const ApiOptions = ({ checked={anthropicBaseUrlSelected} onChange={(checked: boolean) => { setAnthropicBaseUrlSelected(checked) + if (!checked) { setApiConfigurationField("anthropicBaseUrl", "") } @@ -387,6 +389,7 @@ const ApiOptions = ({ checked={openRouterBaseUrlSelected} onChange={(checked: boolean) => { setOpenRouterBaseUrlSelected(checked) + if (!checked) { setApiConfigurationField("openRouterBaseUrl", "") } @@ -510,7 +513,7 @@ const ApiOptions = ({
)} - {apiConfiguration?.apiProvider === "vertex" && ( + {selectedProvider === "vertex" && (
{ setAzureApiVersionSelected(checked) + if (!checked) { setApiConfigurationField("azureApiVersion", "") } @@ -1260,23 +1264,33 @@ const ApiOptions = ({ )} - {selectedProvider === "anthropic" && ( + {selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219" && (
- + { + const budget = checked ? 16_384 : undefined + setAnthropicThinkingBudget(budget) + setApiConfigurationField("anthropicThinking", budget) + }}> Thinking? - {anthropicThinkingEnabled && ( + {anthropicThinkingBudget && ( <>
- Number of tokens Claude is allowed use for its internal reasoning process + Number of tokens Claude is allowed use for its internal reasoning process.
setAnthropicThinkingBudget(value[0])} + onValueChange={(value) => { + const budget = value[0] + setAnthropicThinkingBudget(budget) + setApiConfigurationField("anthropicThinking", budget) + }} />
{anthropicThinkingBudget}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 0d80580b49..29384826ac 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,15 +1,7 @@ -import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react" -import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext" -import { validateApiConfiguration, validateModelId } from "../../utils/validate" -import { vscode } from "../../utils/vscode" -import ApiOptions from "./ApiOptions" -import ExperimentalFeature from "./ExperimentalFeature" -import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" -import ApiConfigManager from "./ApiConfigManager" -import { Dropdown } from "vscrui" -import type { DropdownOption } from "vscrui" -import { ApiConfiguration } from "../../../../src/shared/api" +import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { Dropdown, type DropdownOption } from "vscrui" + import { AlertDialog, AlertDialogContent, @@ -19,7 +11,17 @@ import { AlertDialogAction, AlertDialogHeader, AlertDialogFooter, -} from "../ui/alert-dialog" +} from "@/components/ui" + +import { vscode } from "../../utils/vscode" +import { validateApiConfiguration, validateModelId } from "../../utils/validate" +import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext" +import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" +import { ApiConfiguration } from "../../../../src/shared/api" + +import ExperimentalFeature from "./ExperimentalFeature" +import ApiConfigManager from "./ApiConfigManager" +import ApiOptions from "./ApiOptions" type SettingsViewProps = { onDone: () => void @@ -104,7 +106,9 @@ const SettingsView = forwardRef(({ onDone }, if (prevState.apiConfiguration?.[field] === value) { return prevState } + setChangeDetected(true) + return { ...prevState, apiConfiguration: { @@ -131,7 +135,9 @@ const SettingsView = forwardRef(({ onDone }, }, []) const handleSubmit = () => { + console.log("handleSubmit", apiConfiguration) const apiValidationResult = validateApiConfiguration(apiConfiguration) + const modelIdValidationResult = validateModelId( apiConfiguration, extensionState.glamaModels, @@ -140,6 +146,7 @@ const SettingsView = forwardRef(({ onDone }, setApiErrorMessage(apiValidationResult) setModelIdErrorMessage(modelIdValidationResult) + if (!apiValidationResult && !modelIdValidationResult) { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) @@ -162,18 +169,9 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds }) vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext }) vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) - vscode.postMessage({ - type: "updateExperimental", - values: experiments, - }) + vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) - - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentApiConfigName, - apiConfiguration, - }) - // onDone() + vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) setChangeDetected(false) } } diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index bf00aa6442..6eb8dd25ba 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -1,3 +1,4 @@ +export * from "./alert-dialog" export * from "./autosize-textarea" export * from "./badge" export * from "./button" From 52030b12e1bee2fbf6164fdc2c6009a5346b599d Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 22:13:45 -0800 Subject: [PATCH 08/11] Remove unused code --- src/api/providers/bedrock.ts | 2 +- .../__tests__/bedrock-converse-format.test.ts | 375 +++++++----------- .../transform/__tests__/openai-format.test.ts | 344 +++++----------- .../__tests__/vscode-lm-format.test.ts | 279 +++++-------- src/api/transform/bedrock-converse-format.ts | 52 +-- src/api/transform/openai-format.ts | 60 --- src/api/transform/vscode-lm-format.ts | 47 --- 7 files changed, 349 insertions(+), 810 deletions(-) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 8f897fda2a..3bca70338d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -9,7 +9,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { ApiHandler, SingleCompletionHandler } from "../" import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api" import { ApiStream } from "../transform/stream" -import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../transform/bedrock-converse-format" +import { convertToBedrockConverseMessages } from "../transform/bedrock-converse-format" const BEDROCK_DEFAULT_TEMPERATURE = 0.3 diff --git a/src/api/transform/__tests__/bedrock-converse-format.test.ts b/src/api/transform/__tests__/bedrock-converse-format.test.ts index fdd29c75bf..c56b8a07fc 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.test.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.test.ts @@ -1,254 +1,167 @@ // npx jest src/api/transform/__tests__/bedrock-converse-format.test.ts -import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../bedrock-converse-format" +import { convertToBedrockConverseMessages } from "../bedrock-converse-format" import { Anthropic } from "@anthropic-ai/sdk" import { ContentBlock, ToolResultContentBlock } from "@aws-sdk/client-bedrock-runtime" -import { StreamEvent } from "../../providers/bedrock" -describe("bedrock-converse-format", () => { - describe("convertToBedrockConverseMessages", () => { - test("converts simple text messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { role: "user", content: "Hello" }, - { role: "assistant", content: "Hi there" }, - ] +describe("convertToBedrockConverseMessages", () => { + test("converts simple text messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] - const result = convertToBedrockConverseMessages(messages) + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual([ - { - role: "user", - content: [{ text: "Hello" }], - }, - { - role: "assistant", - content: [{ text: "Hi there" }], - }, - ]) - }) - - test("converts messages with images correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Look at this image:", - }, - { - type: "image", - source: { - type: "base64", - data: "SGVsbG8=", // "Hello" in base64 - media_type: "image/jpeg" as const, - }, - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(2) - expect(result[0].content[0]).toEqual({ text: "Look at this image:" }) - - const imageBlock = result[0].content[1] as ContentBlock - if ("image" in imageBlock && imageBlock.image && imageBlock.image.source) { - expect(imageBlock.image.format).toBe("jpeg") - expect(imageBlock.image.source).toBeDefined() - expect(imageBlock.image.source.bytes).toBeDefined() - } else { - fail("Expected image block not found") - } - }) - - test("converts tool use messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "tool_use", - id: "test-id", - name: "read_file", - input: { - path: "test.txt", - }, - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("assistant") - const toolBlock = result[0].content[0] as ContentBlock - if ("toolUse" in toolBlock && toolBlock.toolUse) { - expect(toolBlock.toolUse).toEqual({ - toolUseId: "test-id", - name: "read_file", - input: "\n\ntest.txt\n\n", - }) - } else { - fail("Expected tool use block not found") - } - }) - - test("converts tool result messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "tool_result", - tool_use_id: "test-id", - content: [{ type: "text", text: "File contents here" }], - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("assistant") - const resultBlock = result[0].content[0] as ContentBlock - if ("toolResult" in resultBlock && resultBlock.toolResult) { - const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }] - expect(resultBlock.toolResult).toEqual({ - toolUseId: "test-id", - content: expectedContent, - status: "success", - }) - } else { - fail("Expected tool result block not found") - } - }) - - test("handles text content correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Hello world", - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(1) - const textBlock = result[0].content[0] as ContentBlock - expect(textBlock).toEqual({ text: "Hello world" }) - }) + expect(result).toEqual([ + { + role: "user", + content: [{ text: "Hello" }], + }, + { + role: "assistant", + content: [{ text: "Hi there" }], + }, + ]) }) - describe("convertToAnthropicMessage", () => { - test("converts metadata events correctly", () => { - const event: StreamEvent = { - metadata: { - usage: { - inputTokens: 10, - outputTokens: 20, + test("converts messages with images correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Look at this image:", }, - }, - } - - const result = convertToAnthropicMessage(event, "test-model") - - expect(result).toEqual({ - id: "", - type: "message", - role: "assistant", - model: "test-model", - usage: { - input_tokens: 10, - output_tokens: 20, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }, - }) - }) - - test("converts content block start events correctly", () => { - const event: StreamEvent = { - contentBlockStart: { - start: { - text: "Hello", + { + type: "image", + source: { + type: "base64", + data: "SGVsbG8=", // "Hello" in base64 + media_type: "image/jpeg" as const, + }, }, - }, - } + ], + }, + ] - const result = convertToAnthropicMessage(event, "test-model") + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual({ - type: "message", + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(2) + expect(result[0].content[0]).toEqual({ text: "Look at this image:" }) + + const imageBlock = result[0].content[1] as ContentBlock + if ("image" in imageBlock && imageBlock.image && imageBlock.image.source) { + expect(imageBlock.image.format).toBe("jpeg") + expect(imageBlock.image.source).toBeDefined() + expect(imageBlock.image.source.bytes).toBeDefined() + } else { + fail("Expected image block not found") + } + }) + + test("converts tool use messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "assistant", - content: [{ type: "text", text: "Hello", citations: null }], - model: "test-model", - }) - }) - - test("converts content block delta events correctly", () => { - const event: StreamEvent = { - contentBlockDelta: { - delta: { - text: " world", + content: [ + { + type: "tool_use", + id: "test-id", + name: "read_file", + input: { + path: "test.txt", + }, }, - }, - } + ], + }, + ] - const result = convertToAnthropicMessage(event, "test-model") + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual({ - type: "message", - role: "assistant", - content: [{ type: "text", text: " world", citations: null }], - model: "test-model", + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("assistant") + const toolBlock = result[0].content[0] as ContentBlock + if ("toolUse" in toolBlock && toolBlock.toolUse) { + expect(toolBlock.toolUse).toEqual({ + toolUseId: "test-id", + name: "read_file", + input: "\n\ntest.txt\n\n", }) - }) + } else { + fail("Expected tool use block not found") + } + }) - test("converts message stop events correctly", () => { - const event: StreamEvent = { - messageStop: { - stopReason: "end_turn" as const, - }, - } - - const result = convertToAnthropicMessage(event, "test-model") - - expect(result).toEqual({ - type: "message", + test("converts tool result messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "assistant", - stop_reason: "end_turn", - stop_sequence: null, - model: "test-model", + content: [ + { + type: "tool_result", + tool_use_id: "test-id", + content: [{ type: "text", text: "File contents here" }], + }, + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages) + + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("assistant") + const resultBlock = result[0].content[0] as ContentBlock + if ("toolResult" in resultBlock && resultBlock.toolResult) { + const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }] + expect(resultBlock.toolResult).toEqual({ + toolUseId: "test-id", + content: expectedContent, + status: "success", }) - }) + } else { + fail("Expected tool result block not found") + } + }) + + test("handles text content correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Hello world", + }, + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages) + + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(1) + const textBlock = result[0].content[0] as ContentBlock + expect(textBlock).toEqual({ text: "Hello world" }) }) }) diff --git a/src/api/transform/__tests__/openai-format.test.ts b/src/api/transform/__tests__/openai-format.test.ts index 812208acd1..f0aa5e1a56 100644 --- a/src/api/transform/__tests__/openai-format.test.ts +++ b/src/api/transform/__tests__/openai-format.test.ts @@ -1,281 +1,131 @@ // npx jest src/api/transform/__tests__/openai-format.test.ts -import { convertToOpenAiMessages, convertToAnthropicMessage } from "../openai-format" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -type PartialChatCompletion = Omit & { - choices: Array< - Partial & { - message: OpenAI.Chat.Completions.ChatCompletion.Choice["message"] - finish_reason: string - index: number - } - > -} +import { convertToOpenAiMessages } from "../openai-format" -describe("OpenAI Format Transformations", () => { - describe("convertToOpenAiMessages", () => { - it("should convert simple text messages", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: "Hello", - }, - { - role: "assistant", - content: "Hi there!", - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(2) - expect(openAiMessages[0]).toEqual({ +describe("convertToOpenAiMessages", () => { + it("should convert simple text messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello", - }) - expect(openAiMessages[1]).toEqual({ + }, + { role: "assistant", content: "Hi there!", - }) + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(2) + expect(openAiMessages[0]).toEqual({ + role: "user", + content: "Hello", }) - - it("should handle messages with image content", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "What is in this image?", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/jpeg", - data: "base64data", - }, - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - expect(openAiMessages[0].role).toBe("user") - - const content = openAiMessages[0].content as Array<{ - type: string - text?: string - image_url?: { url: string } - }> - - expect(Array.isArray(content)).toBe(true) - expect(content).toHaveLength(2) - expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) - expect(content[1]).toEqual({ - type: "image_url", - image_url: { url: "data:image/jpeg;base64,base64data" }, - }) - }) - - it("should handle assistant messages with tool use", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me check the weather.", - }, - { - type: "tool_use", - id: "weather-123", - name: "get_weather", - input: { city: "London" }, - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - - const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam - expect(assistantMessage.role).toBe("assistant") - expect(assistantMessage.content).toBe("Let me check the weather.") - expect(assistantMessage.tool_calls).toHaveLength(1) - expect(assistantMessage.tool_calls![0]).toEqual({ - id: "weather-123", - type: "function", - function: { - name: "get_weather", - arguments: JSON.stringify({ city: "London" }), - }, - }) - }) - - it("should handle user messages with tool results", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "tool_result", - tool_use_id: "weather-123", - content: "Current temperature in London: 20°C", - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - - const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam - expect(toolMessage.role).toBe("tool") - expect(toolMessage.tool_call_id).toBe("weather-123") - expect(toolMessage.content).toBe("Current temperature in London: 20°C") + expect(openAiMessages[1]).toEqual({ + role: "assistant", + content: "Hi there!", }) }) - describe("convertToAnthropicMessage", () => { - it("should convert simple completion", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle messages with image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ { - message: { - role: "assistant", - content: "Hello there!", - refusal: null, + type: "text", + text: "What is in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", }, - finish_reason: "stop", - index: 0, }, ], - usage: { - prompt_tokens: 10, - completion_tokens: 5, - total_tokens: 15, - }, - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.id).toBe("completion-123") - expect(anthropicMessage.role).toBe("assistant") - expect(anthropicMessage.content).toHaveLength(1) - expect(anthropicMessage.content[0]).toEqual({ - type: "text", - text: "Hello there!", - citations: null, - }) - expect(anthropicMessage.stop_reason).toBe("end_turn") - expect(anthropicMessage.usage).toEqual({ - input_tokens: 10, - output_tokens: 5, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }) + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + expect(openAiMessages[0].role).toBe("user") + + const content = openAiMessages[0].content as Array<{ + type: string + text?: string + image_url?: { url: string } + }> + + expect(Array.isArray(content)).toBe(true) + expect(content).toHaveLength(2) + expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(content[1]).toEqual({ + type: "image_url", + image_url: { url: "data:image/jpeg;base64,base64data" }, }) + }) - it("should handle tool calls in completion", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle assistant messages with tool use", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ { - message: { - role: "assistant", - content: "Let me check the weather.", - tool_calls: [ - { - id: "weather-123", - type: "function", - function: { - name: "get_weather", - arguments: '{"city":"London"}', - }, - }, - ], - refusal: null, - }, - finish_reason: "tool_calls", - index: 0, + type: "text", + text: "Let me check the weather.", + }, + { + type: "tool_use", + id: "weather-123", + name: "get_weather", + input: { city: "London" }, }, ], - usage: { - prompt_tokens: 15, - completion_tokens: 8, - total_tokens: 23, - }, - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.content).toHaveLength(2) - expect(anthropicMessage.content[0]).toEqual({ - type: "text", - text: "Let me check the weather.", - citations: null, - }) - expect(anthropicMessage.content[1]).toEqual({ - type: "tool_use", - id: "weather-123", + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam + expect(assistantMessage.role).toBe("assistant") + expect(assistantMessage.content).toBe("Let me check the weather.") + expect(assistantMessage.tool_calls).toHaveLength(1) + expect(assistantMessage.tool_calls![0]).toEqual({ + id: "weather-123", + type: "function", + function: { name: "get_weather", - input: { city: "London" }, - }) - expect(anthropicMessage.stop_reason).toBe("tool_use") + arguments: JSON.stringify({ city: "London" }), + }, }) + }) - it("should handle invalid tool call arguments", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle user messages with tool results", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ { - message: { - role: "assistant", - content: "Testing invalid arguments", - tool_calls: [ - { - id: "test-123", - type: "function", - function: { - name: "test_function", - arguments: "invalid json", - }, - }, - ], - refusal: null, - }, - finish_reason: "tool_calls", - index: 0, + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", }, ], - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.content).toHaveLength(2) - expect(anthropicMessage.content[1]).toEqual({ - type: "tool_use", - id: "test-123", - name: "test_function", - input: {}, // Should default to empty object for invalid JSON - }) - }) + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(toolMessage.role).toBe("tool") + expect(toolMessage.tool_call_id).toBe("weather-123") + expect(toolMessage.content).toBe("Current temperature in London: 20°C") }) }) diff --git a/src/api/transform/__tests__/vscode-lm-format.test.ts b/src/api/transform/__tests__/vscode-lm-format.test.ts index eb800e2b7a..eea8de7c9a 100644 --- a/src/api/transform/__tests__/vscode-lm-format.test.ts +++ b/src/api/transform/__tests__/vscode-lm-format.test.ts @@ -1,8 +1,8 @@ // npx jest src/api/transform/__tests__/vscode-lm-format.test.ts import { Anthropic } from "@anthropic-ai/sdk" -import * as vscode from "vscode" -import { convertToVsCodeLmMessages, convertToAnthropicRole, convertToAnthropicMessage } from "../vscode-lm-format" + +import { convertToVsCodeLmMessages, convertToAnthropicRole } from "../vscode-lm-format" // Mock crypto const mockCrypto = { @@ -29,14 +29,6 @@ interface MockLanguageModelToolResultPart { parts: MockLanguageModelTextPart[] } -type MockMessageContent = MockLanguageModelTextPart | MockLanguageModelToolCallPart | MockLanguageModelToolResultPart - -interface MockLanguageModelChatMessage { - role: string - name?: string - content: MockMessageContent[] -} - // Mock vscode namespace jest.mock("vscode", () => { const LanguageModelChatMessageRole = { @@ -86,174 +78,115 @@ jest.mock("vscode", () => { } }) -describe("vscode-lm-format", () => { - describe("convertToVsCodeLmMessages", () => { - it("should convert simple string messages", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { role: "user", content: "Hello" }, - { role: "assistant", content: "Hi there" }, - ] +describe("convertToVsCodeLmMessages", () => { + it("should convert simple string messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] - const result = convertToVsCodeLmMessages(messages) + const result = convertToVsCodeLmMessages(messages) - expect(result).toHaveLength(2) - expect(result[0].role).toBe("user") - expect((result[0].content[0] as MockLanguageModelTextPart).value).toBe("Hello") - expect(result[1].role).toBe("assistant") - expect((result[1].content[0] as MockLanguageModelTextPart).value).toBe("Hi there") - }) - - it("should handle complex user messages with tool results", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { type: "text", text: "Here is the result:" }, - { - type: "tool_result", - tool_use_id: "tool-1", - content: "Tool output", - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(2) - const [toolResult, textContent] = result[0].content as [ - MockLanguageModelToolResultPart, - MockLanguageModelTextPart, - ] - expect(toolResult.type).toBe("tool_result") - expect(textContent.type).toBe("text") - }) - - it("should handle complex assistant messages with tool calls", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { type: "text", text: "Let me help you with that." }, - { - type: "tool_use", - id: "tool-1", - name: "calculator", - input: { operation: "add", numbers: [2, 2] }, - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - expect(result[0].role).toBe("assistant") - expect(result[0].content).toHaveLength(2) - const [toolCall, textContent] = result[0].content as [ - MockLanguageModelToolCallPart, - MockLanguageModelTextPart, - ] - expect(toolCall.type).toBe("tool_call") - expect(textContent.type).toBe("text") - }) - - it("should handle image blocks with appropriate placeholders", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { type: "text", text: "Look at this:" }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "base64data", - }, - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart - expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]") - }) + expect(result).toHaveLength(2) + expect(result[0].role).toBe("user") + expect((result[0].content[0] as MockLanguageModelTextPart).value).toBe("Hello") + expect(result[1].role).toBe("assistant") + expect((result[1].content[0] as MockLanguageModelTextPart).value).toBe("Hi there") }) - describe("convertToAnthropicRole", () => { - it("should convert assistant role correctly", () => { - const result = convertToAnthropicRole("assistant" as any) - expect(result).toBe("assistant") - }) - - it("should convert user role correctly", () => { - const result = convertToAnthropicRole("user" as any) - expect(result).toBe("user") - }) - - it("should return null for unknown roles", () => { - const result = convertToAnthropicRole("unknown" as any) - expect(result).toBeNull() - }) - }) - - describe("convertToAnthropicMessage", () => { - it("should convert assistant message with text content", async () => { - const vsCodeMessage = { - role: "assistant", - name: "assistant", - content: [new vscode.LanguageModelTextPart("Hello")], - } - - const result = await convertToAnthropicMessage(vsCodeMessage as any) - - expect(result.role).toBe("assistant") - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "text", - text: "Hello", - citations: null, - }) - expect(result.id).toBe("test-uuid") - }) - - it("should convert assistant message with tool calls", async () => { - const vsCodeMessage = { - role: "assistant", - name: "assistant", - content: [ - new vscode.LanguageModelToolCallPart("call-1", "calculator", { operation: "add", numbers: [2, 2] }), - ], - } - - const result = await convertToAnthropicMessage(vsCodeMessage as any) - - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "tool_use", - id: "call-1", - name: "calculator", - input: { operation: "add", numbers: [2, 2] }, - }) - expect(result.id).toBe("test-uuid") - }) - - it("should throw error for non-assistant messages", async () => { - const vsCodeMessage = { + it("should handle complex user messages with tool results", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", - name: "user", - content: [new vscode.LanguageModelTextPart("Hello")], - } + content: [ + { type: "text", text: "Here is the result:" }, + { + type: "tool_result", + tool_use_id: "tool-1", + content: "Tool output", + }, + ], + }, + ] - await expect(convertToAnthropicMessage(vsCodeMessage as any)).rejects.toThrow( - "Roo Code : Only assistant messages are supported.", - ) - }) + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(2) + const [toolResult, textContent] = result[0].content as [ + MockLanguageModelToolResultPart, + MockLanguageModelTextPart, + ] + expect(toolResult.type).toBe("tool_result") + expect(textContent.type).toBe("text") + }) + + it("should handle complex assistant messages with tool calls", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Let me help you with that." }, + { + type: "tool_use", + id: "tool-1", + name: "calculator", + input: { operation: "add", numbers: [2, 2] }, + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("assistant") + expect(result[0].content).toHaveLength(2) + const [toolCall, textContent] = result[0].content as [MockLanguageModelToolCallPart, MockLanguageModelTextPart] + expect(toolCall.type).toBe("tool_call") + expect(textContent.type).toBe("text") + }) + + it("should handle image blocks with appropriate placeholders", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64data", + }, + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart + expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]") + }) +}) + +describe("convertToAnthropicRole", () => { + it("should convert assistant role correctly", () => { + const result = convertToAnthropicRole("assistant" as any) + expect(result).toBe("assistant") + }) + + it("should convert user role correctly", () => { + const result = convertToAnthropicRole("user" as any) + expect(result).toBe("user") + }) + + it("should return null for unknown roles", () => { + const result = convertToAnthropicRole("unknown" as any) + expect(result).toBeNull() }) }) diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index e4dc9eecc8..68d21e4d5b 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -1,9 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { MessageContent } from "../../shared/api" import { ConversationRole, Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime" -// Import StreamEvent type from bedrock.ts -import { StreamEvent } from "../providers/bedrock" +import { MessageContent } from "../../shared/api" /** * Convert Anthropic messages to Bedrock Converse format @@ -175,51 +173,3 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me } }) } - -/** - * Convert Bedrock Converse stream events to Anthropic message format - */ -export function convertToAnthropicMessage( - streamEvent: StreamEvent, - modelId: string, -): Partial { - // Handle metadata events - if (streamEvent.metadata?.usage) { - return { - id: "", // Bedrock doesn't provide message IDs - type: "message", - role: "assistant", - model: modelId, - usage: { - input_tokens: streamEvent.metadata.usage.inputTokens || 0, - output_tokens: streamEvent.metadata.usage.outputTokens || 0, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }, - } - } - - // Handle content blocks - const text = streamEvent.contentBlockStart?.start?.text || streamEvent.contentBlockDelta?.delta?.text - if (text !== undefined) { - return { - type: "message", - role: "assistant", - content: [{ type: "text", text: text, citations: null }], - model: modelId, - } - } - - // Handle message stop - if (streamEvent.messageStop) { - return { - type: "message", - role: "assistant", - stop_reason: streamEvent.messageStop.stopReason || null, - stop_sequence: null, - model: modelId, - } - } - - return {} -} diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index f421769054..134f9f2ed6 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -144,63 +144,3 @@ export function convertToOpenAiMessages( return openAiMessages } - -// Convert OpenAI response to Anthropic format -export function convertToAnthropicMessage( - completion: OpenAI.Chat.Completions.ChatCompletion, -): Anthropic.Messages.Message { - const openAiMessage = completion.choices[0].message - const anthropicMessage: Anthropic.Messages.Message = { - id: completion.id, - type: "message", - role: openAiMessage.role, // always "assistant" - content: [ - { - type: "text", - text: openAiMessage.content || "", - citations: null, - }, - ], - model: completion.model, - stop_reason: (() => { - switch (completion.choices[0].finish_reason) { - case "stop": - return "end_turn" - case "length": - return "max_tokens" - case "tool_calls": - return "tool_use" - case "content_filter": // Anthropic doesn't have an exact equivalent - default: - return null - } - })(), - stop_sequence: null, // which custom stop_sequence was generated, if any (not applicable if you don't use stop_sequence) - usage: { - input_tokens: completion.usage?.prompt_tokens || 0, - output_tokens: completion.usage?.completion_tokens || 0, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }, - } - - if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) { - anthropicMessage.content.push( - ...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => { - let parsedInput = {} - try { - parsedInput = JSON.parse(toolCall.function.arguments || "{}") - } catch (error) { - console.error("Failed to parse tool arguments:", error) - } - return { - type: "tool_use", - id: toolCall.id, - name: toolCall.function.name, - input: parsedInput, - } - }), - ) - } - return anthropicMessage -} diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index 85c2fc7ba5..73716cf912 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -155,50 +155,3 @@ export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModel return null } } - -export async function convertToAnthropicMessage( - vsCodeLmMessage: vscode.LanguageModelChatMessage, -): Promise { - const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role) - - if (anthropicRole !== "assistant") { - throw new Error("Roo Code : Only assistant messages are supported.") - } - - return { - id: crypto.randomUUID(), - type: "message", - model: "vscode-lm", - role: anthropicRole, - content: vsCodeLmMessage.content - .map((part): Anthropic.ContentBlock | null => { - if (part instanceof vscode.LanguageModelTextPart) { - return { - type: "text", - text: part.value, - citations: null, - } - } - - if (part instanceof vscode.LanguageModelToolCallPart) { - return { - type: "tool_use", - id: part.callId || crypto.randomUUID(), - name: part.name, - input: asObjectSafe(part.input), - } - } - - return null - }) - .filter((part): part is Anthropic.ContentBlock => part !== null), - stop_reason: null, - stop_sequence: null, - usage: { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - }, - } -} From fc4d50100e9ae36a6de657701cd4d4525526c9e2 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 22:19:05 -0800 Subject: [PATCH 09/11] Remove console.log --- src/api/providers/anthropic.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 0d4b67b067..2d1f07f833 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -131,7 +131,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { case "message_start": // Tells us cache reads/writes/input/output. const usage = chunk.message.usage - console.log("usage", usage) yield { type: "usage", From 4dd9972c4130d611432284e249ad82089833acd2 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Mon, 24 Feb 2025 22:21:44 -0800 Subject: [PATCH 10/11] Update webview-ui/src/components/settings/ApiOptions.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- webview-ui/src/components/settings/ApiOptions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 3761ddce50..0b6a118656 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1278,7 +1278,7 @@ const ApiOptions = ({ {anthropicThinkingBudget && ( <>
- Number of tokens Claude is allowed use for its internal reasoning process. + Number of tokens Claude is allowed to use for its internal reasoning process.
Date: Mon, 24 Feb 2025 22:22:07 -0800 Subject: [PATCH 11/11] Update webview-ui/src/components/settings/SettingsView.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- webview-ui/src/components/settings/SettingsView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 29384826ac..761e856521 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -135,7 +135,6 @@ const SettingsView = forwardRef(({ onDone }, }, []) const handleSubmit = () => { - console.log("handleSubmit", apiConfiguration) const apiValidationResult = validateApiConfiguration(apiConfiguration) const modelIdValidationResult = validateModelId(