This commit is contained in:
roomote-v0[bot] 2026-04-20 17:05:03 +00:00 committed by GitHub
commit 0c9dc88eff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 408 additions and 21 deletions

9
pnpm-lock.yaml generated
View file

@ -702,7 +702,7 @@ importers:
version: 20.17.57
'@types/vscode':
specifier: ^1.84.0
version: 1.100.0
version: 1.108.1
vitest:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
@ -1103,7 +1103,7 @@ importers:
version: 5.0.5
'@types/vscode':
specifier: ^1.84.0
version: 1.100.0
version: 1.108.1
'@vscode/test-electron':
specifier: ^2.5.2
version: 2.5.2
@ -4679,6 +4679,9 @@ packages:
'@types/vscode@1.103.0':
resolution: {integrity: sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==}
'@types/vscode@1.108.1':
resolution: {integrity: sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@ -14883,6 +14886,8 @@ snapshots:
'@types/vscode@1.103.0': {}
'@types/vscode@1.108.1': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.2.1

View file

@ -1,4 +1,5 @@
import type { Mock } from "vitest"
import { checkModelSupportsImages, IMAGE_CAPABLE_MODEL_PATTERNS, IMAGE_INCAPABLE_MODEL_PATTERNS } from "../vscode-lm"
// Mocks must come first, before imports
vi.mock("vscode", () => {
@ -537,3 +538,134 @@ describe("VsCodeLmHandler", () => {
})
})
})
describe("checkModelSupportsImages", () => {
describe("static vscodeLlmModels lookup", () => {
it("should return supportsImages from static definitions when model family matches", () => {
// Models in vscodeLlmModels should return their static supportsImages value
expect(checkModelSupportsImages("gpt-3.5-turbo", "gpt-3.5-turbo")).toBe(false)
expect(checkModelSupportsImages("gpt-4", "gpt-4")).toBe(false)
expect(checkModelSupportsImages("gpt-4o-mini", "gpt-4o-mini")).toBe(false)
expect(checkModelSupportsImages("gpt-4o", "gpt-4o")).toBe(true)
expect(checkModelSupportsImages("gpt-4.1", "gpt-4.1")).toBe(true)
expect(checkModelSupportsImages("gpt-5", "gpt-5")).toBe(true)
expect(checkModelSupportsImages("gpt-5-mini", "gpt-5-mini")).toBe(true)
expect(checkModelSupportsImages("o1", "o1")).toBe(false)
expect(checkModelSupportsImages("o3-mini", "o3-mini")).toBe(false)
expect(checkModelSupportsImages("o4-mini", "o4-mini")).toBe(false)
})
it("should return supportsImages from static definitions for claude models", () => {
expect(checkModelSupportsImages("claude-3.5-sonnet", "claude-3.5-sonnet")).toBe(true)
expect(checkModelSupportsImages("claude-4-sonnet", "claude-4-sonnet")).toBe(true)
})
it("should return supportsImages from static definitions for gemini models", () => {
expect(checkModelSupportsImages("gemini-2.0-flash-001", "gemini-2.0-flash-001")).toBe(true)
expect(checkModelSupportsImages("gemini-2.5-pro", "gemini-2.5-pro")).toBe(true)
})
})
describe("pattern matching for unknown models", () => {
it("should return true for gpt-4o (but not gpt-4o-mini)", () => {
expect(checkModelSupportsImages("custom", "gpt-4o")).toBe(true)
expect(checkModelSupportsImages("custom", "gpt-4o-mini")).toBe(false)
})
it("should return true for gpt-4.x and higher versions", () => {
expect(checkModelSupportsImages("custom", "gpt-4.1-preview")).toBe(true)
expect(checkModelSupportsImages("custom", "gpt-4.2")).toBe(true)
})
it("should return true for gpt-5 and higher (unknown variants)", () => {
expect(checkModelSupportsImages("custom", "gpt-5-turbo")).toBe(true)
expect(checkModelSupportsImages("custom", "gpt-6")).toBe(true)
})
it("should return true for all claude-* models", () => {
expect(checkModelSupportsImages("custom", "claude-haiku-4.5")).toBe(true)
expect(checkModelSupportsImages("custom", "claude-opus-4.5")).toBe(true)
expect(checkModelSupportsImages("custom", "claude-sonnet-4")).toBe(true)
})
it("should return true for all gemini-* models", () => {
expect(checkModelSupportsImages("custom", "gemini-2.5-pro")).toBe(true)
expect(checkModelSupportsImages("custom", "gemini-3-flash-preview")).toBe(true)
})
})
describe("non-vision models", () => {
it("should return false for gpt-3.5 models", () => {
expect(checkModelSupportsImages("custom", "gpt-3.5-turbo")).toBe(false)
expect(checkModelSupportsImages("custom", "gpt-3.5-turbo-16k")).toBe(false)
})
it("should return false for base gpt-4 and gpt-4-* variants", () => {
expect(checkModelSupportsImages("custom", "gpt-4")).toBe(false)
expect(checkModelSupportsImages("custom", "gpt-4-0125-preview")).toBe(false)
expect(checkModelSupportsImages("custom", "gpt-4-turbo")).toBe(false)
})
it("should return false for reasoning models (o1, o3-mini, o4-mini)", () => {
expect(checkModelSupportsImages("custom", "o1")).toBe(false)
expect(checkModelSupportsImages("custom", "o1-preview")).toBe(false)
expect(checkModelSupportsImages("custom", "o1-mini")).toBe(false)
expect(checkModelSupportsImages("custom", "o3-mini")).toBe(false)
expect(checkModelSupportsImages("custom", "o4-mini")).toBe(false)
})
it("should return false for grok models", () => {
expect(checkModelSupportsImages("custom", "grok-code-fast-1")).toBe(false)
expect(checkModelSupportsImages("custom", "grok-2")).toBe(false)
})
it("should return false for unknown model families", () => {
expect(checkModelSupportsImages("mistral", "mistral-large")).toBe(false)
expect(checkModelSupportsImages("llama", "llama-3-70b")).toBe(false)
expect(checkModelSupportsImages("unknown", "some-random-model")).toBe(false)
})
})
describe("case insensitivity", () => {
it("should match regardless of case for pattern matching", () => {
expect(checkModelSupportsImages("custom", "GPT-4O")).toBe(true)
expect(checkModelSupportsImages("custom", "CLAUDE-SONNET-4")).toBe(true)
expect(checkModelSupportsImages("custom", "GEMINI-2.5-PRO")).toBe(true)
})
})
describe("pattern matching edge cases", () => {
it("should only match IDs that start with known patterns", () => {
expect(checkModelSupportsImages("custom", "my-gpt-4o-model")).toBe(false) // gpt not at start
expect(checkModelSupportsImages("custom", "not-claude-model")).toBe(false) // claude not at start
})
})
})
describe("IMAGE_CAPABLE_MODEL_PATTERNS", () => {
it("should export the model patterns array", () => {
expect(Array.isArray(IMAGE_CAPABLE_MODEL_PATTERNS)).toBe(true)
expect(IMAGE_CAPABLE_MODEL_PATTERNS.length).toBeGreaterThan(0)
})
it("should contain RegExp patterns for vision-capable models", () => {
// All patterns should be RegExp instances
IMAGE_CAPABLE_MODEL_PATTERNS.forEach((pattern) => {
expect(pattern).toBeInstanceOf(RegExp)
})
})
})
describe("IMAGE_INCAPABLE_MODEL_PATTERNS", () => {
it("should export the incapable model patterns array", () => {
expect(Array.isArray(IMAGE_INCAPABLE_MODEL_PATTERNS)).toBe(true)
expect(IMAGE_INCAPABLE_MODEL_PATTERNS.length).toBeGreaterThan(0)
})
it("should contain RegExp patterns for non-vision models", () => {
// All patterns should be RegExp instances
IMAGE_INCAPABLE_MODEL_PATTERNS.forEach((pattern) => {
expect(pattern).toBeInstanceOf(RegExp)
})
})
})

View file

@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import OpenAI from "openai"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@roo-code/types"
import { type ModelInfo, openAiModelInfoSaneDefaults, vscodeLlmModels } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@ -529,6 +529,10 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
const modelId = this.client.id || modelParts.join(SELECTOR_SEPARATOR)
// Check if the model supports images based on known model families
// VS Code Language Model API 1.106+ supports image inputs via LanguageModelDataPart
const supportsImages = checkModelSupportsImages(this.client.family, this.client.id)
// Build model info with conservative defaults for missing values
const modelInfo: ModelInfo = {
maxTokens: -1, // Unlimited tokens by default
@ -536,7 +540,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
typeof this.client.maxInputTokens === "number"
? Math.max(0, this.client.maxInputTokens)
: openAiModelInfoSaneDefaults.contextWindow,
supportsImages: false, // VSCode Language Model API currently doesn't support image inputs
supportsImages,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
@ -586,8 +590,73 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
}
}
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list e.g. because they will never work
const VSCODE_LM_STATIC_BLACKLIST: string[] = ["claude-3.7-sonnet", "claude-3.7-sonnet-thought"]
/**
* Model ID patterns that support image inputs via VS Code Language Model API.
* These models support the LanguageModelDataPart.image() API introduced in VS Code 1.106+.
*
* For models not in the static vscodeLlmModels definitions, we use pattern matching
* to determine image support. Only newer model versions support images.
*
* Source: https://models.dev/api.json (github-copilot provider models)
*/
export const IMAGE_CAPABLE_MODEL_PATTERNS = [
/^gpt-4o$/i, // GPT-4o (omni) supports images, but NOT gpt-4o-mini
/^gpt-4\.[1-9]/i, // GPT-4.1 and higher versions
/^gpt-[5-9]/i, // GPT-5 and higher (gpt-5, gpt-5-mini, gpt-5.1-codex, etc.)
/^claude-/i, // All Claude models support images
/^gemini-/i, // All Gemini models support images
]
/**
* Model ID patterns that explicitly do NOT support images.
* These patterns are checked before IMAGE_CAPABLE_MODEL_PATTERNS.
*/
export const IMAGE_INCAPABLE_MODEL_PATTERNS = [
/^gpt-3\.5/i, // GPT-3.5 models don't support images
/^gpt-4$/i, // Base GPT-4 doesn't support images
/^gpt-4-/i, // GPT-4 variants like gpt-4-0125-preview don't support images
/^gpt-4o-mini/i, // GPT-4o-mini doesn't support images
/^o[1-4]-?/i, // Reasoning models (o1, o3-mini, o4-mini) don't support images
/^grok-/i, // Grok models don't support images
]
/**
* Checks if a model supports image inputs based on its model ID.
* First checks static vscodeLlmModels definitions for known models,
* then falls back to pattern matching for unknown models.
*
* @param family The model family (used for lookup in static definitions)
* @param id The model ID
* @returns true if the model supports image inputs
*/
export function checkModelSupportsImages(family: string, id: string): boolean {
// First, check if the model exists in static definitions by family or id
const familyInfo = vscodeLlmModels[family as keyof typeof vscodeLlmModels]
if (familyInfo) {
return familyInfo.supportsImages ?? false
}
const idInfo = vscodeLlmModels[id as keyof typeof vscodeLlmModels]
if (idInfo) {
return idInfo.supportsImages ?? false
}
// For unknown models, first check if it matches any incapable patterns
if (IMAGE_INCAPABLE_MODEL_PATTERNS.some((pattern) => pattern.test(id))) {
return false
}
// Then check if it matches any capable patterns
return IMAGE_CAPABLE_MODEL_PATTERNS.some((pattern) => pattern.test(id))
}
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list
// e.g. because they don't support native tool calling or will never work
const VSCODE_LM_STATIC_BLACKLIST: string[] = [
"claude-3.7-sonnet",
"claude-3.7-sonnet-thought",
"claude-opus-41", // Does not support native tool calling
]
export async function getVsCodeLmModels() {
try {

View file

@ -26,7 +26,13 @@ interface MockLanguageModelToolCallPart {
interface MockLanguageModelToolResultPart {
type: "tool_result"
callId: string
content: MockLanguageModelTextPart[]
content: (MockLanguageModelTextPart | MockLanguageModelDataPart)[]
}
interface MockLanguageModelDataPart {
type: "data"
data: Uint8Array
mimeType: string
}
// Mock vscode namespace
@ -54,10 +60,32 @@ vitest.mock("vscode", () => {
type = "tool_result"
constructor(
public callId: string,
public content: MockLanguageModelTextPart[],
public content: (MockLanguageModelTextPart | MockLanguageModelDataPart)[],
) {}
}
class MockLanguageModelDataPart {
type = "data"
constructor(
public data: Uint8Array,
public mimeType: string,
) {}
static image(data: Uint8Array, mime: string) {
return new MockLanguageModelDataPart(data, mime)
}
static json(value: any, mime?: string) {
const bytes = new TextEncoder().encode(JSON.stringify(value))
return new MockLanguageModelDataPart(bytes, mime || "application/json")
}
static text(value: string, mime?: string) {
const bytes = new TextEncoder().encode(value)
return new MockLanguageModelDataPart(bytes, mime || "text/plain")
}
}
return {
LanguageModelChatMessage: {
Assistant: vitest.fn((content) => ({
@ -75,6 +103,7 @@ vitest.mock("vscode", () => {
LanguageModelTextPart: MockLanguageModelTextPart,
LanguageModelToolCallPart: MockLanguageModelToolCallPart,
LanguageModelToolResultPart: MockLanguageModelToolResultPart,
LanguageModelDataPart: MockLanguageModelDataPart,
}
})
@ -150,7 +179,7 @@ describe("convertToVsCodeLmMessages", () => {
expect(toolCall.type).toBe("tool_call")
})
it("should handle image blocks with appropriate placeholders", () => {
it("should convert image blocks to LanguageModelDataPart", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
@ -161,7 +190,7 @@ describe("convertToVsCodeLmMessages", () => {
source: {
type: "base64",
media_type: "image/png",
data: "base64data",
data: "dGVzdA==", // "test" in base64
},
},
],
@ -171,8 +200,123 @@ describe("convertToVsCodeLmMessages", () => {
const result = convertToVsCodeLmMessages(messages)
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(2)
// First part should be text
const textPart = result[0].content[0] as MockLanguageModelTextPart
expect(textPart.type).toBe("text")
expect(textPart.value).toBe("Look at this:")
// Second part should be a LanguageModelDataPart for the image
const imagePart = result[0].content[1] as unknown as MockLanguageModelDataPart
expect(imagePart.type).toBe("data")
expect(imagePart.mimeType).toBe("image/png")
expect(imagePart.data).toBeInstanceOf(Uint8Array)
})
it("should handle images in tool results", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-1",
content: [
{ type: "text", text: "Screenshot result:" },
{
type: "image",
source: {
type: "base64",
media_type: "image/jpeg",
data: "dGVzdA==",
},
},
],
},
],
},
]
const result = convertToVsCodeLmMessages(messages)
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(1)
const toolResult = result[0].content[0] as MockLanguageModelToolResultPart
expect(toolResult.type).toBe("tool_result")
expect(toolResult.content).toHaveLength(2)
// First item in tool result should be text
const textPart = toolResult.content[0] as MockLanguageModelTextPart
expect(textPart.type).toBe("text")
// Second item should be image data
const imagePart = toolResult.content[1] as MockLanguageModelDataPart
expect(imagePart.type).toBe("data")
expect(imagePart.mimeType).toBe("image/jpeg")
})
it("should return text placeholder for URL-based images", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{ type: "text", text: "Check this image:" },
{
type: "image",
source: {
type: "url",
url: "https://example.com/image.png",
} as any,
},
],
},
]
const result = convertToVsCodeLmMessages(messages)
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(2)
// First part should be text
const textPart = result[0].content[0] as MockLanguageModelTextPart
expect(textPart.type).toBe("text")
expect(textPart.value).toBe("Check this image:")
// Second part should be a text placeholder (not an empty DataPart)
const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart
expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]")
expect(imagePlaceholder.type).toBe("text")
expect(imagePlaceholder.value).toContain("URL not supported")
expect(imagePlaceholder.value).toContain("https://example.com/image.png")
})
it("should return text placeholder for unknown image source types", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "image",
source: {
type: "unknown",
media_type: "image/png",
data: "", // Required by type but ignored for unknown source types
} as any,
},
],
},
]
const result = convertToVsCodeLmMessages(messages)
expect(result).toHaveLength(1)
expect(result[0].content).toHaveLength(1)
// Should return a text placeholder for unknown source types
const placeholder = result[0].content[0] as MockLanguageModelTextPart
expect(placeholder.type).toBe("text")
expect(placeholder.value).toContain("unsupported source type")
})
})

View file

@ -28,6 +28,46 @@ function asObjectSafe(value: any): object {
}
}
/**
* Converts an Anthropic image block to a VS Code LanguageModelDataPart or TextPart.
* Uses the new LanguageModelDataPart.image() API available in VS Code 1.106+.
* @param imageBlock The Anthropic image block param
* @returns A LanguageModelDataPart for the image, or TextPart if the image cannot be converted
*/
function convertImageToDataPart(
imageBlock: Anthropic.ImageBlockParam,
): vscode.LanguageModelDataPart | vscode.LanguageModelTextPart {
const source = imageBlock.source
const mediaType = source.media_type || "image/png"
if (source.type === "base64") {
// Convert base64 string to Uint8Array
const binaryString = atob(source.data)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return vscode.LanguageModelDataPart.image(bytes, mediaType)
} else if (source.type === "url") {
// URL-based images cannot be directly converted - return a text placeholder
// explaining the limitation. URL images should be fetched and converted to base64 upstream.
console.warn(
"Roo Code <Language Model API>: URL-based images are not supported by the VS Code LM API. " +
"Images must be provided as base64 data.",
)
return new vscode.LanguageModelTextPart(
`[Image from URL not supported: ${(source as any).url || "unknown URL"}. ` +
`VS Code LM API requires base64-encoded image data.]`,
)
}
// Fallback for unknown source types - return a text placeholder
console.warn(`Roo Code <Language Model API>: Unknown image source type: ${(source as any).type}`)
return new vscode.LanguageModelTextPart(
`[Image with unsupported source type "${(source as any).type}" cannot be displayed]`,
)
}
export function convertToVsCodeLmMessages(
anthropicMessages: Anthropic.Messages.MessageParam[],
): vscode.LanguageModelChatMessage[] {
@ -66,15 +106,13 @@ export function convertToVsCodeLmMessages(
const contentParts = [
// Convert tool messages to ToolResultParts
...toolMessages.map((toolMessage) => {
// Process tool result content into TextParts
const toolContentParts: vscode.LanguageModelTextPart[] =
// Process tool result content into TextParts or DataParts
const toolContentParts: (vscode.LanguageModelTextPart | vscode.LanguageModelDataPart)[] =
typeof toolMessage.content === "string"
? [new vscode.LanguageModelTextPart(toolMessage.content)]
: (toolMessage.content?.map((part) => {
if (part.type === "image") {
return new vscode.LanguageModelTextPart(
`[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`,
)
return convertImageToDataPart(part)
}
return new vscode.LanguageModelTextPart(part.text)
}) ?? [new vscode.LanguageModelTextPart("")])
@ -82,12 +120,10 @@ export function convertToVsCodeLmMessages(
return new vscode.LanguageModelToolResultPart(toolMessage.tool_use_id, toolContentParts)
}),
// Convert non-tool messages to TextParts after tool messages
// Convert non-tool messages to TextParts or DataParts after tool messages
...nonToolMessages.map((part) => {
if (part.type === "image") {
return new vscode.LanguageModelTextPart(
`[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`,
)
return convertImageToDataPart(part)
}
return new vscode.LanguageModelTextPart(part.text)
}),

View file

@ -300,7 +300,8 @@ function getSelectedModel({
: vscodeLlmDefaultModelId
const modelFamily = apiConfiguration?.vsCodeLmModelSelector?.family ?? vscodeLlmDefaultModelId
const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
// VS Code LM API 1.106+ supports images via LanguageModelDataPart - use model's supportsImages capability
return { id, info: { ...openAiModelInfoSaneDefaults, ...info } }
}
case "sambanova": {
const id = apiConfiguration.apiModelId ?? defaultModelId