From c3f73e3f30bbff09ac523c3a519b54d06878b2c6 Mon Sep 17 00:00:00 2001 From: nathabonfim59 Date: Sun, 15 Dec 2024 03:08:40 -0300 Subject: [PATCH] refactor(copilot): improved support for `AccessToken` and Enterprise API --- config_template.yml | 18 +++++- copilot.go | 142 ++++++++++++++++++++++++++++++++++++++++---- mods.go | 11 +++- 3 files changed, 156 insertions(+), 15 deletions(-) diff --git a/config_template.yml b/config_template.yml index a42cffae..9c670f77 100644 --- a/config_template.yml +++ b/config_template.yml @@ -94,8 +94,24 @@ apis: copilot: base-url: https://api.githubcopilot.com models: - gpt-4o: + gpt-4o-2024-05-13: + aliases: ["4o-2024", "4o", "gpt-4o"] max-input-chars: 392000 + gpt-4: + aliases: ["4"] + max-input-chars: 24500 + gpt-3.5-turbo: + aliases: ["35t"] + max-input-chars: 12250 + o1-preview-2024-09-12: + aliases: ["o1-preview", "o1p"] + max-input-chars: 128000 + o1-mini-2024-09-12: + aliases: ["o1-mini", "o1m"] + max-input-chars: 128000 + claude-3.5-sonnet: + aliases: ["claude3.5-sonnet", "sonnet-3.5", "claude-3-5-sonnet"] + max-input-chars: 680000 anthropic: base-url: https://api.anthropic.com/v1 api-key: diff --git a/copilot.go b/copilot.go index 1a172c15..010a2ddd 100644 --- a/copilot.go +++ b/copilot.go @@ -2,28 +2,148 @@ package main import ( "encoding/json" + "fmt" + "net/http" "os" "path/filepath" "runtime" + "strings" + "time" ) -func getCopilotAuthToken() (string, error) { - var sessionPath string +const ( + CopilotChatAuthURL = "https://api.github.com/copilot_internal/v2/token" + CopilotEditorVersion = "vscode/1.95.3" + CopilotUserAgent = "curl/7.81.0" // Necessay to bypass the user-agent check +) + +type CopilotAccessToken struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` + Endpoints struct { + API string `json:"api"` // Can change in Github Enterprise instances + OriginTracker string `json:"origin-tracker"` + Proxy string `json:"proxy"` + Telemetry string `json:"telemetry"` + } `json:"endpoints"` + ErrorDetails *struct { + URL string `json:"url,omitempty"` + Message string `json:"message,omitempty"` + Title string `json:"title,omitempty"` + NotificationID string `json:"notification_id,omitempty"` + } `json:"error_details,omitempty"` +} + +type CopilotHTTPClient struct { + client *http.Client + AccessToken *CopilotAccessToken +} + +func NewCopilotHTTPClient() *CopilotHTTPClient { + return &CopilotHTTPClient{ + client: &http.Client{}, + } +} + +func (c *CopilotHTTPClient) Do(req *http.Request) (*http.Response, error) { + req.Header.Set("Accept", "application/json") + req.Header.Set("Editor-Version", CopilotEditorVersion) + req.Header.Set("User-Agent", CopilotUserAgent) + + var isTokenExpired bool = c.AccessToken != nil && c.AccessToken.ExpiresAt < time.Now().Unix() + + if c.AccessToken == nil || isTokenExpired { + // Use the base http.Client for token requests to avoid recursion + accessToken, err := getCopilotAccessToken(c.client) + if err != nil { + return nil, fmt.Errorf("failed to get access token: %w", err) + } + + c.AccessToken = &accessToken + } + + return c.client.Do(req) +} + +func getCopilotRefreshToken() (string, error) { + configPath := filepath.Join(os.Getenv("HOME"), ".config/github-copilot") if runtime.GOOS == "windows" { - // C:\Users\user\AppData\Local\github-copilot\hosts.json - sessionPath = filepath.Join(os.Getenv("LOCALAPPDATA"), "github-copilot", "hosts.json") - } else { - // ~/.config/github-copilot/hosts.json - sessionPath = filepath.Join(os.Getenv("HOME"), ".config/github-copilot", "hosts.json") + configPath = filepath.Join(os.Getenv("LOCALAPPDATA"), "github-copilot") } - bts, err := os.ReadFile(sessionPath) + // Check both possible config file locations + configFiles := []string{ + filepath.Join(configPath, "hosts.json"), + filepath.Join(configPath, "apps.json"), + } + + // Try to get token from config files + for _, path := range configFiles { + token, err := extractCopilotTokenFromFile(path) + if err == nil && token != "" { + return token, nil + } + } + + return "", fmt.Errorf("no token found in %s", strings.Join(configFiles, ", ")) +} + +func extractCopilotTokenFromFile(path string) (string, error) { + bytes, err := os.ReadFile(path) if err != nil { return "", err } - hosts := map[string]map[string]string{} - if err := json.Unmarshal(bts, &hosts); err != nil { + + var config map[string]json.RawMessage + if err := json.Unmarshal(bytes, &config); err != nil { return "", err } - return hosts["github.com"]["oauth_token"], nil + + for key, value := range config { + if key == "github.com" || strings.HasPrefix(key, "github.com:") { + var tokenData map[string]string + if err := json.Unmarshal(value, &tokenData); err != nil { + continue + } + if token, exists := tokenData["oauth_token"]; exists { + return token, nil + } + } + } + + return "", fmt.Errorf("no token found in %s", path) +} + +func getCopilotAccessToken(client *http.Client) (CopilotAccessToken, error) { + refreshToken, err := getCopilotRefreshToken() + if err != nil { + return CopilotAccessToken{}, fmt.Errorf("failed to get refresh token: %w", err) + } + + tokenReq, err := http.NewRequest(http.MethodGet, CopilotChatAuthURL, nil) + if err != nil { + return CopilotAccessToken{}, fmt.Errorf("failed to create token request: %w", err) + } + + tokenReq.Header.Set("Authorization", "token "+refreshToken) + tokenReq.Header.Set("Accept", "application/json") + tokenReq.Header.Set("Editor-Version", CopilotEditorVersion) + tokenReq.Header.Set("User-Agent", CopilotUserAgent) + + tokenResp, err := client.Do(tokenReq) + if err != nil { + return CopilotAccessToken{}, fmt.Errorf("failed to get access token: %w", err) + } + defer tokenResp.Body.Close() + + var tokenResponse CopilotAccessToken + if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResponse); err != nil { + return CopilotAccessToken{}, fmt.Errorf("failed to decode token response: %w", err) + } + + if tokenResponse.ErrorDetails != nil { + return CopilotAccessToken{}, fmt.Errorf("token error: %s", tokenResponse.ErrorDetails.Message) + } + + return tokenResponse, nil } diff --git a/mods.go b/mods.go index 1a6eb991..4ba1bc5b 100644 --- a/mods.go +++ b/mods.go @@ -357,12 +357,17 @@ func (m *Mods) startCompletionCmd(content string) tea.Cmd { cfg.User = api.User } case "copilot": - token, err := getCopilotAuthToken() + copilotHttpClient := NewCopilotHTTPClient() + accessToken, err := getCopilotAccessToken(copilotHttpClient.client) if err != nil { return modsError{err, "Copilot authentication failed"} } - ccfg = openai.DefaultConfig(token) - ccfg.BaseURL = api.BaseURL + + ccfg = openai.DefaultConfig(accessToken.Token) + ccfg.HTTPClient = copilotHttpClient + ccfg.HTTPClient.(*CopilotHTTPClient).AccessToken = &accessToken + ccfg.BaseURL = ordered.First(api.BaseURL, accessToken.Endpoints.API) + default: key, err := m.ensureKey(api, "OPENAI_API_KEY", "https://platform.openai.com/account/api-keys") if err != nil {