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