Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit abc4928

Browse files
committed
fix(vscode-lm): simplify image support and exclude non-tool-call models
- Simplify IMAGE_CAPABLE_MODEL_PREFIXES to ['gpt', 'claude', 'gemini', 'o1', 'o3'] - Use startsWith prefix matching on model ID for cleaner logic - Add claude-opus-41 to VSCODE_LM_STATIC_BLACKLIST (no tool_call support) - Return text placeholder for URL images instead of empty Uint8Array - Update tests to match new prefix-matching semantics Source: https://models.dev/api.json (github-copilot provider models)
1 parent 095b399 commit abc4928

4 files changed

Lines changed: 194 additions & 39 deletions

File tree

src/api/providers/__tests__/vscode-lm.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Mock } from "vitest"
2+
import { checkModelSupportsImages, IMAGE_CAPABLE_MODEL_PREFIXES } from "../vscode-lm"
23

34
// Mocks must come first, before imports
45
vi.mock("vscode", () => {
@@ -537,3 +538,92 @@ describe("VsCodeLmHandler", () => {
537538
})
538539
})
539540
})
541+
542+
describe("checkModelSupportsImages", () => {
543+
describe("OpenAI GPT models", () => {
544+
it("should return true for all gpt-* models (GitHub Copilot)", () => {
545+
// All GPT models in GitHub Copilot support images
546+
expect(checkModelSupportsImages("gpt", "gpt-4o")).toBe(true)
547+
expect(checkModelSupportsImages("gpt", "gpt-4.1")).toBe(true)
548+
expect(checkModelSupportsImages("gpt", "gpt-5")).toBe(true)
549+
expect(checkModelSupportsImages("gpt", "gpt-5.1")).toBe(true)
550+
expect(checkModelSupportsImages("gpt", "gpt-5.2")).toBe(true)
551+
expect(checkModelSupportsImages("gpt-mini", "gpt-5-mini")).toBe(true)
552+
expect(checkModelSupportsImages("gpt-codex", "gpt-5.1-codex")).toBe(true)
553+
expect(checkModelSupportsImages("gpt-codex", "gpt-5.2-codex")).toBe(true)
554+
expect(checkModelSupportsImages("gpt-codex", "gpt-5.1-codex-max")).toBe(true)
555+
expect(checkModelSupportsImages("gpt-codex", "gpt-5.1-codex-mini")).toBe(true)
556+
})
557+
558+
it("should return true for o1 and o3 reasoning models", () => {
559+
expect(checkModelSupportsImages("o1", "o1-preview")).toBe(true)
560+
expect(checkModelSupportsImages("o1", "o1-mini")).toBe(true)
561+
expect(checkModelSupportsImages("o3", "o3")).toBe(true)
562+
})
563+
})
564+
565+
describe("Anthropic Claude models", () => {
566+
it("should return true for all claude-* models (GitHub Copilot)", () => {
567+
// All Claude models in GitHub Copilot support images
568+
expect(checkModelSupportsImages("claude-haiku", "claude-haiku-4.5")).toBe(true)
569+
expect(checkModelSupportsImages("claude-opus", "claude-opus-4.5")).toBe(true)
570+
expect(checkModelSupportsImages("claude-sonnet", "claude-sonnet-4")).toBe(true)
571+
expect(checkModelSupportsImages("claude-sonnet", "claude-sonnet-4.5")).toBe(true)
572+
})
573+
})
574+
575+
describe("Google Gemini models", () => {
576+
it("should return true for all gemini-* models (GitHub Copilot)", () => {
577+
// All Gemini models in GitHub Copilot support images
578+
expect(checkModelSupportsImages("gemini-pro", "gemini-2.5-pro")).toBe(true)
579+
expect(checkModelSupportsImages("gemini-flash", "gemini-3-flash-preview")).toBe(true)
580+
expect(checkModelSupportsImages("gemini-pro", "gemini-3-pro-preview")).toBe(true)
581+
})
582+
})
583+
584+
describe("non-vision models", () => {
585+
it("should return false for grok models (text-only in GitHub Copilot)", () => {
586+
// Grok is the only model family in GitHub Copilot that doesn't support images
587+
expect(checkModelSupportsImages("grok", "grok-code-fast-1")).toBe(false)
588+
})
589+
590+
it("should return false for models with non-matching prefixes", () => {
591+
// Models that don't start with gpt, claude, gemini, o1, or o3
592+
expect(checkModelSupportsImages("mistral", "mistral-large")).toBe(false)
593+
expect(checkModelSupportsImages("llama", "llama-3-70b")).toBe(false)
594+
expect(checkModelSupportsImages("unknown", "some-random-model")).toBe(false)
595+
})
596+
})
597+
598+
describe("case insensitivity", () => {
599+
it("should match regardless of case", () => {
600+
expect(checkModelSupportsImages("GPT", "GPT-4O")).toBe(true)
601+
expect(checkModelSupportsImages("CLAUDE", "CLAUDE-SONNET-4")).toBe(true)
602+
expect(checkModelSupportsImages("GEMINI", "GEMINI-2.5-PRO")).toBe(true)
603+
})
604+
})
605+
606+
describe("prefix matching", () => {
607+
it("should only match IDs that start with known prefixes", () => {
608+
// ID must START with the prefix, not just contain it
609+
expect(checkModelSupportsImages("custom", "gpt-4o")).toBe(true) // ID starts with gpt
610+
expect(checkModelSupportsImages("custom", "my-gpt-model")).toBe(false) // gpt not at start
611+
expect(checkModelSupportsImages("custom", "not-claude-model")).toBe(false) // claude not at start
612+
})
613+
})
614+
})
615+
616+
describe("IMAGE_CAPABLE_MODEL_PREFIXES", () => {
617+
it("should export the model prefixes array", () => {
618+
expect(Array.isArray(IMAGE_CAPABLE_MODEL_PREFIXES)).toBe(true)
619+
expect(IMAGE_CAPABLE_MODEL_PREFIXES.length).toBeGreaterThan(0)
620+
})
621+
622+
it("should include key model prefixes", () => {
623+
expect(IMAGE_CAPABLE_MODEL_PREFIXES).toContain("gpt")
624+
expect(IMAGE_CAPABLE_MODEL_PREFIXES).toContain("claude")
625+
expect(IMAGE_CAPABLE_MODEL_PREFIXES).toContain("gemini")
626+
expect(IMAGE_CAPABLE_MODEL_PREFIXES).toContain("o1")
627+
expect(IMAGE_CAPABLE_MODEL_PREFIXES).toContain("o3")
628+
})
629+
})

src/api/providers/vscode-lm.ts

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -591,46 +591,42 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
591591
}
592592

593593
/**
594-
* Model families known to support image inputs via VS Code Language Model API.
594+
* Model ID prefixes that support image inputs via VS Code Language Model API.
595595
* These models support the LanguageModelDataPart.image() API introduced in VS Code 1.106+.
596+
*
597+
* All GitHub Copilot models with these prefixes support images.
598+
* Only grok-* models don't support images (text only).
599+
*
600+
* Source: https://models.dev/api.json (github-copilot provider models)
596601
*/
597-
const IMAGE_CAPABLE_MODEL_FAMILIES = [
598-
// OpenAI models with vision capabilities
599-
"gpt-4o",
600-
"gpt-4o-mini",
601-
"gpt-4.1",
602-
"gpt-5",
603-
"gpt-5-mini",
604-
// Anthropic Claude models with vision
605-
"claude-3.5-sonnet",
606-
"claude-3-5-sonnet",
607-
"claude-sonnet-4",
608-
"claude-4-sonnet",
609-
// Google Gemini models with vision
610-
"gemini-2.0-flash",
611-
"gemini-2.5-pro",
612-
"gemini-pro-vision",
602+
export const IMAGE_CAPABLE_MODEL_PREFIXES = [
603+
"gpt", // All GPT models (gpt-4o, gpt-4.1, gpt-5, gpt-5.1, gpt-5.2, gpt-5-mini, gpt-5.1-codex, etc.)
604+
"claude", // All Claude models (claude-haiku-4.5, claude-opus-4.5, claude-sonnet-4, claude-sonnet-4.5)
605+
"gemini", // All Gemini models (gemini-2.5-pro, gemini-3-flash-preview, gemini-3-pro-preview)
606+
"o1", // OpenAI o1 reasoning models
607+
"o3", // OpenAI o3 reasoning models
613608
]
614609

615610
/**
616-
* Checks if a model supports image inputs based on its family or ID.
617-
* @param family The model family (e.g., "gpt-4o", "claude-3.5-sonnet")
611+
* Checks if a model supports image inputs based on its model ID.
612+
* Uses prefix matching against known image-capable model families.
613+
*
614+
* @param _family The model family (unused, kept for API compatibility)
618615
* @param id The model ID
619616
* @returns true if the model supports image inputs
620617
*/
621-
function checkModelSupportsImages(family: string, id: string): boolean {
622-
// Check if the family matches any known image-capable model
623-
const familyLower = family.toLowerCase()
618+
export function checkModelSupportsImages(_family: string, id: string): boolean {
624619
const idLower = id.toLowerCase()
625-
626-
return IMAGE_CAPABLE_MODEL_FAMILIES.some(
627-
(capableFamily) =>
628-
familyLower.includes(capableFamily.toLowerCase()) || idLower.includes(capableFamily.toLowerCase()),
629-
)
620+
return IMAGE_CAPABLE_MODEL_PREFIXES.some((prefix) => idLower.startsWith(prefix))
630621
}
631622

632-
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list e.g. because they will never work
633-
const VSCODE_LM_STATIC_BLACKLIST: string[] = ["claude-3.7-sonnet", "claude-3.7-sonnet-thought"]
623+
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list
624+
// e.g. because they don't support native tool calling or will never work
625+
const VSCODE_LM_STATIC_BLACKLIST: string[] = [
626+
"claude-3.7-sonnet",
627+
"claude-3.7-sonnet-thought",
628+
"claude-opus-41", // Does not support native tool calling
629+
]
634630

635631
export async function getVsCodeLmModels() {
636632
try {

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,68 @@ describe("convertToVsCodeLmMessages", () => {
256256
expect(imagePart.type).toBe("data")
257257
expect(imagePart.mimeType).toBe("image/jpeg")
258258
})
259+
260+
it("should return text placeholder for URL-based images", () => {
261+
const messages: Anthropic.Messages.MessageParam[] = [
262+
{
263+
role: "user",
264+
content: [
265+
{ type: "text", text: "Check this image:" },
266+
{
267+
type: "image",
268+
source: {
269+
type: "url",
270+
url: "https://example.com/image.png",
271+
} as any,
272+
},
273+
],
274+
},
275+
]
276+
277+
const result = convertToVsCodeLmMessages(messages)
278+
279+
expect(result).toHaveLength(1)
280+
expect(result[0].content).toHaveLength(2)
281+
282+
// First part should be text
283+
const textPart = result[0].content[0] as MockLanguageModelTextPart
284+
expect(textPart.type).toBe("text")
285+
expect(textPart.value).toBe("Check this image:")
286+
287+
// Second part should be a text placeholder (not an empty DataPart)
288+
const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart
289+
expect(imagePlaceholder.type).toBe("text")
290+
expect(imagePlaceholder.value).toContain("URL not supported")
291+
expect(imagePlaceholder.value).toContain("https://example.com/image.png")
292+
})
293+
294+
it("should return text placeholder for unknown image source types", () => {
295+
const messages: Anthropic.Messages.MessageParam[] = [
296+
{
297+
role: "user",
298+
content: [
299+
{
300+
type: "image",
301+
source: {
302+
type: "unknown",
303+
media_type: "image/png",
304+
data: "", // Required by type but ignored for unknown source types
305+
} as any,
306+
},
307+
],
308+
},
309+
]
310+
311+
const result = convertToVsCodeLmMessages(messages)
312+
313+
expect(result).toHaveLength(1)
314+
expect(result[0].content).toHaveLength(1)
315+
316+
// Should return a text placeholder for unknown source types
317+
const placeholder = result[0].content[0] as MockLanguageModelTextPart
318+
expect(placeholder.type).toBe("text")
319+
expect(placeholder.value).toContain("unsupported source type")
320+
})
259321
})
260322

261323
describe("convertToAnthropicRole", () => {

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ function asObjectSafe(value: any): object {
2929
}
3030

3131
/**
32-
* Converts an Anthropic image block to a VS Code LanguageModelDataPart.
32+
* Converts an Anthropic image block to a VS Code LanguageModelDataPart or TextPart.
3333
* Uses the new LanguageModelDataPart.image() API available in VS Code 1.106+.
3434
* @param imageBlock The Anthropic image block param
35-
* @returns A LanguageModelDataPart for the image
35+
* @returns A LanguageModelDataPart for the image, or TextPart if the image cannot be converted
3636
*/
37-
function convertImageToDataPart(imageBlock: Anthropic.ImageBlockParam): vscode.LanguageModelDataPart {
37+
function convertImageToDataPart(
38+
imageBlock: Anthropic.ImageBlockParam,
39+
): vscode.LanguageModelDataPart | vscode.LanguageModelTextPart {
3840
const source = imageBlock.source
3941
const mediaType = source.media_type || "image/png"
4042

@@ -47,18 +49,23 @@ function convertImageToDataPart(imageBlock: Anthropic.ImageBlockParam): vscode.L
4749
}
4850
return vscode.LanguageModelDataPart.image(bytes, mediaType)
4951
} else if (source.type === "url") {
50-
// For URL-based images, we create a placeholder since LanguageModelDataPart.image
51-
// expects binary data. The URL would need to be fetched first.
52-
// This is a limitation - URL images should be fetched and converted to base64 upstream.
52+
// URL-based images cannot be directly converted - return a text placeholder
53+
// explaining the limitation. URL images should be fetched and converted to base64 upstream.
5354
console.warn(
54-
"Roo Code <Language Model API>: URL-based images require fetching the image data first. Using placeholder.",
55+
"Roo Code <Language Model API>: URL-based images are not supported by the VS Code LM API. " +
56+
"Images must be provided as base64 data.",
57+
)
58+
return new vscode.LanguageModelTextPart(
59+
`[Image from URL not supported: ${(source as any).url || "unknown URL"}. ` +
60+
`VS Code LM API requires base64-encoded image data.]`,
5561
)
56-
return new vscode.LanguageModelDataPart(new Uint8Array(0), mediaType)
5762
}
5863

59-
// Fallback for unknown source types
64+
// Fallback for unknown source types - return a text placeholder
6065
console.warn(`Roo Code <Language Model API>: Unknown image source type: ${(source as any).type}`)
61-
return new vscode.LanguageModelDataPart(new Uint8Array(0), mediaType)
66+
return new vscode.LanguageModelTextPart(
67+
`[Image with unsupported source type "${(source as any).type}" cannot be displayed]`,
68+
)
6269
}
6370

6471
export function convertToVsCodeLmMessages(

0 commit comments

Comments
 (0)