diff --git a/.changeset/spicy-pugs-guess.md b/.changeset/spicy-pugs-guess.md new file mode 100644 index 0000000000..e93267e9d1 --- /dev/null +++ b/.changeset/spicy-pugs-guess.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Support Volcano Ark platform through OpenAI compatible diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 408f4e5cc3..203eb44040 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -10,6 +10,7 @@ import { import { ApiHandler, SingleCompletionHandler } from "../index" import { convertToOpenAiMessages } from "../transform/openai-format" import { convertToR1Format } from "../transform/r1-format" +import { convertToSimpleMessages } from "../transform/simple-format" import { ApiStream } from "../transform/stream" export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { @@ -46,21 +47,31 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const modelInfo = this.getModel().info + const modelUrl = this.options.openAiBaseUrl ?? "" const modelId = this.options.openAiModelId ?? "" const deepseekReasoner = modelId.includes("deepseek-reasoner") + const ark = modelUrl.includes(".volces.com") if (this.options.openAiStreamingEnabled ?? true) { const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { role: "system", content: systemPrompt, } + + let convertedMessages + if (deepseekReasoner) { + convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) + } else if (ark) { + convertedMessages = [systemMessage, ...convertToSimpleMessages(messages)] + } else { + convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + } + const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, temperature: 0, - messages: deepseekReasoner - ? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) - : [systemMessage, ...convertToOpenAiMessages(messages)], + messages: convertedMessages, stream: true as const, stream_options: { include_usage: true }, } diff --git a/src/api/transform/__tests__/simple-format.test.ts b/src/api/transform/__tests__/simple-format.test.ts new file mode 100644 index 0000000000..bf1de1927f --- /dev/null +++ b/src/api/transform/__tests__/simple-format.test.ts @@ -0,0 +1,138 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { convertToSimpleContent, convertToSimpleMessages } from "../simple-format" + +describe("simple-format", () => { + describe("convertToSimpleContent", () => { + it("returns string content as-is", () => { + const content = "Hello world" + expect(convertToSimpleContent(content)).toBe("Hello world") + }) + + it("extracts text from text blocks", () => { + const content = [ + { type: "text", text: "Hello" }, + { type: "text", text: "world" }, + ] as Anthropic.Messages.TextBlockParam[] + expect(convertToSimpleContent(content)).toBe("Hello\nworld") + }) + + it("converts image blocks to descriptive text", () => { + const content = [ + { type: "text", text: "Here's an image:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64data", + }, + }, + ] as Array + expect(convertToSimpleContent(content)).toBe("Here's an image:\n[Image: image/png]") + }) + + it("converts tool use blocks to descriptive text", () => { + const content = [ + { type: "text", text: "Using a tool:" }, + { + type: "tool_use", + id: "tool-1", + name: "read_file", + input: { path: "test.txt" }, + }, + ] as Array + expect(convertToSimpleContent(content)).toBe("Using a tool:\n[Tool Use: read_file]") + }) + + it("handles string tool result content", () => { + const content = [ + { type: "text", text: "Tool result:" }, + { + type: "tool_result", + tool_use_id: "tool-1", + content: "Result text", + }, + ] as Array + expect(convertToSimpleContent(content)).toBe("Tool result:\nResult text") + }) + + it("handles array tool result content with text and images", () => { + const content = [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { type: "text", text: "Result 1" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", + }, + }, + { type: "text", text: "Result 2" }, + ], + }, + ] as Anthropic.Messages.ToolResultBlockParam[] + expect(convertToSimpleContent(content)).toBe("Result 1\n[Image: image/jpeg]\nResult 2") + }) + + it("filters out empty strings", () => { + const content = [ + { type: "text", text: "Hello" }, + { type: "text", text: "" }, + { type: "text", text: "world" }, + ] as Anthropic.Messages.TextBlockParam[] + expect(convertToSimpleContent(content)).toBe("Hello\nworld") + }) + }) + + describe("convertToSimpleMessages", () => { + it("converts messages with string content", () => { + const messages = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] as Anthropic.Messages.MessageParam[] + expect(convertToSimpleMessages(messages)).toEqual([ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ]) + }) + + it("converts messages with complex content", () => { + const messages = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64data", + }, + }, + ], + }, + { + role: "assistant", + content: [ + { type: "text", text: "I see the image" }, + { + type: "tool_use", + id: "tool-1", + name: "analyze_image", + input: { data: "base64data" }, + }, + ], + }, + ] as Anthropic.Messages.MessageParam[] + expect(convertToSimpleMessages(messages)).toEqual([ + { role: "user", content: "Look at this:\n[Image: image/png]" }, + { role: "assistant", content: "I see the image\n[Tool Use: analyze_image]" }, + ]) + }) + }) +}) diff --git a/src/api/transform/simple-format.ts b/src/api/transform/simple-format.ts new file mode 100644 index 0000000000..c1e4895bba --- /dev/null +++ b/src/api/transform/simple-format.ts @@ -0,0 +1,67 @@ +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 { + if (typeof content === "string") { + return content + } + + // Extract text from content blocks + return content + .map((block) => { + if (block.type === "text") { + return block.text + } + if (block.type === "image") { + return `[Image: ${block.source.media_type}]` + } + if (block.type === "tool_use") { + return `[Tool Use: ${block.name}]` + } + if (block.type === "tool_result") { + if (typeof block.content === "string") { + return block.content + } + if (Array.isArray(block.content)) { + return block.content + .map((part) => { + if (part.type === "text") { + return part.text + } + if (part.type === "image") { + return `[Image: ${part.source.media_type}]` + } + return "" + }) + .join("\n") + } + return "" + } + return "" + }) + .filter(Boolean) + .join("\n") +} + +/** + * Convert Anthropic messages to simple format with string content + */ +export function convertToSimpleMessages( + messages: Anthropic.Messages.MessageParam[], +): Array<{ role: "user" | "assistant"; content: string }> { + return messages.map((message) => ({ + role: message.role, + content: convertToSimpleContent(message.content), + })) +}