mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3cb0368a1 | |||
| fde1892d73 |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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++
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
"value": "bedrock",
|
||||
"label": "Amazon Bedrock"
|
||||
},
|
||||
{
|
||||
"value": "github-copilot",
|
||||
"label": "GitHub Copilot"
|
||||
},
|
||||
{
|
||||
"value": "vscode-lm",
|
||||
"label": "VS Code LM API"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user