Compare commits

...

4 Commits

Author SHA1 Message Date
abeatrix d41ee11f47 fix 2026-06-02 17:51:14 -07:00
abeatrix 57a7843ae6 Merge branch 'main' into bee/vscode-lm 2026-06-02 17:27:25 -07:00
abeatrix a459a79a75 patch 2026-06-02 17:26:22 -07:00
abeatrix a097202924 feat(vscode): add GitHub Copilot provider integration
Register a GitHub Copilot gateway provider using the VS Code Language
Model API, enabling model selection and streaming chat completions
through Copilot. Includes message conversion between agent and VS Code
formats, model listing, and token estimation helpers.
2026-06-02 17:06:07 -07:00
6 changed files with 398 additions and 5 deletions
+353 -5
View File
@@ -21,10 +21,17 @@ import {
type ToolPolicy,
} from "@cline/core";
import {
type AgentMessage,
type AgentMessagePart,
type AgentModelEvent,
type AgentTool,
buildClineSystemPrompt,
createClineTelemetryServiceConfig,
createClineTelemetryServiceMetadata,
estimateTokens,
type GatewayProviderContext,
type GatewayProviderRegistration,
type GatewayStreamRequest,
} from "@cline/shared";
import * as vscode from "vscode";
import { displayName, version } from "../package.json";
@@ -34,6 +41,7 @@ import type {
WebviewChatMessage,
WebviewInboundMessage,
WebviewOutboundMessage,
WebviewProviderModel,
WebviewSessionSummary,
} from "./webview-protocol";
@@ -44,6 +52,9 @@ const HUB_POLL_INTERVAL_MS = 200;
const TERMINAL_SHELL_INTEGRATION_TIMEOUT_MS = 5_000;
const TERMINAL_EXECUTION_TIMEOUT_MS = 120_000;
const TERMINAL_OUTPUT_LIMIT = 1_000_000;
const GITHUB_COPILOT_PROVIDER_ID = "github-copilot";
const GITHUB_COPILOT_AUTO_MODEL_ID = "copilot-auto";
const VSCODE_EXTENSION_HUB_OWNER_LABEL = `vscode-extension:${process.pid}`;
const REFRESH_SESSION_EVENTS = new Set([
"session.created",
"session.updated",
@@ -58,9 +69,11 @@ const REFRESH_SESSION_EVENTS = new Set([
let extensionTelemetryHandle:
| ReturnType<typeof createVscodeTelemetry>
| undefined;
let githubCopilotProviderRegistered = false;
export function activate(context: vscode.ExtensionContext): void {
const outputChannel = vscode.window.createOutputChannel("Cline");
registerGitHubCopilotProvider();
extensionTelemetryHandle = createVscodeTelemetry({
extensionVersion: version,
clineType: displayName,
@@ -227,6 +240,310 @@ function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function stringifyAgentPart(part: AgentMessagePart): string {
switch (part.type) {
case "text":
return part.text;
case "reasoning":
return part.text;
case "file":
return `<file path="${part.path}">\n${part.content}\n</file>`;
case "image":
return "[Image attachment omitted: VS Code Language Model API text request]";
case "tool-call":
return JSON.stringify({
type: "tool_call",
id: part.toolCallId,
name: part.toolName,
input: part.input,
});
case "tool-result":
return stringifyContent(part.output);
}
}
function stringifyAgentMessage(message: AgentMessage): string {
return message.content.map((part) => stringifyAgentPart(part)).join("");
}
function toVsCodeSystemMessage(
systemPrompt: string | undefined,
): vscode.LanguageModelChatMessage[] {
const trimmed = systemPrompt?.trim();
return trimmed
? [vscode.LanguageModelChatMessage.User(`[System]\n${trimmed}`)]
: [];
}
async function listGitHubCopilotModels(): Promise<WebviewProviderModel[]> {
const models = await vscode.lm.selectChatModels({ vendor: "copilot" });
if (!models.length) {
return [
{
id: GITHUB_COPILOT_AUTO_MODEL_ID,
name: "Copilot Auto",
supportsThinking: false,
supportsReasoning: false,
},
];
}
return models
.map((model) => ({
id: model.id || GITHUB_COPILOT_AUTO_MODEL_ID,
name: model.name || model.id || "Copilot Auto",
supportsThinking: false,
supportsReasoning: false,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
function getRegisteredGatewayProvider(
providerId: string,
): GatewayProviderRegistration | undefined {
return Llms.getRegisteredGatewayProviders().find(
(provider) => provider.manifest.id === providerId,
);
}
function listGatewayProviderModels(
registration: GatewayProviderRegistration,
): WebviewProviderModel[] {
const manifestModels =
registration.manifest.models.length > 0
? registration.manifest.models
: [
{
id: registration.manifest.defaultModelId,
name: registration.manifest.defaultModelId,
providerId: registration.manifest.id,
capabilities: registration.manifest.capabilities?.includes("tools")
? (["text", "tools"] as const)
: (["text"] as const),
},
];
return manifestModels
.map((model) => ({
id: model.id,
name: model.name ?? model.id,
supportsReasoning: model.capabilities?.includes("reasoning"),
supportsThinking: model.capabilities?.includes("reasoning"),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
function toVsCodeLanguageModelMessage(
message: AgentMessage,
): vscode.LanguageModelChatMessage {
if (message.role === "assistant") {
const content: Array<
vscode.LanguageModelTextPart | vscode.LanguageModelToolCallPart
> = [];
for (const part of message.content) {
if (part.type === "tool-call") {
content.push(
new vscode.LanguageModelToolCallPart(
part.toolCallId,
part.toolName,
(part.input && typeof part.input === "object"
? part.input
: { input: part.input }) as object,
),
);
continue;
}
if (part.type !== "tool-result") {
content.push(
new vscode.LanguageModelTextPart(stringifyAgentPart(part)),
);
}
}
return vscode.LanguageModelChatMessage.Assistant(content);
}
const content: Array<
vscode.LanguageModelTextPart | vscode.LanguageModelToolResultPart
> = [];
for (const part of message.content) {
if (part.type === "tool-result") {
content.push(
new vscode.LanguageModelToolResultPart(part.toolCallId, [
new vscode.LanguageModelTextPart(stringifyContent(part.output)),
]),
);
continue;
}
if (part.type !== "tool-call") {
content.push(new vscode.LanguageModelTextPart(stringifyAgentPart(part)));
}
}
return vscode.LanguageModelChatMessage.User(content);
}
async function selectGitHubCopilotModel(
modelId: string,
): Promise<vscode.LanguageModelChat> {
const selector =
modelId && modelId !== GITHUB_COPILOT_AUTO_MODEL_ID
? { vendor: "copilot", id: modelId }
: { vendor: "copilot" };
const models = await vscode.lm.selectChatModels(selector);
const model = models[0];
if (!model) {
throw new Error(
"GitHub Copilot chat models are not available. Install or enable GitHub Copilot Chat, then sign in.",
);
}
return model;
}
async function* streamGitHubCopilotRequest(
request: GatewayStreamRequest,
context: GatewayProviderContext,
): AsyncIterable<AgentModelEvent> {
const model = await selectGitHubCopilotModel(request.modelId);
const tokenSource = new vscode.CancellationTokenSource();
const abortListener = () => tokenSource.cancel();
request.signal?.addEventListener("abort", abortListener, { once: true });
const messages = [
...toVsCodeSystemMessage(request.systemPrompt),
...request.messages.map((message) => toVsCodeLanguageModelMessage(message)),
];
const inputTokenSource = [
request.systemPrompt ?? "",
...request.messages.map((message) => stringifyAgentMessage(message)),
JSON.stringify(request.tools ?? []),
].join("\n");
const inputTokens = estimateTokens(inputTokenSource.length);
let outputChars = 0;
let sawToolCall = false;
const recordOutputChars = (value: unknown): void => {
outputChars += stringifyContent(value).length;
};
const finishReason = (): AgentModelEvent => ({
type: "finish",
reason: sawToolCall ? "tool-calls" : "stop",
});
try {
const response = await model.sendRequest(
messages,
{
justification: `Cline would like to use '${model.name}' from GitHub Copilot.`,
tools: request.tools?.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
toolMode: request.tools?.length
? vscode.LanguageModelChatToolMode.Auto
: undefined,
},
tokenSource.token,
);
for await (const chunk of response.stream) {
if (chunk instanceof vscode.LanguageModelTextPart) {
outputChars += chunk.value.length;
yield { type: "text-delta", text: chunk.value };
continue;
}
if (chunk instanceof vscode.LanguageModelToolCallPart) {
sawToolCall = true;
recordOutputChars({
callId: chunk.callId,
name: chunk.name,
input: chunk.input,
});
yield {
type: "tool-call-delta",
toolCallId: chunk.callId,
toolName: chunk.name,
input: chunk.input,
};
}
}
yield {
type: "usage",
usage: {
inputTokens,
outputTokens: outputChars > 0 ? estimateTokens(outputChars) : 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost: 0,
},
};
yield finishReason();
} catch (error) {
context.logger?.log("GitHub Copilot provider request failed", {
severity: "error",
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof vscode.CancellationError) {
yield { type: "finish", reason: "aborted" };
return;
}
yield {
type: "finish",
reason: "error",
error: error instanceof Error ? error.message : String(error),
};
} finally {
request.signal?.removeEventListener("abort", abortListener);
tokenSource.dispose();
}
}
function registerGitHubCopilotProvider(): void {
if (githubCopilotProviderRegistered) return;
githubCopilotProviderRegistered = true;
const registration: GatewayProviderRegistration = {
manifest: {
id: GITHUB_COPILOT_PROVIDER_ID,
name: "GitHub Copilot",
description: "GitHub Copilot through VS Code's Language Model API.",
defaultModelId: GITHUB_COPILOT_AUTO_MODEL_ID,
env: ["node"],
capabilities: ["tools"],
models: [
{
id: GITHUB_COPILOT_AUTO_MODEL_ID,
name: "Copilot Auto",
providerId: GITHUB_COPILOT_PROVIDER_ID,
capabilities: ["text", "tools"],
},
],
},
createProvider: () => ({
stream: streamGitHubCopilotRequest,
}),
};
Llms.registerProvider({
provider: {
id: GITHUB_COPILOT_PROVIDER_ID,
name: "GitHub Copilot",
description: "GitHub Copilot through VS Code's Language Model API.",
defaultModelId: GITHUB_COPILOT_AUTO_MODEL_ID,
client: "custom",
source: "system",
capabilities: ["tools"],
},
models: {
[GITHUB_COPILOT_AUTO_MODEL_ID]: {
id: GITHUB_COPILOT_AUTO_MODEL_ID,
name: "Copilot Auto",
capabilities: ["streaming", "tools"],
},
},
});
Llms.registerGatewayProvider(registration);
}
function readTerminalCommandInput(input: unknown): {
command: string;
cwd?: string;
@@ -698,7 +1015,11 @@ class CoreChatWebviewController implements vscode.Disposable {
}
private async discoverOrStartHub(): Promise<HubResolution | undefined> {
const owner = resolveSharedHubOwnerContext();
// VS Code LM providers must execute inside the extension host because
// `vscode.lm` is not available to detached hub daemon processes.
const owner = resolveSharedHubOwnerContext(
VSCODE_EXTENSION_HUB_OWNER_LABEL,
);
if (this.hubUrl) {
const healthy = await probeHubServer(this.hubUrl);
@@ -849,16 +1170,29 @@ class CoreChatWebviewController implements vscode.Disposable {
private async loadProviders(preferredProvider?: string): Promise<void> {
const state = this.providerSettingsManager.read();
const ids = Llms.getProviderIds().sort((a, b) => a.localeCompare(b));
const gatewayProviders = Llms.getRegisteredGatewayProviders();
const gatewayProvidersById = new Map(
gatewayProviders.map((provider) => [provider.manifest.id, provider]),
);
const ids = Array.from(
new Set([
...Llms.getProviderIds(),
...gatewayProviders.map((provider) => provider.manifest.id),
]),
).sort((a, b) => a.localeCompare(b));
const providers: ProviderListItem[] = (
await Promise.all(
ids.map(async (id) => {
const info = await Llms.getProvider(id);
const gatewayProvider = gatewayProvidersById.get(id);
return {
id,
name: info?.name ?? id,
enabled: Boolean(state.providers[id]?.settings),
defaultModelId: info?.defaultModelId,
name: info?.name ?? gatewayProvider?.manifest.name ?? id,
enabled:
gatewayProvidersById.has(id) ||
Boolean(state.providers[id]?.settings),
defaultModelId:
info?.defaultModelId ?? gatewayProvider?.manifest.defaultModelId,
};
}),
)
@@ -877,6 +1211,20 @@ class CoreChatWebviewController implements vscode.Disposable {
private async loadModels(providerId: string): Promise<void> {
const provider = providerId.trim();
if (!provider) return;
const gatewayProvider = getRegisteredGatewayProvider(provider);
if (provider === GITHUB_COPILOT_PROVIDER_ID) {
const models = await listGitHubCopilotModels();
await this.post({ type: "models", providerId: provider, models });
return;
}
if (gatewayProvider) {
await this.post({
type: "models",
providerId: provider,
models: listGatewayProviderModels(gatewayProvider),
});
return;
}
const modelMap = (await Llms.getModelsForProvider(provider)) as Record<
string,
LlmModelInfo
+6
View File
@@ -5,6 +5,12 @@
*/
export * as Llms from "@cline/llms";
export {
getRegisteredGatewayProviders,
registerGatewayProvider,
resetGatewayProviderRegistry,
unregisterGatewayProvider,
} from "@cline/llms";
// Shared contracts and path helpers re-exported for app consumers.
export type {
AddProviderActionRequest,
+4
View File
@@ -54,10 +54,14 @@ export {
BUILT_IN_PROVIDER_IDS,
createHandler,
createHandlerAsync,
getRegisteredGatewayProviders,
isBuiltInProviderId,
normalizeProviderId,
registerAsyncHandler,
registerGatewayProvider,
registerHandler,
resetGatewayProviderRegistry,
unregisterGatewayProvider,
} from "./providers";
export {
type ProviderUsageCostDisplay,
+6
View File
@@ -32,6 +32,12 @@ export {
registerAsyncHandler,
registerHandler,
} from "./providers/factory-registry";
export {
getRegisteredGatewayProviders,
registerGatewayProvider,
resetGatewayProviderRegistry,
unregisterGatewayProvider,
} from "./providers/gateway-provider-registry";
export type {
ApiStreamChunk,
ContentBlock,
@@ -0,0 +1,24 @@
import type { GatewayProviderRegistration } from "@cline/shared";
const registeredGatewayProviders = new Map<
string,
GatewayProviderRegistration
>();
export function registerGatewayProvider(
registration: GatewayProviderRegistration,
): void {
registeredGatewayProviders.set(registration.manifest.id, registration);
}
export function unregisterGatewayProvider(providerId: string): boolean {
return registeredGatewayProviders.delete(providerId);
}
export function getRegisteredGatewayProviders(): GatewayProviderRegistration[] {
return Array.from(registeredGatewayProviders.values());
}
export function resetGatewayProviderRegistry(): void {
registeredGatewayProviders.clear();
}
@@ -15,6 +15,7 @@ import type {
import { estimateTokens } from "@cline/shared";
import { toAsyncIterable } from "./async";
import { BUILTIN_PROVIDER_REGISTRATIONS } from "./builtins-runtime";
import { getRegisteredGatewayProviders } from "./gateway-provider-registry";
import { GatewayRegistry } from "./registry";
export type * from "@cline/shared";
@@ -223,6 +224,10 @@ export class DefaultGateway implements Gateway {
this.registry.registerProvider(provider);
}
for (const provider of getRegisteredGatewayProviders()) {
this.registry.registerProvider(provider);
}
for (const providerConfig of config.providerConfigs ?? []) {
this.registry.configureProvider(providerConfig);
}