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}