Skip to content

Commit 11feb81

Browse files
committed
test(e2e): adding image test
1 parent 866b56f commit 11feb81

8 files changed

Lines changed: 202 additions & 13 deletions

File tree

apps/vscode-e2e/fixtures/openrouter.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
{
22
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "openrouter-image-e2e"
6+
},
7+
"response": {
8+
"toolCalls": [
9+
{
10+
"name": "attempt_completion",
11+
"arguments": "{\"result\":\"Red\"}",
12+
"id": "call_openrouter_image_001"
13+
}
14+
]
15+
}
16+
},
317
{
418
"match": {
519
"userMessage": "openrouter-identity-smoke"

apps/vscode-e2e/src/suite/providers/openrouter.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type CapturedOpenRouterRequest = {
99
xTitle: string | undefined
1010
httpReferer: string | undefined
1111
userAgent: string | undefined
12+
userMessageContent?: unknown
1213
}
1314

1415
function getRequestUrl(input: RequestInfo | URL): string {
@@ -40,10 +41,22 @@ function installOpenRouterRequestCapture(capture: CapturedOpenRouterRequest[], b
4041
if (new URL(url).origin === targetOrigin) {
4142
const xTitle = getHeaderValue(init, "X-Title") ?? getHeaderValue(init, "x-title")
4243
if (xTitle !== undefined) {
44+
let userMessageContent: unknown
45+
if (init?.body && typeof init.body === "string") {
46+
try {
47+
const body = JSON.parse(init.body)
48+
const messages: Array<{ role?: string; content?: unknown }> = body.messages ?? []
49+
const lastUser = [...messages].reverse().find((m) => m.role === "user")
50+
userMessageContent = lastUser?.content
51+
} catch {
52+
// ignore parse errors
53+
}
54+
}
4355
capture.push({
4456
xTitle,
4557
httpReferer: getHeaderValue(init, "HTTP-Referer") ?? getHeaderValue(init, "http-referer"),
4658
userAgent: getHeaderValue(init, "User-Agent") ?? getHeaderValue(init, "user-agent"),
59+
userMessageContent,
4760
})
4861
}
4962
}
@@ -82,7 +95,7 @@ suite("OpenRouter provider", function () {
8295
await globalThis.api.setConfiguration({
8396
apiProvider: "openrouter" as const,
8497
openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : OPENROUTER_API_KEY!,
85-
openRouterModelId: "openai/gpt-4.1",
98+
openRouterModelId: "anthropic/claude-haiku-4-5",
8699
...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }),
87100
})
88101
})
@@ -92,6 +105,41 @@ suite("OpenRouter provider", function () {
92105
restoreFetch = undefined
93106
})
94107

108+
test("Should forward base64 images as image_url content parts in outbound request", async () => {
109+
requests.length = 0
110+
111+
// 8x8 red PNG (1x1 is too small and rejected by Anthropic's vision API)
112+
const base64Png =
113+
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEklEQVR4nGP4z8CAFWEXHbQSACj/P8Fu7N9hAAAAAElFTkSuQmCC"
114+
const dataUri = `data:image/png;base64,${base64Png}`
115+
116+
const api = globalThis.api
117+
const taskId = await api.startNewTask({
118+
configuration: { mode: "ask", autoApprovalEnabled: true },
119+
text: "openrouter-image-e2e: describe the image in one word.",
120+
images: [dataUri],
121+
})
122+
123+
await waitUntilCompleted({ api, taskId })
124+
125+
// Find the first request that contains the probe tag
126+
const probeRequest = requests.find((r) => {
127+
const content = r.userMessageContent
128+
return Array.isArray(content) && JSON.stringify(content).includes("openrouter-image-e2e")
129+
})
130+
131+
assert.ok(probeRequest, "Should have captured an outbound request containing the probe tag")
132+
133+
const content = probeRequest.userMessageContent as Array<{ type: string; image_url?: { url: string } }>
134+
const imagePart = content.find((p) => p.type === "image_url")
135+
assert.ok(imagePart, "User message should contain an image_url content part")
136+
assert.strictEqual(
137+
imagePart?.image_url?.url,
138+
dataUri,
139+
"image_url.url should be the original data URI passed via startNewTask",
140+
)
141+
})
142+
95143
test("Should identify as Zoo Code in outbound DEFAULT_HEADERS", async () => {
96144
requests.length = 0
97145

src/api/providers/__tests__/native-ollama.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,46 @@ describe("NativeOllamaHandler", () => {
132132
)
133133
})
134134

135+
it("should drop unknown block types in tool_result content (empty string contribution)", async () => {
136+
mockChat.mockImplementation(async function* () {
137+
yield { message: { content: "ok" } }
138+
})
139+
140+
const messages: Anthropic.Messages.MessageParam[] = [
141+
{
142+
role: "user",
143+
content: [
144+
{
145+
type: "tool_result",
146+
tool_use_id: "tool-1",
147+
content: [
148+
{ type: "text", text: "before" },
149+
{ type: "document" } as any,
150+
{ type: "text", text: "after" },
151+
],
152+
},
153+
],
154+
},
155+
]
156+
157+
const stream = handler.createMessage("System", messages)
158+
for await (const _ of stream) {
159+
// consume
160+
}
161+
162+
// The unknown block contributes "" so the join produces "before\n\nafter"
163+
expect(mockChat).toHaveBeenCalledWith(
164+
expect.objectContaining({
165+
messages: expect.arrayContaining([
166+
expect.objectContaining({
167+
role: "user",
168+
content: "before\n\nafter",
169+
}),
170+
]),
171+
}),
172+
)
173+
})
174+
135175
it("should not include num_ctx by default", async () => {
136176
// Mock the chat response
137177
mockChat.mockImplementation(async function* () {

src/api/providers/__tests__/openai-codex.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,54 @@ describe("OpenAiCodexHandler.createMessage", () => {
9494
expect(userMsg?.content).toEqual([{ type: "input_text", text: "Look at this:" }])
9595
expect(JSON.stringify(capturedInput)).not.toContain("input_image")
9696
})
97+
98+
it("should emit input_image for base64 images in formatFullConversation", async () => {
99+
const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" })
100+
101+
vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
102+
vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
103+
104+
const capturedInput: any[] = []
105+
;(handler as any).client = {
106+
responses: {
107+
create: vitest.fn().mockImplementation(async (body: any) => {
108+
capturedInput.push(...(body.input ?? []))
109+
return {
110+
async *[Symbol.asyncIterator]() {
111+
yield {
112+
type: "response.completed",
113+
response: {
114+
id: "r1",
115+
status: "completed",
116+
output: [],
117+
usage: { input_tokens: 1, output_tokens: 1 },
118+
},
119+
}
120+
},
121+
}
122+
}),
123+
},
124+
}
125+
126+
const messages: Anthropic.Messages.MessageParam[] = [
127+
{
128+
role: "user",
129+
content: [
130+
{ type: "text", text: "Look at this:" },
131+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } },
132+
],
133+
},
134+
]
135+
136+
const stream = handler.createMessage("system", messages)
137+
for await (const _ of stream) {
138+
// consume
139+
}
140+
141+
const userMsg = capturedInput.find((item: any) => item.role === "user")
142+
expect(userMsg?.content).toContainEqual({
143+
type: "input_image",
144+
image_url: "data:image/png;base64,abc123",
145+
})
146+
})
97147
})

src/api/providers/__tests__/openai-native.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,5 +1894,51 @@ describe("GPT-5 streaming event coverage (additional)", () => {
18941894
expect(userMsg.content).toEqual([{ type: "input_text", text: "Look at this:" }])
18951895
expect(bodyStr).not.toContain("input_image")
18961896
})
1897+
1898+
it("should emit input_image for base64 images in formatFullConversation", async () => {
1899+
const mockFetch = vitest.fn().mockResolvedValue({
1900+
ok: true,
1901+
body: new ReadableStream({
1902+
start(controller) {
1903+
controller.enqueue(
1904+
new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":"ok"}\n\n'),
1905+
)
1906+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1907+
controller.close()
1908+
},
1909+
}),
1910+
})
1911+
global.fetch = mockFetch as any
1912+
1913+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1914+
1915+
const localHandler = new OpenAiNativeHandler({
1916+
apiModelId: "gpt-4.1",
1917+
openAiNativeApiKey: "test-api-key",
1918+
})
1919+
1920+
const b64ImageMessages: Anthropic.Messages.MessageParam[] = [
1921+
{
1922+
role: "user",
1923+
content: [
1924+
{ type: "text", text: "Look at this:" },
1925+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } },
1926+
],
1927+
},
1928+
]
1929+
1930+
const stream = localHandler.createMessage("You are a helpful assistant.", b64ImageMessages)
1931+
for await (const _ of stream) {
1932+
// consume
1933+
}
1934+
1935+
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
1936+
const parsedBody = JSON.parse(bodyStr)
1937+
const userMsg = parsedBody.input[0]
1938+
expect(userMsg.content).toContainEqual({
1939+
type: "input_image",
1940+
image_url: "data:image/png;base64,abc123",
1941+
})
1942+
})
18971943
})
18981944
})

src/api/transform/mistral-format.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,7 @@ export function convertToMistralMessages(anthropicMessages: Anthropic.Messages.M
120120
}
121121
return { type: "text", text: "[Image]" }
122122
}
123-
if (part.type === "text") {
124-
return { type: "text", text: part.text }
125-
}
126-
return { type: "text", text: "" }
123+
return { type: "text", text: part.text }
127124
}),
128125
})
129126
}

src/api/transform/openai-format.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,10 +444,7 @@ export function convertToOpenAiMessages(
444444
}
445445
return { type: "text", text: "[Image]" }
446446
}
447-
if (part.type === "text") {
448-
return { type: "text", text: part.text }
449-
}
450-
return { type: "text", text: "" }
447+
return { type: "text", text: part.text }
451448
}),
452449
})
453450
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,7 @@ export function convertToVsCodeLmMessages(
102102
`[Image (${part.source.type}): not supported by VSCode LM API]`,
103103
)
104104
}
105-
if (part.type === "text") {
106-
return new vscode.LanguageModelTextPart(part.text)
107-
}
108-
return new vscode.LanguageModelTextPart("")
105+
return new vscode.LanguageModelTextPart(part.text)
109106
}),
110107
]
111108

0 commit comments

Comments
 (0)