|
| 1 | +// Package copilot provides authentication and token management for GitHub Copilot API. |
| 2 | +// It handles the OAuth2 device code flow, token exchange, and automatic token refresh. |
| 3 | +package copilot |
| 4 | + |
| 5 | +import ( |
| 6 | + "crypto/rand" |
| 7 | + "encoding/hex" |
| 8 | + "fmt" |
| 9 | + |
| 10 | + copilotshared "github.com/router-for-me/CLIProxyAPI/v6/internal/copilot" |
| 11 | +) |
| 12 | + |
| 13 | +const ( |
| 14 | + GitHubBaseURL = "https://github.com" |
| 15 | + GitHubAPIBaseURL = "https://api.github.com" |
| 16 | + // GitHubClientID is the PUBLIC OAuth client ID for GitHub Copilot's VS Code extension. |
| 17 | + // This is NOT a secret - it's the same client ID used by the official Copilot CLI and |
| 18 | + // VS Code extension, publicly visible in their source code and network requests. |
| 19 | + GitHubClientID = "Iv1.b507a08c87ecfe98" |
| 20 | + GitHubAppScopes = "read:user" |
| 21 | + DeviceCodePath = "/login/device/code" |
| 22 | + AccessTokenPath = "/login/oauth/access_token" |
| 23 | + CopilotTokenPath = "/copilot_internal/v2/token" |
| 24 | + CopilotUserPath = "/copilot_internal/user" |
| 25 | + UserInfoPath = "/user" |
| 26 | + |
| 27 | + CopilotVersion = "0.0.363" |
| 28 | + EditorPluginVersion = "copilot/" + CopilotVersion |
| 29 | + CopilotUserAgent = "copilot/" + CopilotVersion + " (linux v22.15.0)" |
| 30 | + CopilotAPIVersion = "2025-05-01" |
| 31 | + CopilotIntegrationID = "copilot-developer-cli" |
| 32 | + DefaultVSCodeVersion = "1.95.0" |
| 33 | +) |
| 34 | + |
| 35 | +type AccountType = copilotshared.AccountType |
| 36 | + |
| 37 | +const ( |
| 38 | + AccountTypeIndividual AccountType = copilotshared.AccountTypeIndividual |
| 39 | + AccountTypeBusiness AccountType = copilotshared.AccountTypeBusiness |
| 40 | + AccountTypeEnterprise AccountType = copilotshared.AccountTypeEnterprise |
| 41 | +) |
| 42 | + |
| 43 | +var ValidAccountTypes = copilotshared.ValidAccountTypes |
| 44 | + |
| 45 | +const DefaultAccountType = copilotshared.DefaultAccountType |
| 46 | + |
| 47 | +func CopilotBaseURL(accountType AccountType) string { |
| 48 | + switch accountType { |
| 49 | + case AccountTypeBusiness: |
| 50 | + return "https://api.business.githubcopilot.com" |
| 51 | + case AccountTypeEnterprise: |
| 52 | + return "https://api.enterprise.githubcopilot.com" |
| 53 | + default: |
| 54 | + // Individual accounts use the individual Copilot endpoint. |
| 55 | + return "https://api.individual.githubcopilot.com" |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +func StandardHeaders() map[string]string { |
| 60 | + return map[string]string{ |
| 61 | + "Content-Type": "application/json", |
| 62 | + "Accept": "application/json", |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +func GitHubHeaders(githubToken, vsCodeVersion string) map[string]string { |
| 67 | + if vsCodeVersion == "" { |
| 68 | + vsCodeVersion = DefaultVSCodeVersion |
| 69 | + } |
| 70 | + return map[string]string{ |
| 71 | + "Content-Type": "application/json", |
| 72 | + "Accept": "application/json", |
| 73 | + "Authorization": fmt.Sprintf("token %s", githubToken), |
| 74 | + "Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion), |
| 75 | + "Editor-Plugin-Version": EditorPluginVersion, |
| 76 | + "User-Agent": CopilotUserAgent, |
| 77 | + "X-Github-Api-Version": CopilotAPIVersion, |
| 78 | + "X-Vscode-User-Agent-Library-Version": "electron-fetch", |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +func CopilotHeaders(copilotToken, vsCodeVersion string, enableVision bool) map[string]string { |
| 83 | + if vsCodeVersion == "" { |
| 84 | + vsCodeVersion = DefaultVSCodeVersion |
| 85 | + } |
| 86 | + headers := map[string]string{ |
| 87 | + "Content-Type": "application/json", |
| 88 | + "Authorization": fmt.Sprintf("Bearer %s", copilotToken), |
| 89 | + "Copilot-Integration-Id": CopilotIntegrationID, |
| 90 | + "Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion), |
| 91 | + "Editor-Plugin-Version": EditorPluginVersion, |
| 92 | + "User-Agent": CopilotUserAgent, |
| 93 | + "Openai-Intent": "conversation-agent", |
| 94 | + "X-Github-Api-Version": CopilotAPIVersion, |
| 95 | + "X-Request-Id": generateRequestID(), |
| 96 | + "X-Interaction-Id": generateRequestID(), |
| 97 | + "X-Vscode-User-Agent-Library-Version": "electron-fetch", |
| 98 | + } |
| 99 | + if enableVision { |
| 100 | + headers["Copilot-Vision-Request"] = "true" |
| 101 | + } |
| 102 | + return headers |
| 103 | +} |
| 104 | + |
| 105 | +func generateRequestID() string { |
| 106 | + b := make([]byte, 16) |
| 107 | + if _, err := rand.Read(b); err != nil { |
| 108 | + panic(fmt.Sprintf("failed to generate random bytes for request ID: %v", err)) |
| 109 | + } |
| 110 | + return fmt.Sprintf("%s-%s-%s-%s-%s", |
| 111 | + hex.EncodeToString(b[0:4]), |
| 112 | + hex.EncodeToString(b[4:6]), |
| 113 | + hex.EncodeToString(b[6:8]), |
| 114 | + hex.EncodeToString(b[8:10]), |
| 115 | + hex.EncodeToString(b[10:16])) |
| 116 | +} |
| 117 | + |
| 118 | +// MaskToken returns a masked version of a token for safe logging. |
| 119 | +// Shows first 2 and last 2 characters with asterisks in between. |
| 120 | +// Returns "<empty>" for empty tokens and "<short>" for tokens under 5 chars. |
| 121 | +func MaskToken(token string) string { |
| 122 | + if token == "" { |
| 123 | + return "<empty>" |
| 124 | + } |
| 125 | + if len(token) < 5 { |
| 126 | + return "<short>" |
| 127 | + } |
| 128 | + return token[:2] + "****" + token[len(token)-2:] |
| 129 | +} |
0 commit comments