Skip to content

Commit

Permalink
refactor(copilot): improved support for AccessToken and Enterprise API
Browse files Browse the repository at this point in the history
  • Loading branch information
nathabonfim59 committed Dec 15, 2024
1 parent 544f548 commit c3f73e3
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 15 deletions.
18 changes: 17 additions & 1 deletion config_template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
142 changes: 131 additions & 11 deletions copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Check failure on line 15 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported const CopilotChatAuthURL should have comment (or a comment on this block) or be unexported (revive)
CopilotEditorVersion = "vscode/1.95.3"
CopilotUserAgent = "curl/7.81.0" // Necessay to bypass the user-agent check
)

type CopilotAccessToken struct {

Check failure on line 20 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type CopilotAccessToken should have comment or be unexported (revive)
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 {

Check failure on line 37 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type CopilotHTTPClient should have comment or be unexported (revive)
client *http.Client
AccessToken *CopilotAccessToken
}

func NewCopilotHTTPClient() *CopilotHTTPClient {

Check failure on line 42 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported function NewCopilotHTTPClient should have comment or be unexported (revive)
return &CopilotHTTPClient{
client: &http.Client{},
}
}

func (c *CopilotHTTPClient) Do(req *http.Request) (*http.Response, error) {

Check failure on line 48 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method CopilotHTTPClient.Do should have comment or be unexported (revive)
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()

Check failure on line 53 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

var-declaration: should omit type bool from declaration of var isTokenExpired; it will be inferred from the right-hand side (revive)

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)

Check failure on line 65 in copilot.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func (*net/http.Client).Do(req *net/http.Request) (*net/http.Response, error) (wrapcheck)
}

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)

Check failure on line 123 in copilot.go

View workflow job for this annotation

GitHub Actions / lint-soft

should rewrite http.NewRequestWithContext or add (*Request).WithContext (noctx)
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()

Check failure on line 137 in copilot.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `tokenResp.Body.Close` is not checked (errcheck)

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
}
11 changes: 8 additions & 3 deletions mods.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,17 @@ func (m *Mods) startCompletionCmd(content string) tea.Cmd {
cfg.User = api.User
}
case "copilot":
token, err := getCopilotAuthToken()
copilotHttpClient := NewCopilotHTTPClient()

Check failure on line 360 in mods.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var copilotHttpClient should be copilotHTTPClient (revive)
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 {
Expand Down

0 comments on commit c3f73e3

Please sign in to comment.