Ver Fonte

Support the Ark provider when used through OpenAI compatible

Roo Code há 10 meses atrás
pai
commit
2ddf54de5d

+ 5 - 0
.changeset/spicy-pugs-guess.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Support Volcano Ark platform through OpenAI compatible

+ 14 - 3
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 },
 			}

+ 138 - 0
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<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ImageBlockParam>
+			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<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ToolUseBlockParam>
+			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<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ToolResultBlockParam>
+			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]" },
+			])
+		})
+	})
+})

+ 67 - 0
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),
+	}))
+}