Merge pull request #1167 from RooVetGit/cte/claude-3.7-thinking

Claude 3.7 thinking
This commit is contained in:
Chris Estreich 2025-02-24 22:27:59 -08:00 committed by GitHub
commit 6763824b5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1162 additions and 1055 deletions

9
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "3.7.2",
"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",

View file

@ -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",

View file

@ -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()
})
})

View file

@ -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,47 +30,57 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream<Anthropic.Beta.PromptCaching.Messages.RawPromptCachingBetaMessageStreamEvent>
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
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) {
// '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,
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: { type: "ephemeral" },
},
]
? [{ type: "text", text: message.content, cache_control: cacheControl }]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: { type: "ephemeral" } }
? { ...content, cache_control: cacheControl }
: content,
),
}
@ -114,8 +129,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,45 +139,53 @@ 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 "text":
// we may receive multiple text blocks, in which case just insert a line break between them
case "thinking":
// 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: "reasoning", text: "\n" }
}
yield {
type: "text",
text: chunk.content_block.text,
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.
if (chunk.index > 0) {
yield { type: "text", text: "\n" }
}
yield { type: "text", text: chunk.content_block.text }
break
}
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,
}
yield { type: "text", text: chunk.delta.text }
break
}
break
case "content_block_stop":
break
@ -171,10 +195,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] }
}
@ -189,14 +215,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
}
}

View file

@ -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

View file

@ -1,250 +1,167 @@
import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../bedrock-converse-format"
// npx jest src/api/transform/__tests__/bedrock-converse-format.test.ts
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: "<read_file>\n<path>\ntest.txt\n</path>\n</read_file>",
})
} 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,
},
})
})
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" }],
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" }],
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: "<read_file>\n<path>\ntest.txt\n</path>\n</read_file>",
})
})
} 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" })
})
})

View file

@ -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",
)
})
})

View file

@ -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()
})
})

View file

@ -1,275 +1,131 @@
import { convertToOpenAiMessages, convertToAnthropicMessage } from "../openai-format"
// npx jest src/api/transform/__tests__/openai-format.test.ts
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
type PartialChatCompletion = Omit<OpenAI.Chat.Completions.ChatCompletion, "choices"> & {
choices: Array<
Partial<OpenAI.Chat.Completions.ChatCompletion.Choice> & {
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!",
})
expect(anthropicMessage.stop_reason).toBe("end_turn")
expect(anthropicMessage.usage).toEqual({
input_tokens: 10,
output_tokens: 5,
})
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.",
})
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")
})
})

View file

@ -1,6 +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 = {
@ -27,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 = {
@ -84,173 +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",
})
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 <Language Model API>: 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()
})
})

View file

@ -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,49 +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<Anthropic.Messages.Message> {
// 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,
},
}
}
// 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 }],
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 {}
}

View file

@ -1,29 +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:
| string
| Array<
| Anthropic.Messages.TextBlockParam
| Anthropic.Messages.ImageBlockParam
| Anthropic.Messages.ToolUseBlockParam
| Anthropic.Messages.ToolResultBlockParam
>,
): 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":
@ -99,97 +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 })
}
// 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,
},
}
}

View file

@ -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({

View file

@ -144,60 +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 || "",
},
],
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,
},
}
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
}

View file

@ -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
}

View file

@ -155,46 +155,3 @@ export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModel
return null
}
}
export async function convertToAnthropicMessage(
vsCodeLmMessage: vscode.LanguageModelChatMessage,
): Promise<Anthropic.Messages.Message> {
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
if (anthropicRole !== "assistant") {
throw new Error("Roo Code <Language Model API>: 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,
}
}
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,
},
}
}

View file

@ -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<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
>
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
export type ClineOptions = {
provider: ClineProvider

View file

@ -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),
@ -2511,6 +2514,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@ -2593,6 +2597,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
@ -2692,6 +2697,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,

View file

@ -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

View file

@ -32,6 +32,7 @@ describe("checkExistKey", () => {
apiKey: "test-key",
apiProvider: undefined,
anthropicBaseUrl: undefined,
anthropicThinking: undefined,
}
expect(checkExistKey(config)).toBe(true)
})

View file

@ -21,6 +21,7 @@ export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
anthropicBaseUrl?: string
anthropicThinking?: number
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
glamaModelId?: string
glamaModelInfo?: ModelInfo

View file

@ -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,6 +45,7 @@ import { UnboundModelPicker } from "./UnboundModelPicker"
import { ModelInfoView } from "./ModelInfoView"
import { DROPDOWN_Z_INDEX } from "./styles"
import { RequestyModelPicker } from "./RequestyModelPicker"
import { TemperatureControl } from "./TemperatureControl"
interface ApiOptionsProps {
uriScheme: string | undefined
@ -65,6 +68,7 @@ const ApiOptions = ({
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
@ -185,6 +189,7 @@ const ApiOptions = ({
checked={anthropicBaseUrlSelected}
onChange={(checked: boolean) => {
setAnthropicBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("anthropicBaseUrl", "")
}
@ -384,6 +389,7 @@ const ApiOptions = ({
checked={openRouterBaseUrlSelected}
onChange={(checked: boolean) => {
setOpenRouterBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("openRouterBaseUrl", "")
}
@ -507,7 +513,7 @@ const ApiOptions = ({
</div>
)}
{apiConfiguration?.apiProvider === "vertex" && (
{selectedProvider === "vertex" && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.vertexProjectId || ""}
@ -621,6 +627,7 @@ const ApiOptions = ({
checked={azureApiVersionSelected}
onChange={(checked: boolean) => {
setAzureApiVersionSelected(checked)
if (!checked) {
setApiConfigurationField("azureApiVersion", "")
}
@ -1224,7 +1231,6 @@ const ApiOptions = ({
)}
{selectedProvider === "glama" && <GlamaModelPicker />}
{selectedProvider === "openrouter" && <OpenRouterModelPicker />}
{selectedProvider === "requesty" && <RequestyModelPicker />}
@ -1258,8 +1264,43 @@ const ApiOptions = ({
</>
)}
{selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219" && (
<div className="flex flex-col gap-2 mt-2">
<Checkbox
checked={!!anthropicThinkingBudget}
onChange={(checked) => {
const budget = checked ? 16_384 : undefined
setAnthropicThinkingBudget(budget)
setApiConfigurationField("anthropicThinking", budget)
}}>
Thinking?
</Checkbox>
{anthropicThinkingBudget && (
<>
<div className="text-muted-foreground text-sm">
Number of tokens Claude is allowed to use for its internal reasoning process.
</div>
<div className="flex items-center gap-2">
<Slider
min={1024}
max={anthropicModels["claude-3-7-sonnet-20250219"].maxTokens - 1}
step={1024}
value={[anthropicThinkingBudget]}
onValueChange={(value) => {
const budget = value[0]
setAnthropicThinkingBudget(budget)
setApiConfigurationField("anthropicThinking", budget)
}}
/>
<div className="w-10">{anthropicThinkingBudget}</div>
</div>
</>
)}
</div>
)}
{!fromWelcomeView && (
<div style={{ marginTop: "10px" }}>
<div className="mt-2">
<TemperatureControl
value={apiConfiguration?.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}

View file

@ -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<SettingsViewRef, SettingsViewProps>(({ onDone },
if (prevState.apiConfiguration?.[field] === value) {
return prevState
}
setChangeDetected(true)
return {
...prevState,
apiConfiguration: {
@ -132,6 +136,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(
apiConfiguration,
extensionState.glamaModels,
@ -140,6 +145,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
setApiErrorMessage(apiValidationResult)
setModelIdErrorMessage(modelIdValidationResult)
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
@ -162,18 +168,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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)
}
}

View file

@ -1,3 +1,4 @@
export * from "./alert-dialog"
export * from "./autosize-textarea"
export * from "./badge"
export * from "./button"