Skip to content

Commit 9de6af5

Browse files
authored
feat: add src/api/transform/vscode-lm-format.test.ts (RooCodeInc#2600)
1 parent ab59bd9 commit 9de6af5

File tree

4 files changed

+219
-8
lines changed

4 files changed

+219
-8
lines changed

src/api/providers/openai-native.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { convertToOpenAiMessages } from "../transform/openai-format"
1313
import { calculateApiCostOpenAI } from "../../utils/cost"
1414
import { ApiStream } from "../transform/stream"
15-
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
15+
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
1616

1717
export class OpenAiNativeHandler implements ApiHandler {
1818
private options: ApiHandlerOptions

src/api/providers/openai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ApiHandler } from "../index"
66
import { convertToOpenAiMessages } from "../transform/openai-format"
77
import { ApiStream } from "../transform/stream"
88
import { convertToR1Format } from "../transform/r1-format"
9-
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
9+
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
1010

1111
export class OpenAiHandler implements ApiHandler {
1212
private options: ApiHandlerOptions
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// This file contains `declare module "vscode"` so we must import it.
2+
import "../providers/vscode-lm"
3+
import { describe, it } from "mocha"
4+
import "should"
5+
import * as vscode from "vscode"
6+
import { Anthropic } from "@anthropic-ai/sdk"
7+
import { asObjectSafe, convertToAnthropicRole, convertToVsCodeLmMessages, convertToAnthropicMessage } from "./vscode-lm-format"
8+
9+
describe("asObjectSafe", () => {
10+
it("should handle falsy values", () => {
11+
asObjectSafe(0).should.deepEqual({})
12+
asObjectSafe("").should.deepEqual({})
13+
asObjectSafe(null).should.deepEqual({})
14+
asObjectSafe(undefined).should.deepEqual({})
15+
})
16+
17+
it("should parse valid JSON strings", () => {
18+
asObjectSafe('{"key": "value"}').should.deepEqual({ key: "value" })
19+
})
20+
21+
it("should return an empty object for invalid JSON strings", () => {
22+
asObjectSafe("invalid json").should.deepEqual({})
23+
})
24+
25+
it("should convert objects to plain objects", () => {
26+
const input = { prop: "value" }
27+
asObjectSafe(input).should.deepEqual(input)
28+
asObjectSafe(input).should.not.equal(input) // Should be a new object
29+
})
30+
31+
it("should convert arrays to plain objects", () => {
32+
const input = ["hello world"]
33+
asObjectSafe(input).should.deepEqual({ 0: "hello world" })
34+
})
35+
})
36+
37+
describe("convertToAnthropicRole", () => {
38+
it("should convert VSCode roles to Anthropic roles", () => {
39+
// @ts-expect-error(Testing with an invalid role)
40+
const unknownRole = "unknown" as vscode.LanguageModelChatMessageRole
41+
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.Assistant) === "assistant").should.be.true()
42+
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.User) === "user").should.be.true()
43+
;(convertToAnthropicRole(unknownRole) === null).should.be.true()
44+
})
45+
})
46+
47+
describe("convertToVsCodeLmMessages", () => {
48+
it("should convert simple string messages", () => {
49+
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
50+
{ role: "user", content: "Hello" },
51+
{ role: "assistant", content: "Hi there" },
52+
]
53+
54+
const result = convertToVsCodeLmMessages(anthropicMessages)
55+
56+
result.should.have.length(2)
57+
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
58+
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
59+
const textPart0 = result[0].content[0] as vscode.LanguageModelTextPart
60+
textPart0.should.have.property("value", "Hello")
61+
62+
result[1].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
63+
result[1].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
64+
const textPart1 = result[1].content[0] as vscode.LanguageModelTextPart
65+
textPart1.should.have.property("value", "Hi there")
66+
})
67+
68+
it("should convert complex user messages with tool results", () => {
69+
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
70+
{
71+
role: "user",
72+
content: [
73+
{ type: "text", text: "User text" },
74+
{
75+
type: "tool_result",
76+
tool_use_id: "tool-123",
77+
content: [{ type: "text", text: "Tool result" }],
78+
},
79+
],
80+
},
81+
]
82+
83+
const result = convertToVsCodeLmMessages(anthropicMessages)
84+
85+
result.should.have.length(1)
86+
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
87+
result[0].content.should.have.length(2)
88+
89+
// Check that the first content part is a ToolResultPart
90+
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolResultPart)
91+
const toolResultPart = result[0].content[0] as vscode.LanguageModelToolResultPart
92+
toolResultPart.should.have.property("callId", "tool-123")
93+
94+
// Skip detailed testing of internal structure as it may vary
95+
// Just verify it's the right type with the right ID
96+
97+
// Check the second content part is a TextPart
98+
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
99+
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
100+
textPart.should.have.property("value", "User text")
101+
})
102+
103+
it("should convert complex assistant messages with tool calls", () => {
104+
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
105+
{
106+
role: "assistant",
107+
content: [
108+
{ type: "text", text: "Assistant text" },
109+
{
110+
type: "tool_use",
111+
id: "tool-123",
112+
name: "testTool",
113+
input: { param: "value" },
114+
},
115+
],
116+
},
117+
]
118+
119+
const result = convertToVsCodeLmMessages(anthropicMessages)
120+
121+
result.should.have.length(1)
122+
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
123+
result[0].content.should.have.length(2)
124+
125+
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolCallPart)
126+
const toolCallPart = result[0].content[0] as vscode.LanguageModelToolCallPart
127+
toolCallPart.should.have.property("callId", "tool-123")
128+
toolCallPart.should.have.property("name", "testTool")
129+
toolCallPart.should.have.property("input")
130+
toolCallPart.input.should.deepEqual({ param: "value" })
131+
132+
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
133+
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
134+
textPart.should.have.property("value", "Assistant text")
135+
})
136+
137+
it("should handle image blocks with appropriate placeholders", () => {
138+
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
139+
{
140+
role: "user",
141+
content: [
142+
{
143+
type: "image",
144+
source: {
145+
type: "base64",
146+
media_type: "image/jpeg",
147+
data: "base64data",
148+
},
149+
},
150+
],
151+
},
152+
]
153+
154+
const result = convertToVsCodeLmMessages(anthropicMessages)
155+
156+
result.should.have.length(1)
157+
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
158+
const textPart = result[0].content[0] as vscode.LanguageModelTextPart
159+
textPart.should.have.property("value")
160+
textPart.value.should.match(/Image \(base64\): image\/jpeg not supported by VSCode LM API/)
161+
})
162+
})
163+
164+
describe("convertToAnthropicMessage", () => {
165+
it("should convert VSCode assistant messages to Anthropic format", () => {
166+
const vsCodeMsg = vscode.LanguageModelChatMessage.Assistant([
167+
new vscode.LanguageModelTextPart("Test message"),
168+
new vscode.LanguageModelToolCallPart("tool-id", "testTool", { param: "value" }),
169+
])
170+
171+
const result = convertToAnthropicMessage(vsCodeMsg)
172+
173+
result.should.have.property("role", "assistant")
174+
result.should.have.property("content").which.is.an.Array()
175+
result.content.should.have.length(2)
176+
177+
// Check properties carefully to avoid null reference errors
178+
if (result.content && result.content.length >= 1) {
179+
const textContent = result.content[0]
180+
if (textContent) {
181+
textContent.should.have.property("type", "text")
182+
if (textContent.type === "text") {
183+
textContent.should.have.property("text", "Test message")
184+
}
185+
}
186+
}
187+
188+
if (result.content && result.content.length >= 2) {
189+
const toolContent = result.content[1]
190+
if (toolContent) {
191+
toolContent.should.have.property("type", "tool_use")
192+
if (toolContent.type === "tool_use") {
193+
toolContent.should.have.property("id", "tool-id")
194+
toolContent.should.have.property("name", "testTool")
195+
toolContent.should.have.property("input").which.deepEqual({ param: "value" })
196+
}
197+
}
198+
}
199+
})
200+
201+
it("should throw an error for non-assistant messages", () => {
202+
const vsCodeMsg = vscode.LanguageModelChatMessage.User("User message")
203+
204+
try {
205+
convertToAnthropicMessage(vsCodeMsg)
206+
throw new Error("Should have thrown an error")
207+
} catch (error: any) {
208+
error.message.should.match(/Only assistant messages are supported/)
209+
}
210+
})
211+
})

src/api/transform/vscode-lm-format.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as vscode from "vscode"
44
/**
55
* Safely converts a value into a plain object.
66
*/
7-
function asObjectSafe(value: any): object {
7+
export function asObjectSafe(value: any): object {
88
// Handle null/undefined
99
if (!value) {
1010
return {}
@@ -145,7 +145,9 @@ export function convertToVsCodeLmMessages(
145145
return vsCodeLmMessages
146146
}
147147

148-
export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole): string | null {
148+
export function convertToAnthropicRole(
149+
vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole,
150+
): Anthropic.Messages.MessageParam["role"] | null {
149151
switch (vsCodeLmMessageRole) {
150152
case vscode.LanguageModelChatMessageRole.Assistant:
151153
return "assistant"
@@ -156,10 +158,8 @@ export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModel
156158
}
157159
}
158160

159-
export async function convertToAnthropicMessage(
160-
vsCodeLmMessage: vscode.LanguageModelChatMessage,
161-
): Promise<Anthropic.Messages.Message> {
162-
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
161+
export function convertToAnthropicMessage(vsCodeLmMessage: vscode.LanguageModelChatMessage): Anthropic.Messages.Message {
162+
const anthropicRole = convertToAnthropicRole(vsCodeLmMessage.role)
163163
if (anthropicRole !== "assistant") {
164164
throw new Error("Cline <Language Model API>: Only assistant messages are supported.")
165165
}

0 commit comments

Comments
 (0)