Add a golangci-lint config and fix its findings: the OIDC state cookie gets the Secure flag on https deployments, and pool sizes are clamped before the int32 conversion.
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
// Package copilot implements the GitHub Copilot provider. Copilot's chat API is
|
|
// OpenAI-compatible, but auth requires exchanging a GitHub token (PAT/OAuth) for
|
|
// a short-lived Copilot token, refreshed under a mutex as it nears expiry.
|
|
package copilot
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/providers"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/providers/openaicompat"
|
|
)
|
|
|
|
const (
|
|
tokenURL = "https://api.github.com/copilot_internal/v2/token" //nolint:gosec // G101: a URL, not a credential
|
|
chatBaseURL = "https://api.githubcopilot.com"
|
|
// refreshSkew refreshes the token slightly before it actually expires.
|
|
refreshSkew = time.Minute
|
|
)
|
|
|
|
type provider struct {
|
|
*openaicompat.Transport
|
|
name string
|
|
}
|
|
|
|
// New builds a Copilot provider. Credentials: {"pat": "<github token>"};
|
|
// "token" and "api_key" are accepted as aliases.
|
|
func New(cfg providers.Config) (providers.Provider, error) {
|
|
var sec struct {
|
|
PAT string `json:"pat"`
|
|
Token string `json:"token"`
|
|
APIKey string `json:"api_key"`
|
|
}
|
|
if err := providers.DecodeCredentials(cfg, &sec); err != nil {
|
|
return nil, err
|
|
}
|
|
pat := sec.PAT
|
|
if pat == "" {
|
|
pat = sec.Token
|
|
}
|
|
if pat == "" {
|
|
pat = sec.APIKey
|
|
}
|
|
if pat == "" {
|
|
return nil, errors.New("copilot: missing github token (credentials.api_key)")
|
|
}
|
|
|
|
base := cfg.BaseURL
|
|
if base == "" {
|
|
base = chatBaseURL
|
|
}
|
|
ts := &tokenSource{pat: pat, http: cfg.HTTPClient}
|
|
|
|
return &provider{
|
|
name: cfg.Name,
|
|
Transport: &openaicompat.Transport{
|
|
Provider: cfg.Name,
|
|
BaseURL: base,
|
|
HTTP: cfg.HTTPClient,
|
|
Authorize: ts.authorize,
|
|
IncludeUsage: true,
|
|
Headers: http.Header{
|
|
"Editor-Version": {"vscode/1.95.0"},
|
|
"Copilot-Integration-Id": {"vscode-chat"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *provider) Name() string { return p.name }
|
|
func (p *provider) Type() string { return "github_copilot" }
|
|
func (p *provider) Capabilities() []providers.Capability {
|
|
return []providers.Capability{providers.CapabilityChat}
|
|
}
|
|
|
|
// tokenSource exchanges a GitHub token for a Copilot session token and caches it
|
|
// until shortly before expiry.
|
|
type tokenSource struct {
|
|
pat string
|
|
http *http.Client
|
|
|
|
mu sync.Mutex
|
|
token string
|
|
exp time.Time
|
|
}
|
|
|
|
func (ts *tokenSource) authorize(ctx context.Context, req *http.Request) error {
|
|
tok, err := ts.get(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
return nil
|
|
}
|
|
|
|
func (ts *tokenSource) get(ctx context.Context) (string, error) {
|
|
ts.mu.Lock()
|
|
defer ts.mu.Unlock()
|
|
|
|
if ts.token != "" && time.Now().Before(ts.exp.Add(-refreshSkew)) {
|
|
return ts.token, nil
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "token "+ts.pat)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
client := ts.http
|
|
if client == nil {
|
|
client = providers.DefaultHTTPClient()
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("copilot token exchange: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return "", fmt.Errorf("copilot token exchange: status %d: %s", resp.StatusCode, b)
|
|
}
|
|
|
|
var out struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return "", fmt.Errorf("copilot token exchange: decode: %w", err)
|
|
}
|
|
ts.token = out.Token
|
|
ts.exp = time.Unix(out.ExpiresAt, 0)
|
|
return ts.token, nil
|
|
}
|
|
|
|
func init() { providers.Register("github_copilot", New) }
|