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