Skip to content

Commit f39d747

Browse files
author
CodeKing
committed
fix(vscode-lm): enable image support for VS Code LM models
1 parent 37dd364 commit f39d747

13 files changed

Lines changed: 617 additions & 65 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ADR-005: VS Code LM Image Capability Detection
2+
3+
## Status
4+
5+
Accepted (2026-06-27)
6+
7+
## Context
8+
9+
Zoo Code supports the VS Code Language Model API provider. VS Code LM models can expose custom endpoints and provider-specific metadata. Some of these models support image input in native VS Code Chat, but Zoo Code can treat them as text-only.
10+
11+
The current capability mapper in `src/api/providers/vscode-lm.ts` only recognizes:
12+
13+
- `model.capabilities.imageInput`
14+
- `model.capabilities.vision`
15+
- static family metadata in `vscodeLlmModels`
16+
17+
When none of those are present, it sets `supportsImages` to `false`. For image-capable custom endpoint models, this false value propagates into task request generation, where image blocks are removed before the VS Code LM request is built.
18+
19+
## Decision
20+
21+
1. Broaden VS Code LM capability detection to recognize additional image/vision metadata shapes exposed by VS Code LM providers.
22+
2. Keep explicit negative capability metadata authoritative when a model clearly reports image support is disabled.
23+
3. Use safe fallback behavior for unknown custom VS Code LM models so Zoo Code does not strip images before the VS Code LM API can accept or reject them.
24+
4. Preserve full VS Code LM model identity in the webview model picker by using `id` when available instead of only `${vendor}/${family}`.
25+
5. Add regression coverage for:
26+
- provider image capability mapping,
27+
- image blocks reaching `LanguageModelDataPart` in the final VS Code LM request,
28+
- selected model capability propagation in the webview.
29+
30+
## Consequences
31+
32+
### Positive
33+
34+
- Image-capable VS Code LM custom endpoint models are no longer downgraded to text-only inside Zoo Code.
35+
- Images survive the task request pipeline and can be converted to `LanguageModelDataPart`.
36+
- Model selection is less prone to collisions when multiple VS Code LM models share vendor/family.
37+
38+
### Negative
39+
40+
- Unknown VS Code LM custom models may show image UI even if the provider later rejects images. This is preferable to silently dropping user images before the API request because VS Code LM is the source of truth for request acceptance.
41+
42+
## Alternatives Considered
43+
44+
1. **Only support `capabilities.imageInput` and `capabilities.vision`**: rejected because it fails for custom endpoints that VS Code Chat can use with images.
45+
2. **Always disable images for unknown VS Code LM models**: rejected because it causes silent image removal and contradicts VS Code Chat behavior.
46+
3. **Bypass `maybeRemoveImageBlocks()` for VS Code LM only**: rejected because capability propagation is used by UI, tools, and prompts; fixing only the final request would leave inconsistent behavior.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Research: VS Code LM Image Support
2+
3+
## Status
4+
5+
Completed initial pipeline trace on 2026-06-27.
6+
7+
## Problem
8+
9+
VS Code LM models that accept images in native VS Code Chat can behave as text-only in Zoo Code. The reported example is `customendpoint/gpt-5.5`.
10+
11+
## Pipeline traced
12+
13+
1. **Model discovery**
14+
15+
- `src/api/providers/vscode-lm.ts` calls `vscode.lm.selectChatModels({})` in `getVsCodeLmModels()` and maps each model through `getVsCodeLmModelInfo()`.
16+
- `getVsCodeLmModelInfo()` currently only checks `model.capabilities.imageInput`, `model.capabilities.vision`, static family metadata, then defaults to `false`.
17+
18+
2. **Selected model state**
19+
20+
- `webview-ui/src/components/settings/providers/VSCodeLM.tsx` receives `vsCodeLmModels` from the extension and stores the selected model object, including `info`, through `vsCodeLmModelSelector`.
21+
- The settings UI currently keys VS Code LM models by `${vendor}/${family}`. That can collide for custom endpoints or multiple models with the same vendor/family but different `id` or `version`.
22+
- `webview-ui/src/components/ui/hooks/useSelectedModel.ts` reads `vsCodeLmModelSelector.info` and exposes model capabilities to chat UI.
23+
24+
3. **UI gating**
25+
26+
- `webview-ui/src/components/chat/ChatView.tsx` computes `shouldDisableImages = !model?.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE`.
27+
- `webview-ui/src/components/chat/ChatTextArea.tsx` disables paste/drop/select image interactions when `shouldDisableImages` is true.
28+
- `webview-ui/src/components/chat/ChatRow.tsx` also disables image attachment while editing when `!model?.supportsImages`.
29+
30+
4. **Image ingestion**
31+
32+
- `src/core/webview/webviewMessageHandler.ts` resolves incoming images through `resolveImageMentions()`.
33+
- `resolveImageMentions()` defaults `supportsImages` to `true`; the current webview handler does not pass the current provider capability. This means images are not stripped at this step because of VS Code LM capability detection.
34+
35+
5. **Task request generation**
36+
37+
- `src/core/task/Task.ts` merges API history and then calls `maybeRemoveImageBlocks(mergedForApi, this.api)` before `buildCleanConversationHistory()` and `this.api.createMessage()`.
38+
- `src/api/transform/image-cleaning.ts` converts image blocks into text placeholders when `apiHandler.getModel().info.supportsImages` is false.
39+
40+
6. **VS Code LM request transform**
41+
- `src/api/transform/vscode-lm-format.ts` converts Anthropic image blocks to `vscode.LanguageModelDataPart` when that constructor is available.
42+
- Existing tests in `src/api/transform/__tests__/vscode-lm-format.spec.ts` already prove image blocks become data parts when they survive to this transform.
43+
44+
## Root cause
45+
46+
The root cause is capability detection and preservation for VS Code LM custom models. `getVsCodeLmModelInfo()` is too narrow: it treats a VS Code LM model as image-capable only when `capabilities.imageInput` or `capabilities.vision` is present, or when static family metadata says images are supported. Custom endpoint models can be image-capable in VS Code Chat without matching these fields/static families, so Zoo Code records `supportsImages: false`.
47+
48+
Once `supportsImages` is false, `Task.attemptApiRequest()` calls `maybeRemoveImageBlocks()`, which replaces image blocks with `[Referenced image in conversation]`. Therefore `convertToVsCodeLmMessages()` never gets an image block and cannot create `LanguageModelDataPart` for the final `client.sendRequest()` call.
49+
50+
A secondary issue is model identity in the settings UI. `VSCodeLM.tsx` uses `${vendor}/${family}` as the picker key, ignoring `id` and `version`. This can preserve or select the wrong `info` when multiple VS Code LM models share vendor/family.
51+
52+
## Responsible files
53+
54+
- `src/api/providers/vscode-lm.ts`
55+
- `src/api/transform/image-cleaning.ts`
56+
- `src/core/task/Task.ts`
57+
- `src/api/transform/vscode-lm-format.ts`
58+
- `webview-ui/src/components/settings/providers/VSCodeLM.tsx`
59+
- `webview-ui/src/components/ui/hooks/useSelectedModel.ts`
60+
- `webview-ui/src/components/chat/ChatView.tsx`
61+
- `webview-ui/src/components/chat/ChatTextArea.tsx`
62+
- `webview-ui/src/components/chat/ChatRow.tsx`
63+
64+
## Planned verification
65+
66+
- Add provider tests for broader VS Code LM image-capability shapes and fallback behavior for custom endpoints.
67+
- Add a provider request test proving Anthropic image blocks reach `sendRequest()` as `LanguageModelDataPart`.
68+
- Add settings hook/UI tests for preserving `info.supportsImages` and distinct model identity.
69+
- Run targeted Vitest suites from the correct package directories.
70+
- Runtime verification with a real VS Code LM image-capable model requires an interactive VS Code extension host and an installed/authenticated model provider. If unavailable in this environment, document exact manual steps and do not claim completed runtime verification.

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ const vsCodeLmSchema = baseProviderSettingsSchema.extend({
276276
family: z.string().optional(),
277277
version: z.string().optional(),
278278
id: z.string().optional(),
279+
info: modelInfoSchema.optional(),
279280
})
280281
.optional(),
281282
})

packages/types/src/vscode-extension-host.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList,
1212
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
1313
import type { GitCommit } from "./git.js"
1414
import type { McpServer } from "./mcp.js"
15-
import type { ModelRecord, RouterModels } from "./model.js"
15+
import type { ModelInfo, ModelRecord, RouterModels } from "./model.js"
1616
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
1717
import type { SkillMetadata } from "./skills.js"
1818
import type { TelemetrySetting } from "./telemetry.js"
@@ -136,7 +136,7 @@ export interface ExtensionMessage {
136136
openAiModels?: string[]
137137
ollamaModels?: ModelRecord
138138
lmStudioModels?: ModelRecord
139-
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
139+
vsCodeLmModels?: LanguageModelChatSelector[]
140140
mcpServers?: McpServer[]
141141
commits?: GitCommit[]
142142
listApiConfig?: ProviderSettingsEntry[]
@@ -782,6 +782,7 @@ export interface LanguageModelChatSelector {
782782
family?: string
783783
version?: string
784784
id?: string
785+
info?: ModelInfo
785786
}
786787

787788
export interface ClineSayTool {

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ vi.mock("vscode", () => {
77
constructor(public value: string) {}
88
}
99

10+
class MockLanguageModelDataPart {
11+
type = "data"
12+
constructor(
13+
public data: Uint8Array,
14+
public mimeType: string,
15+
) {}
16+
}
17+
1018
class MockLanguageModelToolCallPart {
1119
type = "tool_call"
1220
constructor(
@@ -52,6 +60,7 @@ vi.mock("vscode", () => {
5260
})),
5361
},
5462
LanguageModelTextPart: MockLanguageModelTextPart,
63+
LanguageModelDataPart: MockLanguageModelDataPart,
5564
LanguageModelToolCallPart: MockLanguageModelToolCallPart,
5665
lm: {
5766
selectChatModels: vi.fn(),
@@ -383,6 +392,44 @@ describe("VsCodeLmHandler", () => {
383392
)
384393
})
385394

395+
it("should pass image blocks to VS Code LM as data parts", async () => {
396+
const systemPrompt = "You are a helpful assistant"
397+
const imageData = Buffer.from("image-data").toString("base64")
398+
const messages: Anthropic.Messages.MessageParam[] = [
399+
{
400+
role: "user" as const,
401+
content: [
402+
{ type: "text", text: "Describe this image" },
403+
{ type: "image", source: { type: "base64", media_type: "image/png", data: imageData } },
404+
],
405+
},
406+
]
407+
408+
mockLanguageModelChat.sendRequest.mockResolvedValueOnce({
409+
stream: (async function* () {
410+
yield new vscode.LanguageModelTextPart("Image description")
411+
return
412+
})(),
413+
text: (async function* () {
414+
yield "Image description"
415+
return
416+
})(),
417+
})
418+
419+
const stream = handler.createMessage(systemPrompt, messages)
420+
for await (const _chunk of stream) {
421+
// Drain stream so sendRequest is invoked.
422+
}
423+
424+
const requestMessages = mockLanguageModelChat.sendRequest.mock.calls[0][0]
425+
const userMessage = requestMessages[1]
426+
const imagePart = userMessage.content[1]
427+
428+
expect(imagePart.type).toBe("data")
429+
expect(imagePart.mimeType).toBe("image/png")
430+
expect(Buffer.from(imagePart.data).toString()).toBe("image-data")
431+
})
432+
386433
it("should handle errors", async () => {
387434
const systemPrompt = "You are a helpful assistant"
388435
const messages: Anthropic.Messages.MessageParam[] = [
@@ -413,6 +460,56 @@ describe("VsCodeLmHandler", () => {
413460
expect(model.info.contextWindow).toBe(4096)
414461
})
415462

463+
it("should mark VS Code LM models with imageInput capability as supporting images", async () => {
464+
const mockModel = { ...mockLanguageModelChat, capabilities: { imageInput: true } }
465+
;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel])
466+
handler["client"] = null
467+
await handler.initializeClient()
468+
469+
const model = handler.getModel()
470+
expect(model.info.supportsImages).toBe(true)
471+
})
472+
473+
it("should mark VS Code LM models with supportsImages capability as supporting images", async () => {
474+
const mockModel = { ...mockLanguageModelChat, capabilities: { supportsImages: true } }
475+
;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel])
476+
handler["client"] = null
477+
await handler.initializeClient()
478+
479+
const model = handler.getModel()
480+
expect(model.info.supportsImages).toBe(true)
481+
})
482+
483+
it("should mark VS Code LM models with image modality as supporting images", async () => {
484+
const mockModel = { ...mockLanguageModelChat, capabilities: { inputModalities: ["text", "image"] } }
485+
;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel])
486+
handler["client"] = null
487+
await handler.initializeClient()
488+
489+
const model = handler.getModel()
490+
expect(model.info.supportsImages).toBe(true)
491+
})
492+
493+
it("should preserve explicit false image capability", async () => {
494+
const mockModel = { ...mockLanguageModelChat, capabilities: { imageInput: false } }
495+
;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel])
496+
handler["client"] = null
497+
await handler.initializeClient()
498+
499+
const model = handler.getModel()
500+
expect(model.info.supportsImages).toBe(false)
501+
})
502+
503+
it("should allow images for unknown custom VS Code LM models by default", async () => {
504+
const mockModel = { ...mockLanguageModelChat, family: "unknown-custom-family" }
505+
;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel])
506+
handler["client"] = null
507+
await handler.initializeClient()
508+
509+
const model = handler.getModel()
510+
expect(model.info.supportsImages).toBe(true)
511+
})
512+
416513
it("should return fallback model info when no client exists", () => {
417514
// Clear the client first
418515
handler["client"] = null

0 commit comments

Comments
 (0)