Compare commits

...

2 Commits

Author SHA1 Message Date
Saoud Rizwan c3cb0368a1 chore: remove debugging code from GitHub Copilot provider 2026-01-16 10:16:35 -08:00
Saoud Rizwan fde1892d73 feat: add GitHub Copilot provider (WIP)
Add a new GitHub Copilot provider that allows users to use their existing
GitHub Copilot subscription to access AI models directly through the
Copilot API (api.githubcopilot.com).

New files:
- proto/cline/github_copilot.proto - Proto definitions for OAuth login flow
- src/core/api/providers/github-copilot.ts - API handler
- src/core/api/providers/github-copilot-auth.ts - OAuth device code flow
- src/core/controller/gitHubCopilot/ - Login/logout handlers
- webview-ui/.../GitHubCopilotProvider.tsx - Settings UI component

Modified files:
- proto/cline/models.proto - Added GITHUB_COPILOT enum and model ID fields
- src/shared/api.ts - Added provider type and model definitions
- src/shared/providers/providers.json - Added provider entry
- src/shared/storage/state-keys.ts - Added state keys
- src/core/api/index.ts - Wired up handler in factory
- Proto conversions and providerUtils for field mappings

Status: Work in progress - has bugs that need debugging
2026-01-16 10:09:11 -08:00
16 changed files with 854 additions and 1 deletions
+47
View File
@@ -0,0 +1,47 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
// Service for GitHub Copilot authentication
service GitHubCopilotService {
// Initiates the GitHub Copilot OAuth device code flow
// Returns verification URL and user code for the user to enter
rpc loginWithGitHubCopilot(GitHubCopilotLoginRequest) returns (stream GitHubCopilotLoginResponse);
// Logs out of GitHub Copilot by clearing the access token
rpc logoutGitHubCopilot(EmptyRequest) returns (Empty);
}
message GitHubCopilotLoginRequest {
// Optional GitHub Enterprise URL (leave empty for github.com)
optional string enterprise_url = 1;
}
message GitHubCopilotLoginResponse {
// Status of the login flow
GitHubCopilotLoginStatus status = 1;
// Verification URL for the user to visit (only set when status is WAITING_FOR_CODE)
optional string verification_url = 2;
// User code to enter at the verification URL (only set when status is WAITING_FOR_CODE)
optional string user_code = 3;
// Error message (only set when status is FAILED)
optional string error = 4;
}
enum GitHubCopilotLoginStatus {
// Waiting for user to enter the code
WAITING_FOR_CODE = 0;
// Successfully authenticated
SUCCESS = 1;
// Authentication failed
FAILED = 2;
}
+6
View File
@@ -439,6 +439,7 @@ enum ApiProvider {
HICAP = 37;
AIHUBMIX = 38;
NOUSRESEARCH = 39;
GITHUB_COPILOT = 40;
}
enum ApiFormat {
@@ -617,6 +618,7 @@ message ModelsApiConfiguration {
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137;
optional string plan_mode_nous_research_model_id = 138;
optional string gemini_plan_mode_thinking_level = 139;
optional string plan_mode_github_copilot_model_id = 140;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -659,4 +661,8 @@ message ModelsApiConfiguration {
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237;
optional string act_mode_nous_research_model_id = 238;
optional string gemini_act_mode_thinking_level = 239;
optional string act_mode_github_copilot_model_id = 240;
// GitHub Copilot configuration
optional string github_copilot_enterprise_url = 241;
}
+9
View File
@@ -42,6 +42,7 @@ import { VertexHandler } from "./providers/vertex"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { XAIHandler } from "./providers/xai"
import { ZAiHandler } from "./providers/zai"
import { GitHubCopilotHandler } from "./providers/github-copilot"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
export type CommonApiHandlerOptions = {
@@ -436,6 +437,14 @@ function createHandlerForProvider(
nousResearchApiKey: options.nousResearchApiKey,
apiModelId: mode === "plan" ? options.planModeNousResearchModelId : options.actModeNousResearchModelId,
})
case "github-copilot":
return new GitHubCopilotHandler({
onRetryAttempt: options.onRetryAttempt,
gitHubCopilotAccessToken: options.gitHubCopilotAccessToken,
gitHubCopilotModelId:
mode === "plan" ? options.planModeGitHubCopilotModelId : options.actModeGitHubCopilotModelId,
gitHubCopilotEnterpriseUrl: options.gitHubCopilotEnterpriseUrl,
})
default:
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
@@ -0,0 +1,157 @@
import { fetch } from "@/shared/net"
import { GITHUB_COPILOT_CLIENT_ID } from "./github-copilot"
interface DeviceCodeResponse {
verification_uri: string
user_code: string
device_code: string
interval: number
expires_in: number
}
interface AccessTokenResponse {
access_token?: string
error?: string
error_description?: string
}
export interface GitHubCopilotAuthResult {
success: boolean
accessToken?: string
error?: string
}
function getAuthUrls(enterpriseUrl?: string) {
if (enterpriseUrl) {
const domain = enterpriseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")
return {
deviceCodeUrl: `https://${domain}/login/device/code`,
accessTokenUrl: `https://${domain}/login/oauth/access_token`,
}
}
return {
deviceCodeUrl: "https://github.com/login/device/code",
accessTokenUrl: "https://github.com/login/oauth/access_token",
}
}
/**
* Initiates the GitHub device code OAuth flow.
* Returns the device code response with the verification URL and user code.
*/
export async function initiateDeviceCodeFlow(enterpriseUrl?: string): Promise<{
verificationUri: string
userCode: string
deviceCode: string
interval: number
expiresIn: number
}> {
const urls = getAuthUrls(enterpriseUrl)
const response = await fetch(urls.deviceCodeUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "cline/1.0",
},
body: JSON.stringify({
client_id: GITHUB_COPILOT_CLIENT_ID,
scope: "read:user",
}),
})
if (!response.ok) {
throw new Error(`Failed to initiate device authorization: ${response.status} ${response.statusText}`)
}
const data = (await response.json()) as DeviceCodeResponse
return {
verificationUri: data.verification_uri,
userCode: data.user_code,
deviceCode: data.device_code,
interval: data.interval,
expiresIn: data.expires_in,
}
}
/**
* Polls for the access token after the user has authorized the device.
* Should be called repeatedly with the device code until an access token is received or an error occurs.
*/
export async function pollForAccessToken(
deviceCode: string,
enterpriseUrl?: string
): Promise<{ status: "pending" | "success" | "failed"; accessToken?: string; error?: string }> {
const urls = getAuthUrls(enterpriseUrl)
const response = await fetch(urls.accessTokenUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "cline/1.0",
},
body: JSON.stringify({
client_id: GITHUB_COPILOT_CLIENT_ID,
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
})
if (!response.ok) {
return { status: "failed", error: `HTTP ${response.status}: ${response.statusText}` }
}
const data = (await response.json()) as AccessTokenResponse
if (data.access_token) {
return { status: "success", accessToken: data.access_token }
}
if (data.error === "authorization_pending") {
return { status: "pending" }
}
if (data.error) {
return { status: "failed", error: data.error_description || data.error }
}
return { status: "pending" }
}
/**
* Complete OAuth flow - polls until success, failure, or timeout.
* This is a convenience function that handles the polling loop.
*/
export async function completeDeviceCodeFlow(
deviceCode: string,
interval: number,
expiresIn: number,
enterpriseUrl?: string,
onProgress?: (message: string) => void
): Promise<GitHubCopilotAuthResult> {
const startTime = Date.now()
const expiresAt = startTime + expiresIn * 1000
while (Date.now() < expiresAt) {
await new Promise((resolve) => setTimeout(resolve, interval * 1000))
onProgress?.("Waiting for authorization...")
const result = await pollForAccessToken(deviceCode, enterpriseUrl)
if (result.status === "success" && result.accessToken) {
return { success: true, accessToken: result.accessToken }
}
if (result.status === "failed") {
return { success: false, error: result.error }
}
// status === "pending", continue polling
}
return { success: false, error: "Authorization timed out. Please try again." }
}
+154
View File
@@ -0,0 +1,154 @@
import {
GitHubCopilotModelId,
gitHubCopilotDefaultModelId,
gitHubCopilotModels,
ModelInfo,
} from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
// GitHub Copilot OAuth Client ID (from OpenCode)
export const GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz"
// GitHub Copilot API base URL
const GITHUB_COPILOT_API_BASE_URL = "https://api.githubcopilot.com"
export interface GitHubCopilotHandlerOptions extends CommonApiHandlerOptions {
gitHubCopilotAccessToken?: string
gitHubCopilotModelId?: string
gitHubCopilotEnterpriseUrl?: string
}
function getBaseUrl(enterpriseUrl?: string): string {
if (enterpriseUrl) {
// Remove protocol and trailing slash from enterprise URL
const domain = enterpriseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")
return `https://copilot-api.${domain}`
}
return GITHUB_COPILOT_API_BASE_URL
}
export class GitHubCopilotHandler implements ApiHandler {
private options: GitHubCopilotHandlerOptions
private client: OpenAI | undefined
constructor(options: GitHubCopilotHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.gitHubCopilotAccessToken) {
throw new Error("GitHub Copilot access token is required. Please log in to GitHub Copilot first.")
}
const baseUrl = getBaseUrl(this.options.gitHubCopilotEnterpriseUrl)
// Create a custom fetch that adds Copilot-specific headers
const copilotFetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const headers = new Headers(init?.headers)
// Add Copilot-specific headers
headers.set("User-Agent", "cline/1.0")
headers.set("Openai-Intent", "conversation-edits")
headers.set("X-Initiator", "agent")
// Check if this is a vision request (has images in the body)
if (init?.body) {
try {
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
if (body?.messages) {
const hasImages = body.messages.some(
(msg: any) =>
Array.isArray(msg.content) &&
msg.content.some((part: any) => part.type === "image_url")
)
if (hasImages) {
headers.set("Copilot-Vision-Request", "true")
}
}
} catch {
// Ignore JSON parse errors
}
}
// Remove default authorization and set Copilot bearer token
headers.delete("Authorization")
headers.set("Authorization", `Bearer ${this.options.gitHubCopilotAccessToken}`)
return fetch(input, { ...init, headers })
}
try {
this.client = new OpenAI({
apiKey: "", // Not used, we set auth in custom fetch
baseURL: baseUrl,
fetch: copilotFetch,
})
} catch (error: any) {
throw new Error(`Error creating GitHub Copilot client: ${error.message}`)
}
}
return this.client
}
@withRetry({ maxRetries: 3 })
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const toolCallProcessor = new ToolCallProcessor()
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools, false),
})
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
const inputTokens = chunk.usage.prompt_tokens || 0
const outputTokens = chunk.usage.completion_tokens || 0
yield {
type: "usage",
inputTokens,
outputTokens,
totalCost: 0, // Free with Copilot subscription
}
}
}
}
getModel(): { id: GitHubCopilotModelId; info: ModelInfo } {
const modelId = this.options.gitHubCopilotModelId
if (modelId && modelId in gitHubCopilotModels) {
const id = modelId as GitHubCopilotModelId
const info = gitHubCopilotModels[id]
return { id, info: { ...info } }
}
return {
id: gitHubCopilotDefaultModelId,
info: { ...gitHubCopilotModels[gitHubCopilotDefaultModelId] },
}
}
}
@@ -0,0 +1,91 @@
import * as vscode from "vscode"
import {
GitHubCopilotLoginRequest,
GitHubCopilotLoginResponse,
GitHubCopilotLoginStatus,
} from "@shared/proto/cline/github_copilot"
import { initiateDeviceCodeFlow, completeDeviceCodeFlow } from "@/core/api/providers/github-copilot-auth"
import { Controller } from "../index"
import { StreamingResponseHandler } from "../grpc-handler"
/**
* Handles the GitHub Copilot OAuth device code login flow.
* Streams status updates back to the webview.
*/
export async function loginWithGitHubCopilot(
controller: Controller,
request: GitHubCopilotLoginRequest,
responseStream: StreamingResponseHandler<GitHubCopilotLoginResponse>,
_requestId?: string,
): Promise<void> {
const enterpriseUrl = request.enterpriseUrl || undefined
try {
// Step 1: Initiate device code flow
const deviceCodeResponse = await initiateDeviceCodeFlow(enterpriseUrl)
// Step 2: Send the user code and verification URL
await responseStream(
{
status: GitHubCopilotLoginStatus.WAITING_FOR_CODE,
verificationUrl: deviceCodeResponse.verificationUri,
userCode: deviceCodeResponse.userCode,
},
false,
)
// Open the verification URL in browser
await vscode.env.openExternal(vscode.Uri.parse(deviceCodeResponse.verificationUri))
// Step 3: Poll for the access token using the complete flow helper
const result = await completeDeviceCodeFlow(
deviceCodeResponse.deviceCode,
deviceCodeResponse.interval,
deviceCodeResponse.expiresIn,
enterpriseUrl,
)
if (result.success && result.accessToken) {
// Step 4: Store the token
controller.stateManager.setSecretsBatch({
gitHubCopilotAccessToken: result.accessToken,
})
// Also store enterprise URL if provided
if (enterpriseUrl) {
controller.stateManager.setGlobalStateBatch({
gitHubCopilotEnterpriseUrl: enterpriseUrl,
})
}
// Post updated state to webview
await controller.postStateToWebview()
// Send success
await responseStream(
{
status: GitHubCopilotLoginStatus.SUCCESS,
},
true,
)
} else {
// Send failure
await responseStream(
{
status: GitHubCopilotLoginStatus.FAILED,
error: result.error || "Authentication failed",
},
true,
)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
await responseStream(
{
status: GitHubCopilotLoginStatus.FAILED,
error: errorMessage,
},
true,
)
}
}
@@ -0,0 +1,22 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
/**
* Handles logging out of GitHub Copilot by clearing the access token.
*/
export async function logoutGitHubCopilot(controller: Controller, _request: EmptyRequest): Promise<Empty> {
// Clear the access token
controller.stateManager.setSecretsBatch({
gitHubCopilotAccessToken: undefined,
})
// Clear enterprise URL
controller.stateManager.setGlobalStateBatch({
gitHubCopilotEnterpriseUrl: undefined,
})
// Post updated state to webview
await controller.postStateToWebview()
return Empty.create()
}
@@ -112,6 +112,11 @@ export async function updateApiConfigurationProto(
: undefined,
geminiPlanModeThinkingLevel: protoApiConfiguration.geminiPlanModeThinkingLevel,
geminiActModeThinkingLevel: protoApiConfiguration.geminiActModeThinkingLevel,
// GitHub Copilot - map proto field names to TypeScript field names
planModeGitHubCopilotModelId: protoApiConfiguration.planModeGithubCopilotModelId,
actModeGitHubCopilotModelId: protoApiConfiguration.actModeGithubCopilotModelId,
gitHubCopilotEnterpriseUrl: protoApiConfiguration.githubCopilotEnterpriseUrl,
}
// Update the API configuration in storage
+2 -1
View File
@@ -1904,7 +1904,8 @@ export class Task {
let response: ClineAskResponse
// Skip auto-retry for Cline provider insufficient credits or auth errors
if (!isClineProviderInsufficientCredits && !isAuthError && this.taskState.autoRetryAttempts < 3) {
// TEMP: Disabled auto-retry for debugging GitHub Copilot - change back to < 3
if (!isClineProviderInsufficientCredits && !isAuthError && this.taskState.autoRetryAttempts < 0) {
// Auto-retry enabled with max 3 attempts: automatically approve the retry
this.taskState.autoRetryAttempts++
+81
View File
@@ -42,6 +42,7 @@ export type ApiProvider =
| "minimax"
| "hicap"
| "nousResearch"
| "github-copilot"
export const DEFAULT_API_PROVIDER = "openrouter" as ApiProvider
@@ -4205,3 +4206,83 @@ export const nousResearchModels = {
"This incarnation of Hermes 4 balances scale and size. It handles complex reasoning tasks, while staying fast and cost effective. A versatile choice for many use cases.",
},
} as const satisfies Record<string, ModelInfo>
// GitHub Copilot
// Models available through GitHub Copilot subscription
// https://api.githubcopilot.com
export type GitHubCopilotModelId = keyof typeof gitHubCopilotModels
export const gitHubCopilotDefaultModelId: GitHubCopilotModelId = "claude-sonnet-4"
export const gitHubCopilotModels = {
"claude-sonnet-4": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Free with Copilot subscription
outputPrice: 0,
description: "Claude Sonnet 4 via GitHub Copilot",
},
"claude-sonnet-4-5": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Claude Sonnet 4.5 via GitHub Copilot",
},
"gpt-4o": {
maxTokens: 16384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "GPT-4o via GitHub Copilot",
},
"gpt-4o-mini": {
maxTokens: 16384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "GPT-4o Mini via GitHub Copilot",
},
o1: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "OpenAI o1 via GitHub Copilot",
},
o3: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "OpenAI o3 via GitHub Copilot",
},
"gemini-2.0-flash": {
maxTokens: 8192,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Gemini 2.0 Flash via GitHub Copilot",
},
"gemini-2.5-pro": {
maxTokens: 8192,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Gemini 2.5 Pro via GitHub Copilot",
},
} as const satisfies Record<string, ModelInfo>
@@ -321,6 +321,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.HICAP
case "nousResearch":
return ProtoApiProvider.NOUSRESEARCH
case "github-copilot":
return ProtoApiProvider.GITHUB_COPILOT
default:
return ProtoApiProvider.ANTHROPIC
}
@@ -409,6 +411,8 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid
return "minimax"
case ProtoApiProvider.NOUSRESEARCH:
return "nousResearch"
case ProtoApiProvider.GITHUB_COPILOT:
return "github-copilot"
default:
return "anthropic"
}
@@ -543,6 +547,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
planModeNousResearchModelId: config.planModeNousResearchModelId,
planModeVercelAiGatewayModelId: config.planModeVercelAiGatewayModelId,
planModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.planModeVercelAiGatewayModelInfo),
planModeGithubCopilotModelId: config.planModeGitHubCopilotModelId,
// Act mode configurations
actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined,
@@ -585,6 +590,10 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
actModeNousResearchModelId: config.actModeNousResearchModelId,
actModeVercelAiGatewayModelId: config.actModeVercelAiGatewayModelId,
actModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.actModeVercelAiGatewayModelInfo),
actModeGithubCopilotModelId: config.actModeGitHubCopilotModelId,
// GitHub Copilot configuration
githubCopilotEnterpriseUrl: config.gitHubCopilotEnterpriseUrl,
}
}
@@ -720,6 +729,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
planModeNousResearchModelId: protoConfig.planModeNousResearchModelId,
planModeVercelAiGatewayModelId: protoConfig.planModeVercelAiGatewayModelId,
planModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.planModeVercelAiGatewayModelInfo),
planModeGitHubCopilotModelId: protoConfig.planModeGithubCopilotModelId,
// Act mode configurations
actModeApiProvider:
@@ -763,5 +773,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
actModeNousResearchModelId: protoConfig.actModeNousResearchModelId,
actModeVercelAiGatewayModelId: protoConfig.actModeVercelAiGatewayModelId,
actModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.actModeVercelAiGatewayModelInfo),
actModeGitHubCopilotModelId: protoConfig.actModeGithubCopilotModelId,
// GitHub Copilot configuration
gitHubCopilotEnterpriseUrl: protoConfig.githubCopilotEnterpriseUrl,
}
}
+4
View File
@@ -24,6 +24,10 @@
"value": "bedrock",
"label": "Amazon Bedrock"
},
{
"value": "github-copilot",
"label": "GitHub Copilot"
},
{
"value": "vscode-lm",
"label": "VS Code LM API"
+6
View File
@@ -171,6 +171,7 @@ const API_HANDLER_SETTINGS_FIELDS = {
planModeNousResearchModelId: { default: undefined as string | undefined },
planModeVercelAiGatewayModelId: { default: undefined as string | undefined },
planModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
planModeGitHubCopilotModelId: { default: undefined as string | undefined },
// Act mode configurations
actModeApiModelId: { default: undefined as string | undefined },
@@ -213,6 +214,10 @@ const API_HANDLER_SETTINGS_FIELDS = {
actModeNousResearchModelId: { default: undefined as string | undefined },
actModeVercelAiGatewayModelId: { default: undefined as string | undefined },
actModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
actModeGitHubCopilotModelId: { default: undefined as string | undefined },
// GitHub Copilot configuration
gitHubCopilotEnterpriseUrl: { default: undefined as string | undefined },
// Model-specific settings
planModeApiProvider: { default: DEFAULT_API_PROVIDER as ApiProvider },
@@ -334,6 +339,7 @@ const SECRETS_KEYS = [
"ocaApiKey",
"ocaRefreshToken",
"mcpOAuthSecrets",
"gitHubCopilotAccessToken",
] as const
export const LocalStateKeys = [
@@ -52,6 +52,7 @@ import { VertexProvider } from "./providers/VertexProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
import { XaiProvider } from "./providers/XaiProvider"
import { ZAiProvider } from "./providers/ZAiProvider"
import { GitHubCopilotProvider } from "./providers/GitHubCopilotProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
interface ApiOptionsProps {
@@ -429,6 +430,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider currentMode={currentMode} />}
{apiConfiguration && selectedProvider === "github-copilot" && (
<GitHubCopilotProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration && selectedProvider === "groq" && (
<GroqProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
@@ -0,0 +1,226 @@
import { useState } from "react"
import { gitHubCopilotModels } from "@shared/api"
import { GitHubCopilotLoginStatus } from "@shared/proto/cline/github_copilot"
import { Mode } from "@shared/storage/types"
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { GitHubCopilotServiceClient } from "@/services/grpc-client"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
interface GitHubCopilotProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
}
export const GitHubCopilotProvider = ({ showModelOptions, isPopup, currentMode }: GitHubCopilotProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const [loginState, setLoginState] = useState<{
isLoading: boolean
userCode?: string
verificationUrl?: string
error?: string
}>({ isLoading: false })
// Get model info based on selected model
const getModelInfo = () => {
const modelId =
currentMode === "plan"
? apiConfiguration?.planModeGitHubCopilotModelId
: apiConfiguration?.actModeGitHubCopilotModelId
if (modelId && modelId in gitHubCopilotModels) {
return {
selectedModelId: modelId,
selectedModelInfo: gitHubCopilotModels[modelId as keyof typeof gitHubCopilotModels],
}
}
return {
selectedModelId: "claude-sonnet-4",
selectedModelInfo: gitHubCopilotModels["claude-sonnet-4"],
}
}
const { selectedModelId, selectedModelInfo } = getModelInfo()
const hasToken = Boolean(apiConfiguration?.gitHubCopilotAccessToken)
const handleLogin = () => {
setLoginState({ isLoading: true })
const unsubscribe = GitHubCopilotServiceClient.loginWithGitHubCopilot(
{ enterpriseUrl: apiConfiguration?.gitHubCopilotEnterpriseUrl },
{
onResponse: (response) => {
if (response.status === GitHubCopilotLoginStatus.WAITING_FOR_CODE) {
setLoginState({
isLoading: true,
userCode: response.userCode,
verificationUrl: response.verificationUrl,
})
} else if (response.status === GitHubCopilotLoginStatus.SUCCESS) {
setLoginState({ isLoading: false })
unsubscribe()
} else if (response.status === GitHubCopilotLoginStatus.FAILED) {
setLoginState({ isLoading: false, error: response.error })
unsubscribe()
}
},
onError: (error) => {
setLoginState({ isLoading: false, error: error.message || "Login failed" })
},
onComplete: () => {
if (loginState.isLoading) {
setLoginState((prev) => ({ ...prev, isLoading: false }))
}
},
},
)
}
const handleLogout = async () => {
await GitHubCopilotServiceClient.logoutGitHubCopilot({})
setLoginState({ isLoading: false })
}
return (
<div>
{/* Description */}
<p
style={{
fontSize: "12px",
marginBottom: "12px",
color: "var(--vscode-descriptionForeground)",
}}>
Use your GitHub Copilot subscription to access AI models. Requires an active GitHub Copilot
subscription.
</p>
{/* Enterprise URL (optional) - show before login */}
{!hasToken && (
<div style={{ marginBottom: "12px" }}>
<label
htmlFor="github-copilot-enterprise-url"
style={{ display: "block", marginBottom: "4px", fontWeight: 500 }}>
GitHub Enterprise URL (Optional)
</label>
<VSCodeTextField
id="github-copilot-enterprise-url"
value={apiConfiguration?.gitHubCopilotEnterpriseUrl || ""}
onInput={(e: any) => handleFieldChange("gitHubCopilotEnterpriseUrl", e.target.value)}
placeholder="company.ghe.com"
style={{ width: "100%" }}
/>
<p
style={{
fontSize: "11px",
marginTop: "4px",
color: "var(--vscode-descriptionForeground)",
}}>
Leave blank for GitHub.com. For GitHub Enterprise, enter your domain.
</p>
</div>
)}
{/* Login/Logout Button */}
{!hasToken ? (
<div style={{ marginBottom: "12px" }}>
<VSCodeButton onClick={handleLogin} disabled={loginState.isLoading} style={{ width: "100%" }}>
{loginState.isLoading ? "Authenticating..." : "Login with GitHub"}
</VSCodeButton>
{/* Show user code during login */}
{loginState.userCode && (
<div
style={{
marginTop: "12px",
padding: "12px",
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
border: "1px solid var(--vscode-inputValidation-infoBorder)",
borderRadius: "4px",
}}>
<p style={{ margin: 0, marginBottom: "8px" }}>
Enter this code at{" "}
<a
href={loginState.verificationUrl}
style={{ color: "var(--vscode-textLink-foreground)" }}>
{loginState.verificationUrl}
</a>
:
</p>
<p
style={{
margin: 0,
fontSize: "24px",
fontWeight: "bold",
fontFamily: "monospace",
textAlign: "center",
letterSpacing: "4px",
}}>
{loginState.userCode}
</p>
</div>
)}
{/* Show error */}
{loginState.error && (
<div
style={{
marginTop: "12px",
padding: "8px",
backgroundColor: "var(--vscode-inputValidation-errorBackground)",
border: "1px solid var(--vscode-inputValidation-errorBorder)",
borderRadius: "4px",
color: "var(--vscode-inputValidation-errorForeground)",
}}>
{loginState.error}
</div>
)}
</div>
) : (
<div style={{ marginBottom: "12px" }}>
{/* Authenticated status */}
<div
style={{
marginBottom: "12px",
padding: "8px",
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
border: "1px solid var(--vscode-inputValidation-infoBorder)",
borderRadius: "4px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span style={{ color: "var(--vscode-inputValidation-infoForeground)" }}>
Authenticated with GitHub Copilot
</span>
<VSCodeButton appearance="secondary" onClick={handleLogout}>
Logout
</VSCodeButton>
</div>
</div>
)}
{showModelOptions && (
<>
<ModelSelector
label="Model"
models={gitHubCopilotModels}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeGitHubCopilotModelId", act: "actModeGitHubCopilotModelId" },
e.target.value,
currentMode,
)
}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
</>
)}
</div>
)
}
@@ -21,6 +21,8 @@ import {
fireworksModels,
geminiDefaultModelId,
geminiModels,
gitHubCopilotDefaultModelId,
gitHubCopilotModels,
groqDefaultModelId,
groqModels,
hicapModelInfoSaneDefaults,
@@ -131,6 +133,8 @@ export function getModelsForProvider(
return huggingFaceModels
case "nousResearch":
return nousResearchModels
case "github-copilot":
return gitHubCopilotModels
case "litellm":
return dynamicModels?.liteLlmModels
// Providers with dynamic models - return undefined
@@ -481,6 +485,19 @@ export function normalizeApiConfiguration(
? nousResearchModels[nousResearchModelId as keyof typeof nousResearchModels]
: nousResearchModels[nousResearchDefaultModelId],
}
case "github-copilot":
const gitHubCopilotModelId =
currentMode === "plan"
? apiConfiguration?.planModeGitHubCopilotModelId
: apiConfiguration?.actModeGitHubCopilotModelId
return {
selectedProvider: provider,
selectedModelId: gitHubCopilotModelId || gitHubCopilotDefaultModelId,
selectedModelInfo:
gitHubCopilotModelId && gitHubCopilotModelId in gitHubCopilotModels
? gitHubCopilotModels[gitHubCopilotModelId as keyof typeof gitHubCopilotModels]
: gitHubCopilotModels[gitHubCopilotDefaultModelId],
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
@@ -516,6 +533,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
aihubmixModelId: undefined,
nousResearchModelId: undefined,
vercelAiGatewayModelId: undefined,
gitHubCopilotModelId: undefined,
// Model info objects
openAiModelInfo: undefined,
@@ -569,6 +587,8 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
mode === "plan" ? apiConfiguration.planModeNousResearchModelId : apiConfiguration.actModeNousResearchModelId,
vercelAiGatewayModelId:
mode === "plan" ? apiConfiguration.planModeVercelAiGatewayModelId : apiConfiguration.actModeVercelAiGatewayModelId,
gitHubCopilotModelId:
mode === "plan" ? apiConfiguration.planModeGitHubCopilotModelId : apiConfiguration.actModeGitHubCopilotModelId,
// Model info objects
openAiModelInfo: mode === "plan" ? apiConfiguration.planModeOpenAiModelInfo : apiConfiguration.actModeOpenAiModelInfo,
@@ -769,6 +789,11 @@ export async function syncModeConfigurations(
updates.actModeNousResearchModelId = sourceFields.nousResearchModelId
break
case "github-copilot":
updates.planModeGitHubCopilotModelId = sourceFields.gitHubCopilotModelId
updates.actModeGitHubCopilotModelId = sourceFields.gitHubCopilotModelId
break
case "aihubmix":
updates.planModeAihubmixModelId = sourceFields.aihubmixModelId
updates.planModeAihubmixModelInfo = sourceFields.aihubmixModelInfo