feat(vscode): add the VS Code Language Model (vscode-lm) provider

Run Cline inference through the VS Code Language Model API (vscode.lm), enabling
models contributed by any extension that registers a language model chat
provider with VS Code. GitHub Copilot is the most common such vendor, but the
implementation is vendor-agnostic — it selects models via
vscode.lm.selectChatModels and has no Copilot-specific logic.

- VsCodeLmHandler implements the Cline SDK ApiHandler and is registered with the
  SDK handler registry; the model selector travels as a vendor/family[/version/id]
  string in modelId and is parsed back here. Selector segments are
  percent-encoded so values containing slashes round-trip intact.
- Native tool calling: tool definitions are passed to sendRequest and tool calls
  are surfaced as tool-call chunks; tool results round-trip as
  LanguageModelToolResultPart, with structured tool output serialized to text and
  a trailing user message appended when a turn ends on tool results so models can
  read the output.
- Gated to VS Code: registration is conditioned on the vscode.lm API being
  present, and the provider is hidden in the UI on hosts without it (JetBrains).

Depends on @cline/{shared,llms,agents,core} 0.0.42-nightly.1780514867, the first
published SDK build with the custom-registered-handler routing this provider
needs.
This commit is contained in:
Dominic Cooney
2026-06-24 14:11:27 +09:00
parent d2ac39455b
commit 6acc231da5
16 changed files with 2401 additions and 729 deletions
+1164 -666
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -493,10 +493,10 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@bufbuild/protobuf": "^2.2.5",
"@cline/agents": "^0.0.42",
"@cline/core": "^0.0.42",
"@cline/llms": "^0.0.42",
"@cline/shared": "^0.0.42",
"@cline/agents": "0.0.42-nightly.1780514867",
"@cline/core": "0.0.42-nightly.1780514867",
"@cline/llms": "0.0.42-nightly.1780514867",
"@cline/shared": "0.0.42-nightly.1780514867",
"@google/genai": "^1.30.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/proto-loader": "^0.7.13",
+6
View File
@@ -10,6 +10,7 @@ import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
import { StateManager } from "./core/storage/StateManager"
import { AgentConfigLoader } from "./core/task/tools/subagent/AgentConfigLoader"
import { ExtensionRegistryInfo } from "./registry"
import { registerVsCodeLmHandler } from "./sdk/vscode-lm/register-vscode-lm"
import { ErrorService } from "./services/error"
import { featureFlagsService } from "./services/feature-flags"
import { getDistinctId } from "./services/logging/distinctId"
@@ -51,6 +52,11 @@ export async function initialize(storageContext: StorageContext): Promise<Webvie
})
}
// Register host-only SDK provider handlers (e.g. VS Code Language Model API),
// which depend on the `vscode` module and cannot live in the SDK package.
// Must run before any handler is built (standalone utilities or task loop).
registerVsCodeLmHandler()
// =============== External services ===============
await ErrorService.initialize()
// Initialize PostHog client provider (skip in self-hosted mode)
@@ -17,6 +17,7 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, type LanguageDisplay } from
import { Logger } from "@shared/services/Logger"
import type { Settings } from "@shared/storage/state-keys"
import type { Mode } from "@shared/storage/types"
import { stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { StateManager } from "@/core/storage/StateManager"
import { ExtensionRegistryInfo } from "@/registry"
import { getDistinctId } from "@/services/logging/distinctId"
@@ -328,6 +329,16 @@ export function resolveApiKey(providerId: string, config: ApiConfiguration): str
* Uses mode-specific model ID fields when available, falls back to generic fields.
*/
export function resolveModelId(providerId: string, mode: Mode, config: ApiConfiguration): string | undefined {
// VS Code LM has no plain model-id field: the selected model is stored as a
// structured LanguageModelChatSelector ({vendor, family, ...}) in
// plan/actModeVsCodeLmModelSelector. The SDK ProviderConfig only carries a
// string modelId, so we stringify the selector to "vendor/family[/version/id]"
// and the VS Code LM handler parses it back. See sdk/vscode-lm/vscode-lm-handler.ts.
if (providerId === "vscode-lm") {
const selector = mode === "plan" ? config.planModeVsCodeLmModelSelector : config.actModeVsCodeLmModelSelector
return selector ? stringifyVsCodeLmModelSelector(selector) || undefined : undefined
}
// Check provider-specific mode model ID fields.
// If the provider has a dedicated field, do not fall back to generic
// *ModeApiModelId. Those generic slots may contain a stale model from a
+54
View File
@@ -0,0 +1,54 @@
# VS Code Language Model provider (`vscode-lm`)
Routes Cline inference through the [VS Code Language Model API](https://code.visualstudio.com/api/extension-guides/language-model)
(`vscode.lm`), letting Cline use models contributed by any extension that
registers a language model chat provider (a `vendor`) with VS Code. GitHub
Copilot is the most common such vendor, but the implementation is
vendor-agnostic — it selects models via `vscode.lm.selectChatModels` and has no
Copilot-specific logic. VS Code only — the `vscode.lm` API does not exist on
other hosts (e.g. JetBrains), so the provider is gated on the API being present.
## Files
- **`vscode-lm-handler.ts`** — `VsCodeLmHandler`, a Cline SDK `ApiHandler`
(`@cline/llms`) backed by `vscode.lm`. Selects a chat model, streams the
response, forwards tool definitions, and surfaces tool calls and usage.
- **`vscode-lm-format.ts`** — converts SDK `Message`s to
`vscode.LanguageModelChatMessage`s and back-converts tool results.
- **`register-vscode-lm.ts`** — registers the handler with the SDK handler
registry and exposes the `vscode.lm` availability check used for gating.
## How it plugs into the SDK
The SDK's `@cline/llms` handler registry (`registerHandler`) exists for providers
that need host-only dependencies — here, `vscode.lm` — which cannot live in the
host-agnostic SDK package. `registerVsCodeLmHandler()` (called during extension
activation) registers the factory for the `vscode-lm` provider id when the API is
available. The SDK then resolves `vscode-lm` to this handler for both the main
task loop and standalone utility calls.
## Model selection
`ProviderConfig` has no field for a VS Code LM selector, so the selected model
travels as a `vendor/family[/version/id]` string in `ProviderConfig.modelId` —
the model-id channel the rest of the SDK adapter uses. The webview stores the
structured `LanguageModelChatSelector` in
`plan/actModeVsCodeLmModelSelector`; the session factory stringifies it into
`modelId`, and `parseVsCodeLmSelector` reads it back into a selector here.
## Tool calling
Tool definitions are passed to `sendRequest` via `LanguageModelChatRequestOptions.tools`
(with `toolMode: Auto`). Tool invocations arrive as `LanguageModelToolCallPart`
and are forwarded as tool-call chunks the SDK runtime executes.
Tool results round-trip as `LanguageModelToolResultPart`. Two details the VS Code
LM API requires:
- **Result content is serialized to text.** SDK tool executors return rich,
untyped result objects (e.g. `ToolOperationResult` `{ query, result, success }`),
not typed text blocks; `extractToolOutputText` pulls their text out so the model
receives the actual output.
- **The prompt must not end on a tool result.** Copilot models require the final
message to be a non-tool-result user message; when a turn ends on tool results,
a short trailing user message is appended.
@@ -0,0 +1,38 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
const registerHandler = vi.fn()
vi.mock("@cline/llms", () => ({ registerHandler }))
vi.mock("@/shared/services/Logger", () => ({ Logger: { debug: vi.fn() } }))
// `vscode.lm` presence is the gate. Use a mutable mock so each test controls it.
const lm: { selectChatModels?: unknown } = {}
vi.mock("vscode", () => ({
get lm() {
return lm
},
}))
describe("registerVsCodeLmHandler gating", () => {
beforeEach(() => {
vi.resetModules()
registerHandler.mockClear()
delete lm.selectChatModels
})
it("does not register when the vscode.lm API is unavailable (e.g. JetBrains)", async () => {
const { registerVsCodeLmHandler, isVsCodeLmApiAvailable } = await import("./register-vscode-lm")
expect(isVsCodeLmApiAvailable()).toBe(false)
registerVsCodeLmHandler()
expect(registerHandler).not.toHaveBeenCalled()
})
it("registers once when the vscode.lm API is available", async () => {
lm.selectChatModels = vi.fn()
const { registerVsCodeLmHandler, isVsCodeLmApiAvailable, VSCODE_LM_PROVIDER_ID } = await import("./register-vscode-lm")
expect(isVsCodeLmApiAvailable()).toBe(true)
registerVsCodeLmHandler()
registerVsCodeLmHandler() // idempotent
expect(registerHandler).toHaveBeenCalledTimes(1)
expect(registerHandler).toHaveBeenCalledWith(VSCODE_LM_PROVIDER_ID, expect.any(Function))
})
})
@@ -0,0 +1,45 @@
// Registers the VS Code Language Model handler with the Cline SDK.
//
// The SDK's `@cline/llms` factory registry (registerHandler) exists for
// providers that need host-only dependencies — here, the `vscode` module /
// `vscode.lm` API — which cannot live in the SDK package. Once registered,
// `createHandler({ providerId: "vscode-lm", ... })` returns this handler, and
// the agent runtime resolves "vscode-lm" through the same registry, so both the
// main task loop and standalone utility calls use the VS Code LM API.
//
// The `vscode.lm` API only exists in VS Code and derivatives. On other hosts
// (e.g. JetBrains, which runs the shared activation path through a `vscode`
// shim) it is absent, so registration is gated on the API being present.
import { registerHandler } from "@cline/llms"
import * as vscode from "vscode"
import { Logger } from "@/shared/services/Logger"
import { VsCodeLmHandler } from "./vscode-lm-handler"
export const VSCODE_LM_PROVIDER_ID = "vscode-lm"
/**
* Whether the host exposes the VS Code Language Model API. False on hosts
* without `vscode.lm` (e.g. JetBrains / standalone), where the provider must not
* be registered or surfaced.
*/
export function isVsCodeLmApiAvailable(): boolean {
return typeof vscode.lm?.selectChatModels === "function"
}
// Registration is process-global (the SDK registry is a module singleton), so a
// module-local flag guards against redundant re-registration on reload.
let registered = false
/**
* Register the VS Code LM handler factory, once, when the `vscode.lm` API is
* available. No-ops on hosts without the API and on repeated activation.
*/
export function registerVsCodeLmHandler(): void {
if (registered || !isVsCodeLmApiAvailable()) {
return
}
registerHandler(VSCODE_LM_PROVIDER_ID, (config) => new VsCodeLmHandler(config))
registered = true
Logger.debug("[vscode-lm] Registered VS Code Language Model handler with the SDK")
}
@@ -0,0 +1,159 @@
import type { Message } from "@cline/llms"
import { describe, expect, it, vi } from "vitest"
// The shared vscode vitest stub does not define the Language Model API classes,
// so mock the small subset the transform uses. Each class records enough to
// assert structure in tests.
vi.mock("vscode", () => {
class LanguageModelTextPart {
constructor(public value: string) {}
}
class LanguageModelToolCallPart {
constructor(
public callId: string,
public name: string,
public input: object,
) {}
}
class LanguageModelToolResultPart {
constructor(
public callId: string,
public content: unknown[],
) {}
}
const LanguageModelChatMessage = {
User: (content: unknown) => ({ role: "user", content }),
Assistant: (content: unknown) => ({ role: "assistant", content }),
}
return { LanguageModelTextPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelChatMessage }
})
// Logger is referenced by asObjectSafe; stub it to avoid host dependencies.
vi.mock("@/shared/services/Logger", () => ({ Logger: { warn: vi.fn(), error: vi.fn(), debug: vi.fn() } }))
const { convertToVsCodeLmMessages, asObjectSafe } = await import("./vscode-lm-format")
const vscode = await import("vscode")
describe("convertToVsCodeLmMessages", () => {
it("converts simple string user/assistant messages", () => {
const messages: Message[] = [
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi there" },
]
const result = convertToVsCodeLmMessages(messages)
expect(result).toEqual([
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi there" },
])
})
it("converts a user message with text + tool_result, tool results first", () => {
const messages: Message[] = [
{
role: "user",
content: [
{ type: "text", text: "after tool" },
{ type: "tool_result", tool_use_id: "call-1", name: "read", content: "file contents" },
],
},
]
const [message] = convertToVsCodeLmMessages(messages)
const content = message.content as unknown[]
// Tool result part is emitted before the text part.
expect(content[0]).toBeInstanceOf(vscode.LanguageModelToolResultPart)
expect((content[0] as { callId: string }).callId).toBe("call-1")
expect(content[1]).toBeInstanceOf(vscode.LanguageModelTextPart)
expect((content[1] as { value: string }).value).toBe("after tool")
})
it("renders images as placeholders (VS Code LM does not accept images)", () => {
const messages: Message[] = [{ role: "user", content: [{ type: "image", data: "abc", mediaType: "image/png" }] }]
const [message] = convertToVsCodeLmMessages(messages)
const part = (message.content as { value: string }[])[0]
expect(part).toBeInstanceOf(vscode.LanguageModelTextPart)
expect(part.value).toContain("image/png")
expect(part.value).toContain("not supported")
})
it("extracts text from structured (untyped) tool_result content (run_commands shape)", () => {
// SDK tool executors return rich, untyped objects (ToolOperationResult:
// { query, result, success }) — NOT typed text blocks. The result text
// must reach the model, not an empty string.
const messages: Message[] = [
{ role: "assistant", content: [{ type: "tool_use", id: "c1", name: "run_commands", input: {} }] },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "c1",
name: "run_commands",
// biome-ignore lint/suspicious/noExplicitAny: simulating runtime untyped content
content: [{ query: "ls", result: "demo.ts\ndemo.txt\nsrc\n", success: true }] as any,
},
],
},
{ role: "user", content: "continue" },
]
const result = convertToVsCodeLmMessages(messages)
const toolResultMsg = result[1]
const trPart = (toolResultMsg.content as { content: { value: string }[] }[])[0]
expect(trPart).toBeInstanceOf(vscode.LanguageModelToolResultPart)
// The inner text part must contain the actual command output, not "".
expect(trPart.content[0].value).toBe("demo.ts\ndemo.txt\nsrc\n")
})
it("appends a trailing plain user message when the convo ends on tool results", () => {
const messages: Message[] = [
{ role: "assistant", content: [{ type: "tool_use", id: "c1", name: "read", input: {} }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "c1", name: "read", content: "file body" }] },
]
const result = convertToVsCodeLmMessages(messages)
// Last message must NOT be the tool-result one (Copilot requirement); it's
// a plain string user message (LanguageModelChatMessage.User(string)).
const last = result[result.length - 1]
expect(last.role).toBe("user")
expect(typeof last.content).toBe("string")
expect(last.content as unknown as string).toContain("result of calling one or more tools")
// The tool-result message is preserved before the nudge.
expect(result).toHaveLength(3)
expect((result[1].content as unknown[])[0]).toBeInstanceOf(vscode.LanguageModelToolResultPart)
})
it("does NOT append a trailing nudge when the convo ends on a normal user message", () => {
const messages: Message[] = [
{ role: "assistant", content: [{ type: "tool_use", id: "c1", name: "read", input: {} }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "c1", name: "read", content: "body" }] },
{ role: "user", content: "thanks, continue" },
]
const result = convertToVsCodeLmMessages(messages)
expect(result).toHaveLength(3)
expect(result[result.length - 1].content).toBe("thanks, continue")
})
it("converts an assistant tool_use into a tool call part with object input", () => {
const messages: Message[] = [
{ role: "assistant", content: [{ type: "tool_use", id: "call-9", name: "edit", input: { path: "a.ts" } }] },
]
const [message] = convertToVsCodeLmMessages(messages)
const part = (message.content as unknown[])[0]
expect(part).toBeInstanceOf(vscode.LanguageModelToolCallPart)
expect((part as { name: string }).name).toBe("edit")
expect((part as { input: object }).input).toEqual({ path: "a.ts" })
})
})
describe("asObjectSafe", () => {
it("parses JSON strings", () => {
expect(asObjectSafe('{"a":1}')).toEqual({ a: 1 })
})
it("clones objects", () => {
const input = { a: 1 }
expect(asObjectSafe(input)).toEqual({ a: 1 })
expect(asObjectSafe(input)).not.toBe(input)
})
it("returns {} for nullish or invalid", () => {
expect(asObjectSafe(null)).toEqual({})
expect(asObjectSafe("not json")).toEqual({})
})
})
@@ -0,0 +1,206 @@
// Converts Cline SDK messages (@cline/llms `Message`) into VS Code Language
// Model API messages (`vscode.LanguageModelChatMessage`).
//
// SDK content blocks (text / image / tool_use / tool_result) map onto VS Code LM
// parts. VS Code LM does not accept images, so image inputs become a text
// placeholder. Tool calls and tool results round-trip as
// `LanguageModelToolCallPart` / `LanguageModelToolResultPart`.
import type { ContentBlock, Message } from "@cline/llms";
import * as vscode from "vscode";
import { Logger } from "@/shared/services/Logger";
import { extractToolOutputText } from "../message-translator";
/**
* Safely coerce a tool-call input value into a plain object for
* `LanguageModelToolCallPart`, which requires an object input.
*/
export function asObjectSafe(value: unknown): object {
if (!value) {
return {};
}
try {
if (typeof value === "string") {
return JSON.parse(value);
}
if (typeof value === "object") {
return Object.assign({}, value);
}
return {};
} catch (error) {
Logger.warn("Cline <Language Model API>: Failed to parse object:", error);
return {};
}
}
function imagePlaceholder(mediaType?: string): vscode.LanguageModelTextPart {
return new vscode.LanguageModelTextPart(
`[Image (${mediaType || "unknown media-type"}) not supported by VSCode LM API]`,
);
}
/**
* Render the content of a tool_result block into VS Code LM text parts.
*/
function toolResultTextParts(
content: ContentBlock & { type: "tool_result" },
): vscode.LanguageModelTextPart[] {
if (typeof content.content === "string") {
return [new vscode.LanguageModelTextPart(content.content)];
}
// SDK tool executors (run_commands, search_codebase, read_files, MCP, ...)
// put rich, untyped result objects in `content` — e.g. an array of
// ToolOperationResult `{ query, result, success, error? }` — rather than typed
// text/image/file content blocks. Such items have no `.type`/`.text`, so they
// can only be read via extractToolOutputText (which pulls `.result`/`.error`
// out of ToolOperationResult[], else falls back to JSON.stringify). When no
// item looks like a typed content block, treat the whole array as untyped
// output and extract its text in one pass.
const isTypedBlock = (
part: unknown,
): part is { type: "text" | "image" | "file" } =>
typeof part === "object" &&
part !== null &&
typeof (part as { type?: unknown }).type === "string";
if (!content.content.some(isTypedBlock)) {
const text = extractToolOutputText(content.content);
return [new vscode.LanguageModelTextPart(text)];
}
const parts = content.content.map((part) => {
switch (part.type) {
case "text":
return new vscode.LanguageModelTextPart(part.text ?? "");
case "image":
return imagePlaceholder(part.mediaType);
case "file":
return new vscode.LanguageModelTextPart(part.content ?? "");
default:
// Untyped item interleaved with typed blocks — serialize just this item.
return new vscode.LanguageModelTextPart(extractToolOutputText(part));
}
});
return parts.length > 0 ? parts : [new vscode.LanguageModelTextPart("")];
}
export function convertToVsCodeLmMessages(
messages: Message[],
): vscode.LanguageModelChatMessage[] {
const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = [];
for (const message of messages) {
// Simple string content.
if (typeof message.content === "string") {
vsCodeLmMessages.push(
message.role === "assistant"
? vscode.LanguageModelChatMessage.Assistant(message.content)
: vscode.LanguageModelChatMessage.User(message.content),
);
continue;
}
if (message.role === "user") {
const toolResults = message.content.filter(
(part): part is ContentBlock & { type: "tool_result" } =>
part.type === "tool_result",
);
const nonTool = message.content.filter(
(part) => part.type === "text" || part.type === "image",
);
const contentParts = [
// Tool results first, as VS Code LM tool result parts.
...toolResults.map(
(toolResult) =>
new vscode.LanguageModelToolResultPart(
toolResult.tool_use_id,
toolResultTextParts(toolResult),
),
),
// Then text / image-placeholder parts.
...nonTool.map((part) =>
part.type === "image"
? imagePlaceholder(part.mediaType)
: new vscode.LanguageModelTextPart(
part.type === "text" ? part.text : "",
),
),
];
vsCodeLmMessages.push(vscode.LanguageModelChatMessage.User(contentParts));
continue;
}
if (message.role === "assistant") {
const toolUses = message.content.filter(
(part): part is ContentBlock & { type: "tool_use" } =>
part.type === "tool_use",
);
const nonTool = message.content.filter(
(part) => part.type === "text" || part.type === "image",
);
const contentParts = [
// Tool calls first.
...toolUses.map(
(toolUse) =>
new vscode.LanguageModelToolCallPart(
toolUse.id,
toolUse.name,
asObjectSafe(toolUse.input),
),
),
// Then text (images are not generated/produced for assistant turns).
...nonTool.map((part) =>
part.type === "image"
? new vscode.LanguageModelTextPart(
"[Image generation not supported by VSCode LM API]",
)
: new vscode.LanguageModelTextPart(
part.type === "text" ? part.text : "",
),
),
];
vsCodeLmMessages.push(
vscode.LanguageModelChatMessage.Assistant(contentParts),
);
}
}
// Copilot (and other VS Code LM) models require that the prompt does NOT end
// on a tool-result message — the final message must be a non-tool-result
// UserMessage. If we end on tool results, the model can't "see" the output and
// tends to re-run tools or call random ones. Mirror the official VS Code chat
// sample (chat-sample/src/toolsPrompt.tsx), which appends a trailing user
// nudge after tool results.
if (endsWithToolResult(vsCodeLmMessages)) {
vsCodeLmMessages.push(
vscode.LanguageModelChatMessage.User(
"Above is the result of calling one or more tools. The user cannot see the results, so you should explain them to the user if referencing them in your answer.",
),
);
}
return vsCodeLmMessages;
}
/**
* True when the last message is a user message whose content consists of (only)
* tool result parts.
*/
function endsWithToolResult(
messages: vscode.LanguageModelChatMessage[],
): boolean {
const last = messages[messages.length - 1];
if (!last || !Array.isArray(last.content) || last.content.length === 0) {
return false;
}
return last.content.some(
(part) => part instanceof vscode.LanguageModelToolResultPart,
);
}
@@ -0,0 +1,218 @@
import type { ProviderConfig, ToolDefinition } from "@cline/llms";
import { describe, expect, it, vi } from "vitest";
// Real-enough `vscode` mock: the handler uses `instanceof` on the stream parts
// and the LanguageModelChatToolMode enum, and constructs a CancellationTokenSource.
vi.mock("vscode", () => {
class LanguageModelTextPart {
constructor(public value: string) {}
}
class LanguageModelToolCallPart {
constructor(
public callId: string,
public name: string,
public input: object,
) {}
}
class LanguageModelToolResultPart {
constructor(
public callId: string,
public content: unknown[],
) {}
}
class CancellationTokenSource {
token = {};
cancel = vi.fn();
dispose = vi.fn();
}
class CancellationError extends Error {}
return {
LanguageModelTextPart,
LanguageModelToolCallPart,
LanguageModelToolResultPart,
CancellationTokenSource,
CancellationError,
LanguageModelChatToolMode: { Auto: 1, Required: 2 },
lm: { selectChatModels: vi.fn() },
LanguageModelChatMessage: {
User: (content: unknown) => ({ role: "user", content }),
Assistant: (content: unknown) => ({ role: "assistant", content }),
},
};
});
vi.mock("@/shared/services/Logger", () => ({
Logger: { warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));
const { parseVsCodeLmSelector, VsCodeLmHandler } = await import(
"./vscode-lm-handler"
);
const vscode = await import("vscode");
describe("parseVsCodeLmSelector", () => {
it("returns an empty selector for empty/undefined input", () => {
expect(parseVsCodeLmSelector(undefined)).toEqual({});
expect(parseVsCodeLmSelector("")).toEqual({});
});
it("parses vendor/family (the common case)", () => {
expect(parseVsCodeLmSelector("copilot/claude-sonnet")).toEqual({
vendor: "copilot",
family: "claude-sonnet",
});
});
it("parses vendor/family/version/id positionally", () => {
expect(parseVsCodeLmSelector("copilot/claude-sonnet/1.0/abc")).toEqual({
vendor: "copilot",
family: "claude-sonnet",
version: "1.0",
id: "abc",
});
});
it("parses encoded family values containing slashes", () => {
expect(parseVsCodeLmSelector("copilot/openai%2Fgpt-4o")).toEqual({
vendor: "copilot",
family: "openai/gpt-4o",
});
});
it("omits trailing missing segments", () => {
expect(parseVsCodeLmSelector("copilot")).toEqual({ vendor: "copilot" });
});
});
// Build a fake LanguageModelChat whose stream yields the given parts, and
// capture the requestOptions passed to sendRequest for assertions.
function fakeModel(parts: unknown[]) {
const calls: { options: Record<string, unknown> }[] = [];
const model = {
id: "copilot-claude",
name: "Claude",
vendor: "copilot",
family: "claude-sonnet",
version: "1",
maxInputTokens: 200_000,
sendRequest: vi.fn(
(_messages: unknown, options: Record<string, unknown>) => {
calls.push({ options });
return Promise.resolve({
stream: (async function* () {
for (const part of parts) {
yield part;
}
})(),
text: (async function* () {})(),
});
},
),
countTokens: vi.fn(async () => 0),
};
return { model, calls };
}
async function collect<T>(gen: AsyncGenerator<T>): Promise<T[]> {
const out: T[] = [];
for await (const v of gen) {
out.push(v);
}
return out;
}
const config = {
providerId: "vscode-lm",
modelId: "copilot/claude-sonnet",
} as ProviderConfig;
describe("VsCodeLmHandler.createMessage tool calling", () => {
it("passes tools + Auto toolMode to sendRequest when tools are provided", async () => {
const { model, calls } = fakeModel([
new vscode.LanguageModelTextPart("hello"),
]);
vi.mocked(vscode.lm.selectChatModels).mockResolvedValue([model] as never);
const handler = new VsCodeLmHandler(config);
const tools: ToolDefinition[] = [
{
name: "read_file",
description: "Reads a file",
inputSchema: {
type: "object",
properties: { path: { type: "string" } },
},
},
];
await collect(
handler.createMessage("sys", [{ role: "user", content: "hi" }], tools),
);
expect(calls).toHaveLength(1);
expect(calls[0].options.toolMode).toBe(
vscode.LanguageModelChatToolMode.Auto,
);
expect(calls[0].options.tools).toEqual([
{
name: "read_file",
description: "Reads a file",
inputSchema: {
type: "object",
properties: { path: { type: "string" } },
},
},
]);
});
it("does not set tools when none are provided", async () => {
const { model, calls } = fakeModel([
new vscode.LanguageModelTextPart("hi"),
]);
vi.mocked(vscode.lm.selectChatModels).mockResolvedValue([model] as never);
const handler = new VsCodeLmHandler(config);
await collect(
handler.createMessage("sys", [{ role: "user", content: "hi" }]),
);
expect(calls[0].options.tools).toBeUndefined();
expect(calls[0].options.toolMode).toBeUndefined();
});
it("emits a native tool_calls chunk for a LanguageModelToolCallPart", async () => {
const { model } = fakeModel([
new vscode.LanguageModelTextPart("thinking"),
new vscode.LanguageModelToolCallPart("call-1", "read_file", {
path: "a.ts",
}),
]);
vi.mocked(vscode.lm.selectChatModels).mockResolvedValue([model] as never);
const handler = new VsCodeLmHandler(config);
const chunks = await collect(
handler.createMessage(
"sys",
[{ role: "user", content: "hi" }],
[{ name: "read_file", description: "", inputSchema: {} }],
),
);
const toolChunk = chunks.find((c) => c.type === "tool_calls");
expect(toolChunk).toEqual({
type: "tool_calls",
id: expect.any(String),
tool_call: {
call_id: "call-1",
function: {
id: "call-1",
name: "read_file",
arguments: { path: "a.ts" },
},
},
});
// A text chunk and a final usage chunk are also present.
expect(chunks.some((c) => c.type === "text" && c.text === "thinking")).toBe(
true,
);
expect(chunks.at(-1)?.type).toBe("usage");
});
});
@@ -0,0 +1,295 @@
// Cline SDK `ApiHandler` (from `@cline/llms`) backed by the VS Code Language
// Model API (`vscode.lm`). Registered via `registerHandler("vscode-lm", ...)`
// (see ./register-vscode-lm.ts) so the SDK routes "vscode-lm" inference here.
//
// Model selection: the SDK `ProviderConfig` has no field for a VS Code LM
// selector, so the selector travels as a `vendor/family[/version/id]` string in
// `ProviderConfig.modelId` (the model-id channel the rest of the SDK adapter
// uses) and is parsed back into a `LanguageModelChatSelector` here. See
// parseVsCodeLmSelector / apps/vscode/src/shared/vsCodeSelectorUtils.ts.
import type {
ApiHandler,
ApiStreamChunk,
HandlerModelInfo,
Message,
ProviderConfig,
ToolDefinition,
} from "@cline/llms";
import { nanoid } from "nanoid";
import * as vscode from "vscode";
import { Logger } from "@/shared/services/Logger";
import {
parseVsCodeLmModelSelector,
SELECTOR_SEPARATOR,
} from "@/shared/vsCodeSelectorUtils";
import { convertToVsCodeLmMessages } from "./vscode-lm-format";
const FALLBACK_CONTEXT_WINDOW = 128_000;
/**
* Parse a stringified VS Code LM selector (`vendor/family[/version/id]`) back
* into a `LanguageModelChatSelector`. The model picker commonly supplies just
* `vendor` and `family`; any further segments map to version/id positionally,
* mirroring `stringifyVsCodeLmModelSelector`.
*/
export function parseVsCodeLmSelector(
modelId: string | undefined,
): vscode.LanguageModelChatSelector {
return parseVsCodeLmModelSelector(modelId);
}
function extractText(message: vscode.LanguageModelChatMessage): string {
if (typeof message.content === "string") {
return message.content;
}
if (Array.isArray(message.content)) {
return message.content
.filter(
(part): part is vscode.LanguageModelTextPart =>
part instanceof vscode.LanguageModelTextPart,
)
.map((part) => part.value)
.join("");
}
return "";
}
export class VsCodeLmHandler implements ApiHandler {
private readonly selector: vscode.LanguageModelChatSelector;
private client: vscode.LanguageModelChat | null = null;
private currentRequestCancellation: vscode.CancellationTokenSource | null =
null;
private abortSignal: AbortSignal | undefined;
constructor(config: ProviderConfig) {
this.selector = parseVsCodeLmSelector(config.modelId);
this.abortSignal = config.abortSignal;
}
setAbortSignal(signal: AbortSignal | undefined): void {
this.abortSignal = signal;
}
abort(): void {
this.ensureCleanState();
}
private ensureCleanState(): void {
if (this.currentRequestCancellation) {
this.currentRequestCancellation.cancel();
this.currentRequestCancellation.dispose();
this.currentRequestCancellation = null;
}
}
private async getClient(): Promise<vscode.LanguageModelChat> {
if (this.client) {
return this.client;
}
try {
const models = await vscode.lm.selectChatModels(this.selector);
if (models && models.length > 0) {
this.client = models[0];
return this.client;
}
throw new Error(
`No VS Code language model matched selector ${JSON.stringify(this.selector)}. Ensure GitHub Copilot (or another LM provider) is installed and the model is enabled.`,
);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(
`Cline <Language Model API>: Failed to select model: ${message}`,
);
}
}
getMessages(
systemPrompt: string,
messages: Message[],
): vscode.LanguageModelChatMessage[] {
return [
vscode.LanguageModelChatMessage.Assistant(systemPrompt),
...convertToVsCodeLmMessages(messages),
];
}
// VS Code LM does not expose a tokenizer cheaply, so approximate as chars/4.
// Good enough for budgeting and usage display.
private approxTokens(text: string): number {
return Math.ceil((text || "").length / 4);
}
async *createMessage(
systemPrompt: string,
messages: Message[],
tools?: ToolDefinition[],
): AsyncGenerator<ApiStreamChunk> {
this.ensureCleanState();
const client = await this.getClient();
const id = nanoid();
const vsCodeLmMessages = this.getMessages(systemPrompt, messages);
const inputTokens = vsCodeLmMessages.reduce(
(sum, msg) => sum + this.approxTokens(extractText(msg)),
0,
);
this.currentRequestCancellation = new vscode.CancellationTokenSource();
if (this.abortSignal) {
if (this.abortSignal.aborted) {
this.currentRequestCancellation.cancel();
} else {
this.abortSignal.addEventListener(
"abort",
() => this.currentRequestCancellation?.cancel(),
{ once: true },
);
}
}
let accumulatedText = "";
try {
const requestOptions: vscode.LanguageModelChatRequestOptions = {
justification: `Cline would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
};
// Native tool calling: the VS Code LM API (finalized in VS Code 1.95)
// accepts tool definitions on the request and streams back
// LanguageModelToolCallPart for invocations. Map the SDK's
// ToolDefinition[] onto vscode.LanguageModelChatTool[]; tool RESULTS are
// already round-tripped as LanguageModelToolResultPart by
// convertToVsCodeLmMessages. We use "Auto" tool mode so the model
// decides when to call tools (vs. forcing a call).
if (tools && tools.length > 0) {
requestOptions.tools = tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
requestOptions.toolMode = vscode.LanguageModelChatToolMode.Auto;
}
const response = await client.sendRequest(
vsCodeLmMessages,
requestOptions,
this.currentRequestCancellation.token,
);
for await (const chunk of response.stream) {
if (chunk instanceof vscode.LanguageModelTextPart) {
if (typeof chunk.value !== "string") {
continue;
}
accumulatedText += chunk.value;
const textChunk: ApiStreamChunk = {
type: "text",
text: chunk.value,
id,
};
yield textChunk;
} else if (chunk instanceof vscode.LanguageModelToolCallPart) {
// A tool invocation from the model. callId/name/input come straight
// from the API (input is already a parsed object); forward them as a
// tool-call chunk for the SDK runtime to execute.
if (!chunk.name || !chunk.callId || !chunk.input) {
continue;
}
const toolCallChunk: ApiStreamChunk = {
type: "tool_calls",
id,
tool_call: {
call_id: chunk.callId,
function: {
id: chunk.callId,
name: chunk.name,
arguments: chunk.input as Record<string, unknown>,
},
},
};
// Approximate output token accounting still wants the tool call's
// size; serialize for the estimate without affecting the chunk.
accumulatedText += JSON.stringify({
name: chunk.name,
input: chunk.input,
callId: chunk.callId,
});
yield toolCallChunk;
}
}
const usageChunk: ApiStreamChunk = {
type: "usage",
inputTokens,
outputTokens: this.approxTokens(accumulatedText),
totalCost: 0,
id,
};
yield usageChunk;
} catch (error) {
this.ensureCleanState();
if (error instanceof vscode.CancellationError) {
throw new Error(
"Cline <Language Model API>: Request cancelled by user",
);
}
if (error instanceof Error) {
Logger.error("Cline <Language Model API>: Stream error:", error);
throw error;
}
throw new Error(
`Cline <Language Model API>: Response stream error: ${String(error)}`,
);
} finally {
this.ensureCleanState();
}
}
getModel(): HandlerModelInfo {
if (this.client) {
const modelParts = [
this.client.vendor,
this.client.family,
this.client.version,
].filter(Boolean);
const modelId = this.client.id || modelParts.join(SELECTOR_SEPARATOR);
const contextWindow =
typeof this.client.maxInputTokens === "number" &&
this.client.maxInputTokens > 0
? this.client.maxInputTokens
: FALLBACK_CONTEXT_WINDOW;
return {
id: modelId,
info: {
id: modelId,
contextWindow,
maxInputTokens: contextWindow,
maxTokens: -1,
description: `VSCode Language Model: ${modelId}`,
},
};
}
// No client yet (e.g. getModel() called before createMessage()).
const fallbackId =
[
this.selector.vendor,
this.selector.family,
this.selector.version,
this.selector.id,
]
.filter(Boolean)
.join(SELECTOR_SEPARATOR) || "vscode-lm";
return {
id: fallbackId,
info: {
id: fallbackId,
contextWindow: FALLBACK_CONTEXT_WINDOW,
maxInputTokens: FALLBACK_CONTEXT_WINDOW,
maxTokens: -1,
description: `VSCode Language Model (Fallback): ${fallbackId}`,
},
};
}
}
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import {
parseVsCodeLmModelSelector,
stringifyVsCodeLmModelSelector,
} from "./vsCodeSelectorUtils";
describe("VS Code LM selector string encoding", () => {
it("round-trips vendor/family selectors", () => {
const selector = { vendor: "copilot", family: "claude-sonnet" };
const value = stringifyVsCodeLmModelSelector(selector);
expect(value).toBe("copilot/claude-sonnet");
expect(parseVsCodeLmModelSelector(value)).toEqual(selector);
});
it("preserves family values containing slashes", () => {
const selector = { vendor: "copilot", family: "openai/gpt-4o" };
const value = stringifyVsCodeLmModelSelector(selector);
expect(value).toBe("copilot/openai%2Fgpt-4o");
expect(parseVsCodeLmModelSelector(value)).toEqual(selector);
});
it("round-trips optional version and id segments", () => {
const selector = {
vendor: "copilot",
family: "anthropic/claude",
version: "2026/06",
id: "model/id",
};
const value = stringifyVsCodeLmModelSelector(selector);
expect(parseVsCodeLmModelSelector(value)).toEqual(selector);
});
it("keeps compatibility with old unencoded vendor/family values", () => {
expect(parseVsCodeLmModelSelector("copilot/claude-sonnet")).toEqual({
vendor: "copilot",
family: "claude-sonnet",
});
});
});
+58 -4
View File
@@ -1,7 +1,61 @@
import { LanguageModelChatSelector } from "vscode"
import type { LanguageModelChatSelector } from "vscode";
export const SELECTOR_SEPARATOR = "/"
export const SELECTOR_SEPARATOR = "/";
export function stringifyVsCodeLmModelSelector(selector: LanguageModelChatSelector): string {
return [selector.vendor, selector.family, selector.version, selector.id].filter(Boolean).join(SELECTOR_SEPARATOR)
function encodeSelectorSegment(value: string): string {
return encodeURIComponent(value);
}
function decodeSelectorSegment(value: string): string {
try {
return decodeURIComponent(value);
} catch {
// A segment that isn't valid percent-encoding is passed through as-is, so
// a malformed or hand-written selector string still yields a usable value.
return value;
}
}
export function stringifyVsCodeLmModelSelector(
selector: LanguageModelChatSelector,
): string {
return [selector.vendor, selector.family, selector.version, selector.id]
.filter((part): part is string => Boolean(part))
.map(encodeSelectorSegment)
.join(SELECTOR_SEPARATOR);
}
/**
* Parse a stringified VS Code LM selector (`vendor/family[/version/id]`) back
* into a `LanguageModelChatSelector`.
*
* Selector segments are URI-encoded by stringifyVsCodeLmModelSelector so model
* family/id values can themselves contain `/` (common for BYOK providers such as
* `openai/gpt-4o`). Splitting on the separator therefore yields up to four
* positional segments (vendor, family, version, id); each is decoded
* independently, and missing trailing segments are simply omitted.
*/
export function parseVsCodeLmModelSelector(
modelId: string | undefined,
): LanguageModelChatSelector {
if (!modelId) {
return {};
}
const parts = modelId.split(SELECTOR_SEPARATOR);
const [vendor, family, version, id] = parts.map(decodeSelectorSegment);
const selector: LanguageModelChatSelector = {};
if (vendor) {
selector.vendor = vendor;
}
if (family) {
selector.family = family;
}
if (version) {
selector.version = version;
}
if (id) {
selector.id = id;
}
return selector;
}
+24 -8
View File
@@ -1,5 +1,5 @@
import path from "node:path"
import { defineConfig } from "vitest/config"
import path from "node:path";
import { defineConfig } from "vitest/config";
// Vitest config for the VSCode extension's SDK-adapter and model-catalog
// unit tests. (The bulk of the extension's unit tests still run under mocha
@@ -8,6 +8,7 @@ export default defineConfig({
test: {
include: [
"src/sdk/**/*.test.ts",
"src/shared/vsCodeSelectorUtils.test.ts",
"src/core/storage/remote-config/**/*.test.ts",
"src/shared/model-catalog/provider-helpers.test.ts",
"src/core/controller/models/__tests__/providerCatalogHandlers.test.ts",
@@ -24,9 +25,18 @@ export default defineConfig({
},
resolve: {
alias: {
"@cline/core": path.resolve(__dirname, "src/test/cline-core-vitest-stub.ts"),
"@cline/llms": path.resolve(__dirname, "node_modules/@cline/llms/dist/index.js"),
"@cline/shared": path.resolve(__dirname, "node_modules/@cline/shared/dist/index.js"),
"@cline/core": path.resolve(
__dirname,
"src/test/cline-core-vitest-stub.ts",
),
"@cline/llms": path.resolve(
__dirname,
"node_modules/@cline/llms/dist/index.js",
),
"@cline/shared": path.resolve(
__dirname,
"node_modules/@cline/shared/dist/index.js",
),
vscode: path.resolve(__dirname, "src/test/vscode-vitest-stub.ts"),
"@": path.resolve(__dirname, "src"),
"@api": path.resolve(__dirname, "src/core/api"),
@@ -35,12 +45,18 @@ export default defineConfig({
"@hosts": path.resolve(__dirname, "src/hosts"),
"@integrations": path.resolve(__dirname, "src/integrations"),
"@services": path.resolve(__dirname, "src/services"),
"@shared/proto/cline/common": path.resolve(__dirname, "src/shared/proto/cline/common.ts"),
"@shared/proto/cline/models": path.resolve(__dirname, "src/shared/proto/cline/models.ts"),
"@shared/proto/cline/common": path.resolve(
__dirname,
"src/shared/proto/cline/common.ts",
),
"@shared/proto/cline/models": path.resolve(
__dirname,
"src/shared/proto/cline/models.ts",
),
"@shared/proto": path.resolve(__dirname, "src/shared/proto"),
"@shared": path.resolve(__dirname, "src/shared"),
"@utils": path.resolve(__dirname, "src/utils"),
"@packages": path.resolve(__dirname, "src/packages"),
},
},
})
});
@@ -1,48 +1,62 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { Mode } from "@shared/storage/types"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useState } from "react"
import { useInterval } from "react-use"
import * as vscodemodels from "vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { DROPDOWN_Z_INDEX, DropdownContainer } from "../ApiOptions"
import { getModeSpecificFields } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { EmptyRequest } from "@shared/proto/cline/common";
import type { Mode } from "@shared/storage/types";
import {
parseVsCodeLmModelSelector,
stringifyVsCodeLmModelSelector,
} from "@shared/vsCodeSelectorUtils";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { useCallback, useEffect, useState } from "react";
import { useInterval } from "react-use";
import type * as vscodemodels from "vscode";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ModelsServiceClient } from "@/services/grpc-client";
import { DROPDOWN_Z_INDEX, DropdownContainer } from "../ApiOptions";
import { getModeSpecificFields } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
interface VSCodeLmProviderProps {
currentMode: Mode
currentMode: Mode;
}
export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const [vsCodeLmModels, setVsCodeLmModels] = useState<
vscodemodels.LanguageModelChatSelector[]
>([]);
const { apiConfiguration } = useExtensionState();
const { handleModeFieldChange } = useApiConfigurationHandlers();
const { vsCodeLmModelSelector } = getModeSpecificFields(apiConfiguration, currentMode)
const { vsCodeLmModelSelector } = getModeSpecificFields(
apiConfiguration,
currentMode,
);
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getVsCodeLmModels(EmptyRequest.create({}))
if (response && response.models) {
setVsCodeLmModels(response.models)
const response = await ModelsServiceClient.getVsCodeLmModels(
EmptyRequest.create({}),
);
if (response?.models) {
setVsCodeLmModels(response.models);
}
} catch (error) {
console.error("Failed to fetch VS Code LM models:", error)
setVsCodeLmModels([])
console.error("Failed to fetch VS Code LM models:", error);
setVsCodeLmModels([]);
}
}, [])
}, []);
useEffect(() => {
requestVsCodeLmModels()
}, [requestVsCodeLmModels])
requestVsCodeLmModels();
}, [requestVsCodeLmModels]);
useInterval(requestVsCodeLmModels, 2000)
useInterval(requestVsCodeLmModels, 2000);
return (
<div>
<DropdownContainer className="dropdown-container" zIndex={DROPDOWN_Z_INDEX - 2}>
<DropdownContainer
className="dropdown-container"
zIndex={DROPDOWN_Z_INDEX - 2}
>
<label htmlFor="vscode-lm-model">
<span style={{ fontWeight: 500 }}>Language Model</span>
</label>
@@ -50,30 +64,36 @@ export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
<VSCodeDropdown
id="vscode-lm-model"
onChange={(e) => {
const value = (e.target as HTMLInputElement).value
const value = (e.target as HTMLInputElement).value;
if (!value) {
return
return;
}
const [vendor, family] = value.split("/")
handleModeFieldChange(
{ plan: "planModeVsCodeLmModelSelector", act: "actModeVsCodeLmModelSelector" },
{ vendor, family },
{
plan: "planModeVsCodeLmModelSelector",
act: "actModeVsCodeLmModelSelector",
},
parseVsCodeLmModelSelector(value),
currentMode,
)
);
}}
style={{ width: "100%" }}
value={
vsCodeLmModelSelector
? `${vsCodeLmModelSelector.vendor ?? ""}/${vsCodeLmModelSelector.family ?? ""}`
? stringifyVsCodeLmModelSelector(vsCodeLmModelSelector)
: ""
}>
}
>
<VSCodeOption value="">Select a model...</VSCodeOption>
{vsCodeLmModels.map((model) => (
<VSCodeOption key={`${model.vendor}/${model.family}`} value={`${model.vendor}/${model.family}`}>
{model.vendor} - {model.family}
</VSCodeOption>
))}
{vsCodeLmModels.map((model) => {
const value = stringifyVsCodeLmModelSelector(model);
return (
<VSCodeOption key={value} value={value}>
{model.vendor} - {model.family}
</VSCodeOption>
);
})}
</VSCodeDropdown>
) : (
<p
@@ -81,13 +101,19 @@ export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Use models from your GitHub Copilot subscription. Install the{" "}
<a href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot">Copilot extension</a> and
enable Claude models in Copilot settings to get started.
}}
>
Uses models contributed by other extensions through the VS Code
Language Model API. The most common source is GitHub Copilot —
install the{" "}
<a href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot">
Copilot extension
</a>{" "}
and enable models in Copilot settings — but any extension that
registers a language model provider will appear here.
</p>
)}
</DropdownContainer>
</div>
)
}
);
};
@@ -1,6 +1,7 @@
import type { ApiConfiguration, ApiProvider } from "@shared/api"
import PROVIDERS from "@shared/providers/providers.json"
import type { RemoteConfigFields } from "@shared/storage/state-keys"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
/**
* Returns a list of API providers that are configured (have required credentials/settings)
@@ -215,8 +216,11 @@ export function getConfiguredProviders(
configured.push("litellm")
}
// VSCode LM - always potentially available
configured.push("vscode-lm")
// VSCode LM - VS Code only (the vscode.lm API does not exist on other hosts
// such as JetBrains). Potentially available there whenever models are installed.
if (PLATFORM_CONFIG.type === PlatformType.VSCODE) {
configured.push("vscode-lm")
}
// Claude Code - requires path
if (apiConfiguration.claudeCodePath) {