Skip to content

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

typescript
provideLanguageModelChatInformation(
  options: { silent: boolean },
  token: CancellationToken
): Promise<LanguageModelChatInformation[]>

返回可用模型列表。VSCode 在几个时机调用它:

  • 用户点开模型选择器时(silent: false
  • 周期性刷新时(silent: true

silent 参数是个重要的设计选择——当 silent: true 时,Provider 应该返回缓存的列表,避免频繁的网络请求。只有用户主动操作时才需要实时刷新。

每个返回的 LanguageModelChatInformation 对象描述一个模型:

typescript
{
  id: "gpt-4o",           // 唯一标识
  name: "GPT-4o",         // 展示名称
  family: "openai",       // 模型家族(归类用)
  version: "1.0.0",       // 版本
  maxInputTokens: 128000, // 输入 token 上限
  maxOutputTokens: 16384, // 输出 token 上限
  capabilities: {
    toolCalling: true,     // 是否支持工具调用
    imageInput: false,     // 是否支持图片输入
  }
}

provideLanguageModelChatResponse

typescript
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

typescript
provideTokenCount(
  model: LanguageModelChatInformation,
  text: string | LanguageModelChatRequestMessage,
  token: CancellationToken
): Promise<number>

估算文本的 token 数量。Copilot 用这个来决定上下文裁剪策略。大多数 Provider 的实现是简单的字符数除以 4:

typescript
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_filereplace_string_in_filerun_in_terminal
  • Ask 模式options.tools 只包含读操作工具——read_filelist_directorygrep_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 的消息格式很简单:

json
{
  "messages": [
    { "role": "system", "content": "..." },
    { "role": "user", "content": "..." },
    { "role": "assistant", "content": "..." }
  ]
}

而 VSCode 的消息使用 LanguageModelChatRequestMessage 对象,角色是枚举值(User = 1, Assistant = 2),内容是 LanguageModelTextPart 数组。Provider 需要做一层映射:

typescript
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:

typescript
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 URLOpenAI 兼容端点(如 http://localhost:11434/v1
API Key认证令牌(Ollama 不需要)
Model List可用模型列表(手动指定或从 /v1/models 自动获取)
Temperature生成温度
Max Tokens最大输出 token 数

这些配置通过 contributes.configuration 声明,用户在 VSCode Settings 中编辑。

局限性与设计权衡

Proposed API 的限制

chatProvider 至今仍是 Proposed API。这意味着:

  1. 无法上架 Marketplace。用户只能通过 VSIX 手动安装。
  2. 需要用户手动启用。在 ~/.vscode/argv.json 中添加 enable-proposed-api
  3. API 可能随时变更。每次 VSCode 更新都可能破坏兼容性。
  4. 只对 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 之上,遵循相同的消息流转规范。

最后更新于:

Hosted by GitHub Pages