VSCode Copilot 如何接入本地模型:Language Model Chat Provider 机制解析
GitHub Copilot Chat 的模型选择器下拉菜单里,你通常只能看到 GitHub 官方提供的模型(GPT-4o、Claude Sonnet 等)。但如果你装了某些第三方扩展,下拉菜单里会突然多出 Ollama、LM Studio 甚至自建 API 后面的模型。这背后的机制是 VSCode 的 Language Model Chat Provider API——一个允许任何扩展向 Copilot Chat 注册自定义 LLM 的接口。
本文从这个 API 的设计哲学讲起,拆解它的消息流转、工具信号、Provider 发现机制,然后以 OAI Compatible Provider for Copilot(一个将 OpenAI 兼容 API 接入 Copilot 的扩展)为例,分析它的实现原理和关键设计选择。
从 API 设计看 Copilot 的扩展模型
VSCode 的 Language Model 体系分两层:
- 消费者层(Consumer):
vscode.lm.selectChatModels()和vscode.lm.sendChatRequest()。任何扩展都可以调用这些 API 来使用已注册的模型。这是正式 API,不需要特殊权限。 - 提供者层(Provider):
vscode.lm.registerLanguageModelChatProvider()。扩展通过实现LanguageModelChatProvider接口来注册自己的模型。这是 Proposed API(chatProvider),目前只对 Copilot Individual 订阅用户开放。
这个设计让 Copilot Chat 变成了一个模型无关的聊天框架——它本身不关心模型从哪来,只要有 Provider 注册了模型,用户就能在模型选择器中看到并使用。
Provider 的三个核心方法
一个 LanguageModelChatProvider 需要实现三个方法:
provideLanguageModelChatInformation
provideLanguageModelChatInformation(
options: { silent: boolean },
token: CancellationToken
): Promise<LanguageModelChatInformation[]>返回可用模型列表。VSCode 在几个时机调用它:
- 用户点开模型选择器时(
silent: false) - 周期性刷新时(
silent: true)
silent 参数是个重要的设计选择——当 silent: true 时,Provider 应该返回缓存的列表,避免频繁的网络请求。只有用户主动操作时才需要实时刷新。
每个返回的 LanguageModelChatInformation 对象描述一个模型:
{
id: "gpt-4o", // 唯一标识
name: "GPT-4o", // 展示名称
family: "openai", // 模型家族(归类用)
version: "1.0.0", // 版本
maxInputTokens: 128000, // 输入 token 上限
maxOutputTokens: 16384, // 输出 token 上限
capabilities: {
toolCalling: true, // 是否支持工具调用
imageInput: false, // 是否支持图片输入
}
}provideLanguageModelChatResponse
provideLanguageModelChatResponse(
model: LanguageModelChatInformation,
messages: readonly LanguageModelChatRequestMessage[],
options: ProvideLanguageModelChatResponseOptions,
progress: Progress<LanguageModelResponsePart2>,
token: CancellationToken
): Promise<void>这是核心方法——处理一次聊天请求。参数解析:
- model:用户选择的模型
- messages:完整的消息历史(包含 Copilot 注入的系统消息)
- options:包含
tools(可用工具列表)等上下文信息 - progress:流式输出通道,调用
progress.report(new LanguageModelTextPart("..."))向聊天窗口推送文本 - token:取消令牌,用户停止生成时会触发
Provider 需要做的事情:把 VSCode 的消息格式转换成目标 API 的格式,发起请求,然后把流式响应逐段 report 回去。
provideTokenCount
provideTokenCount(
model: LanguageModelChatInformation,
text: string | LanguageModelChatRequestMessage,
token: CancellationToken
): Promise<number>估算文本的 token 数量。Copilot 用这个来决定上下文裁剪策略。大多数 Provider 的实现是简单的字符数除以 4:
return Math.ceil(text.length / 4);精确计算需要跑 tokenizer,但对于 Provider 来说,近似值通常够用。
Copilot 发送了什么
理解 provideLanguageModelChatResponse 收到的参数对实现 Provider 至关重要。
消息结构
Copilot 发送的 messages 不是用户输入的原文——它会在用户消息外面套一层包装:
<context>
...编辑器上下文、打开的文件、选中的代码...
</context>
<userRequest>
用户实际输入的问题
</userRequest>系统消息和用户消息的具体包装格式取决于 Copilot 的模式(Agent / Ask / Edit),而且随版本更新可能变化。这意味着 Provider 如果需要提取用户的原始输入,就需要一个解析层来剥离这些包装标签。
options.tools 信号
options.tools 是一个工具数组,包含当前模式下可用的工具列表。这个字段虽然 Provider 通常不需要真正使用(工具调用由 Copilot 自己处理),但它承载了一个重要的隐含信号:
- Agent 模式:
options.tools包含大量写操作工具——create_file、replace_string_in_file、run_in_terminal等 - Ask 模式:
options.tools只包含读操作工具——read_file、list_directory、grep_search等 - Edit 模式:工具列表有所不同
也就是说,通过检查 options.tools 中是否包含写操作工具,Provider 可以推断出当前 Copilot 处于什么模式。这对于需要将模式信息传递给后端的 Provider 来说非常有用。
没有 Conversation ID
VSCode 的 Language Model Chat Provider API 不提供会话 ID。每次调用 provideLanguageModelChatResponse 时,Provider 收到的是完整的消息历史,但没有一个标识「这属于哪个对话」的 ID。
这意味着如果 Provider 的后端需要会话管理(比如 Cursor CLI 的 --resume 机制),就必须自己实现会话匹配——通过比对消息序列来判断「这是之前那个对话的延续」还是「一个全新的对话」。
OAI Compatible Provider 的实现原理
OAI Compatible Provider for Copilot 是社区中最典型的 Language Model Chat Provider 实现。它的核心思路是:把 VSCode 的消息格式转换成 OpenAI Chat Completion API 格式,发给任何兼容 OpenAI 协议的后端(Ollama、LM Studio、vLLM、one-api 等),然后把响应流转回 Copilot Chat。
架构
Copilot Chat UI
│
▼
OAI Compatible Provider Extension
│ 1. 从 VSCode 消息格式转换为 OpenAI 消息格式
│ 2. 构建 POST /v1/chat/completions 请求
│ 3. 处理 SSE 流式响应
│
▼
OpenAI-Compatible API Server
(Ollama / LM Studio / vLLM / one-api / 自建代理)
│
▼
实际模型
(Llama / Qwen / DeepSeek / 任何本地模型)消息转换
OpenAI 的消息格式很简单:
{
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "..." }
]
}而 VSCode 的消息使用 LanguageModelChatRequestMessage 对象,角色是枚举值(User = 1, Assistant = 2),内容是 LanguageModelTextPart 数组。Provider 需要做一层映射:
function convertRole(role: LanguageModelChatMessageRole): string {
switch (role) {
case LanguageModelChatMessageRole.User: return "user";
case LanguageModelChatMessageRole.Assistant: return "assistant";
default: return "system";
}
}
function convertMessages(messages: readonly LanguageModelChatRequestMessage[]) {
return messages.map(msg => ({
role: convertRole(msg.role),
content: msg.content
.filter(part => part instanceof LanguageModelTextPart)
.map(part => part.value)
.join(""),
}));
}流式响应处理
OpenAI API 的流式响应是 Server-Sent Events (SSE) 格式:
data: {"id":"...","choices":[{"delta":{"content":"Hello"}}]}
data: {"id":"...","choices":[{"delta":{"content":" world"}}]}
data: [DONE]Provider 需要逐行解析这些事件,提取 delta.content,然后通过 progress.report() 推送给 Copilot Chat:
const response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
body: JSON.stringify({ model, messages, stream: true }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
const data = JSON.parse(line.slice(6));
const text = data.choices?.[0]?.delta?.content;
if (text) {
progress.report(new vscode.LanguageModelTextPart(text));
}
}
}配置管理
典型的 OAI Compatible Provider 需要以下配置:
| 配置项 | 用途 |
|---|---|
| API URL | OpenAI 兼容端点(如 http://localhost:11434/v1) |
| API Key | 认证令牌(Ollama 不需要) |
| Model List | 可用模型列表(手动指定或从 /v1/models 自动获取) |
| Temperature | 生成温度 |
| Max Tokens | 最大输出 token 数 |
这些配置通过 contributes.configuration 声明,用户在 VSCode Settings 中编辑。
局限性与设计权衡
Proposed API 的限制
chatProvider 至今仍是 Proposed API。这意味着:
- 无法上架 Marketplace。用户只能通过 VSIX 手动安装。
- 需要用户手动启用。在
~/.vscode/argv.json中添加enable-proposed-api。 - API 可能随时变更。每次 VSCode 更新都可能破坏兼容性。
- 只对 Copilot Individual 用户开放。组织版(Business / Enterprise)有额外的策略限制。
这些限制决定了基于此 API 的扩展目前更适合个人用户和技术爱好者,而非企业级部署。
消息格式的不稳定性
Copilot 注入的系统消息和 <userRequest> 包装格式并没有公开文档。不同 Copilot 版本、不同模式下的格式可能不同。Provider 在做消息转换时需要对格式变化有一定容错能力,不能硬编码解析逻辑。
会话管理的挑战
没有 Conversation ID 是这个 API 最大的设计遗憾之一。对于无状态的 HTTP API 后端(如 OpenAI Compatible),这不是问题——每次请求都带完整历史就行。但对于有状态的后端(如 Cursor CLI 的 --resume 机制),Provider 必须自己实现会话匹配算法,这增加了相当多的复杂度。
模式检测的间接性
Copilot 的 Agent / Ask / Edit 模式没有直接暴露给 Provider。Provider 只能通过 options.tools 的内容间接推断。这种「通过观察副作用来推断意图」的方式不太优雅,但目前是唯一的办法。
与其他方案的对比
在 Language Model Chat Provider 出现之前,社区有几种将自定义模型接入 VSCode 的方式:
| 方案 | 原理 | 优缺点 |
|---|---|---|
| HTTP 代理 | 在本地起一个代理服务器,冒充 GitHub Copilot 的 API | 可以绕过 Copilot 订阅限制,但不稳定且违反 ToS |
| 独立 Chat 面板 | 扩展自建聊天 UI | 不在 Copilot Chat 中,用户需要切换面板 |
| Language Model Chat Provider | 官方 API,直接注册到 Copilot Chat | 集成度最高,但受 Proposed API 限制 |
Language Model Chat Provider 的优势在于原生集成:模型出现在 Copilot 的模型选择器中,聊天历史共享,工具调用由 Copilot 统一管理。用户不需要学习新的 UI,只需要切换模型就能用自己的后端。
小结
Language Model Chat Provider API 的本质是把 Copilot Chat 从一个封闭的 GitHub AI 客户端变成了一个开放的聊天框架。Provider 负责模型发现、消息转换和流式传输,Copilot 负责 UI、上下文管理和工具调用。这个分工让任何人都可以把自己的模型后端接入 Copilot Chat——无论是本地运行的 Ollama、云端的自建 API,还是 Cursor 的 CLI。
OAI Compatible Provider 选择了最通用的路线:OpenAI 协议是事实标准,几乎所有推理框架都支持。而 cursor-copilot-bridge 选择了一条更特殊的路线:直接调用 Cursor CLI 作为后端,复用 Cursor 订阅的全部模型。两种方案的共同点是,它们都建立在同一个 Provider API 之上,遵循相同的消息流转规范。