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}