main.go

  1// Package main implements a tool to fetch GitHub Copilot models and generate a Catwalk provider configuration.
  2package main
  3
  4import (
  5	"context"
  6	"encoding/json"
  7	"fmt"
  8	"io"
  9	"net/http"
 10	"os"
 11	"path/filepath"
 12	"regexp"
 13	"runtime"
 14	"slices"
 15	"strings"
 16	"time"
 17
 18	"github.com/charmbracelet/catwalk/pkg/catwalk"
 19)
 20
 21type Response struct {
 22	Object string  `json:"object"`
 23	Data   []Model `json:"data"`
 24}
 25
 26type Model struct {
 27	ID                 string     `json:"id"`
 28	Name               string     `json:"name"`
 29	Version            string     `json:"version"`
 30	Vendor             string     `json:"vendor"`
 31	Preview            bool       `json:"preview"`
 32	ModelPickerEnabled bool       `json:"model_picker_enabled"`
 33	Capabilities       Capability `json:"capabilities"`
 34	Policy             *Policy    `json:"policy,omitempty"`
 35}
 36
 37type Capability struct {
 38	Family    string   `json:"family"`
 39	Type      string   `json:"type"`
 40	Tokenizer string   `json:"tokenizer"`
 41	Limits    Limits   `json:"limits"`
 42	Supports  Supports `json:"supports"`
 43}
 44
 45type Limits struct {
 46	MaxContextWindowTokens int `json:"max_context_window_tokens,omitempty"`
 47	MaxOutputTokens        int `json:"max_output_tokens,omitempty"`
 48	MaxPromptTokens        int `json:"max_prompt_tokens,omitempty"`
 49}
 50
 51type Supports struct {
 52	ToolCalls         bool `json:"tool_calls,omitempty"`
 53	ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
 54}
 55
 56type Policy struct {
 57	State string `json:"state"`
 58	Terms string `json:"terms"`
 59}
 60
 61var versionedModelRegexp = regexp.MustCompile(`-\d{4}-\d{2}-\d{2}$`)
 62
 63func main() {
 64	if err := run(); err != nil {
 65		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
 66		os.Exit(1)
 67	}
 68}
 69
 70func run() error {
 71	copilotModels, err := fetchCopilotModels()
 72	if err != nil {
 73		return err
 74	}
 75
 76	// NOTE(@andreynering): Exclude versioned models and keep only the main version of each.
 77	copilotModels = slices.DeleteFunc(copilotModels, func(m Model) bool {
 78		return m.ID != m.Version || versionedModelRegexp.MatchString(m.ID) || strings.Contains(m.ID, "embedding")
 79	})
 80
 81	catwalkModels := modelsToCatwalk(copilotModels)
 82	slices.SortStableFunc(catwalkModels, func(a, b catwalk.Model) int {
 83		return strings.Compare(a.ID, b.ID)
 84	})
 85
 86	provider := catwalk.Provider{
 87		ID:                  catwalk.InferenceProviderCopilot,
 88		Name:                "GitHub Copilot",
 89		Models:              catwalkModels,
 90		APIEndpoint:         "https://api.githubcopilot.com",
 91		Type:                catwalk.TypeOpenAICompat,
 92		DefaultLargeModelID: "claude-sonnet-4.5",
 93		DefaultSmallModelID: "claude-haiku-4.5",
 94	}
 95	data, err := json.MarshalIndent(provider, "", "  ")
 96	if err != nil {
 97		return fmt.Errorf("unable to marshal json: %w", err)
 98	}
 99	if err := os.WriteFile("internal/providers/configs/copilot.json", data, 0o600); err != nil {
100		return fmt.Errorf("unable to write copilog.json: %w", err)
101	}
102	return nil
103}
104
105func fetchCopilotModels() ([]Model, error) {
106	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
107	defer cancel()
108
109	req, err := http.NewRequestWithContext(
110		ctx,
111		"GET",
112		"https://api.githubcopilot.com/models",
113		nil,
114	)
115	if err != nil {
116		return nil, fmt.Errorf("unable to create request: %w", err)
117	}
118	req.Header.Set("Accept", "application/json")
119	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", copilotToken()))
120
121	client := &http.Client{}
122	resp, err := client.Do(req)
123	if err != nil {
124		return nil, fmt.Errorf("unable to make http request: %w", err)
125	}
126	defer resp.Body.Close() //nolint:errcheck
127
128	if resp.StatusCode != http.StatusOK {
129		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
130	}
131
132	bts, err := io.ReadAll(resp.Body)
133	if err != nil {
134		return nil, fmt.Errorf("unable to read response body: %w", err)
135	}
136
137	// for debugging
138	_ = os.MkdirAll("tmp", 0o700)
139	_ = os.WriteFile("tmp/copilot-response.json", bts, 0o600)
140
141	var data Response
142	if err := json.Unmarshal(bts, &data); err != nil {
143		return nil, fmt.Errorf("unable to unmarshal json: %w", err)
144	}
145	return data.Data, nil
146}
147
148func modelsToCatwalk(m []Model) []catwalk.Model {
149	models := make([]catwalk.Model, 0, len(m))
150	for _, model := range m {
151		models = append(models, modelToCatwalk(model))
152	}
153	return models
154}
155
156func modelToCatwalk(m Model) catwalk.Model {
157	return catwalk.Model{
158		ID:               m.ID,
159		Name:             m.Name,
160		DefaultMaxTokens: int64(m.Capabilities.Limits.MaxOutputTokens),
161		ContextWindow:    int64(m.Capabilities.Limits.MaxContextWindowTokens),
162	}
163}
164
165func copilotToken() string {
166	if token := os.Getenv("COPILOT_TOKEN"); token != "" {
167		return token
168	}
169	return tokenFromDisk()
170}
171
172func tokenFromDisk() string {
173	data, err := os.ReadFile(tokenFilePath())
174	if err != nil {
175		return ""
176	}
177	var content map[string]struct {
178		User        string `json:"user"`
179		OAuthToken  string `json:"oauth_token"`
180		GitHubAppID string `json:"githubAppId"`
181	}
182	if err := json.Unmarshal(data, &content); err != nil {
183		return ""
184	}
185	if app, ok := content["github.com:Iv1.b507a08c87ecfe98"]; ok {
186		return app.OAuthToken
187	}
188	return ""
189}
190
191func tokenFilePath() string {
192	switch runtime.GOOS {
193	case "windows":
194		return filepath.Join(os.Getenv("LOCALAPPDATA"), "github-copilot/apps.json")
195	default:
196		return filepath.Join(os.Getenv("HOME"), ".config/github-copilot/apps.json")
197	}
198}