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 "charm.land/catwalk/pkg/catwalk"
19)
20
21type Response struct {
22 Object string `json:"object"`
23 Data []Model `json:"data"`
24}
25
26type APITokenResponse struct {
27 Token string `json:"token"`
28 ExpiresAt int64 `json:"expires_at"`
29 Endpoints APITokenResponseEndpoints `json:"endpoints"`
30}
31
32type APITokenResponseEndpoints struct {
33 API string `json:"api"`
34}
35
36type APIToken struct {
37 APIKey string
38 ExpiresAt time.Time
39 APIEndpoint string
40}
41
42type Model struct {
43 ID string `json:"id"`
44 Name string `json:"name"`
45 Version string `json:"version"`
46 Vendor string `json:"vendor"`
47 Preview bool `json:"preview"`
48 ModelPickerEnabled bool `json:"model_picker_enabled"`
49 Capabilities Capability `json:"capabilities"`
50 Policy *Policy `json:"policy,omitempty"`
51}
52
53type Capability struct {
54 Family string `json:"family"`
55 Type string `json:"type"`
56 Tokenizer string `json:"tokenizer"`
57 Limits Limits `json:"limits"`
58 Supports Supports `json:"supports"`
59}
60
61type Limits struct {
62 MaxContextWindowTokens int `json:"max_context_window_tokens,omitempty"`
63 MaxOutputTokens int `json:"max_output_tokens,omitempty"`
64 MaxPromptTokens int `json:"max_prompt_tokens,omitempty"`
65}
66
67type Supports struct {
68 ToolCalls bool `json:"tool_calls,omitempty"`
69 ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
70 Vision bool `json:"vision,omitempty"`
71}
72
73type Policy struct {
74 State string `json:"state"`
75 Terms string `json:"terms"`
76}
77
78var versionedModelRegexp = regexp.MustCompile(`-\d{4}-\d{2}-\d{2}$`)
79
80func main() {
81 if err := run(); err != nil {
82 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
83 os.Exit(1)
84 }
85}
86
87func run() error {
88 copilotModels, err := fetchCopilotModels()
89 if err != nil {
90 return err
91 }
92
93 // NOTE(@andreynering): Exclude versioned models and keep only the main version of each.
94 copilotModels = slices.DeleteFunc(copilotModels, func(m Model) bool {
95 return m.ID != m.Version || versionedModelRegexp.MatchString(m.ID) || strings.Contains(m.ID, "embedding")
96 })
97
98 catwalkModels := modelsToCatwalk(copilotModels)
99 slices.SortStableFunc(catwalkModels, func(a, b catwalk.Model) int {
100 return strings.Compare(a.ID, b.ID)
101 })
102
103 provider := catwalk.Provider{
104 ID: catwalk.InferenceProviderCopilot,
105 Name: "GitHub Copilot",
106 Models: catwalkModels,
107 APIEndpoint: "https://api.githubcopilot.com",
108 Type: catwalk.TypeOpenAICompat,
109 DefaultLargeModelID: "claude-sonnet-4.5",
110 DefaultSmallModelID: "claude-haiku-4.5",
111 }
112 data, err := json.MarshalIndent(provider, "", " ")
113 if err != nil {
114 return fmt.Errorf("unable to marshal json: %w", err)
115 }
116 data = append(data, '\n')
117 if err := os.WriteFile("internal/providers/configs/copilot.json", data, 0o600); err != nil {
118 return fmt.Errorf("unable to write copilog.json: %w", err)
119 }
120 return nil
121}
122
123func fetchCopilotModels() ([]Model, error) {
124 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
125 defer cancel()
126
127 oauthToken := copilotToken()
128 if oauthToken == "" {
129 return nil, fmt.Errorf("no OAuth token available")
130 }
131
132 // Step 1: Fetch API token from the token endpoint
133 tokenURL := "https://api.github.com/copilot_internal/v2/token" //nolint:gosec
134 tokenReq, err := http.NewRequestWithContext(ctx, "GET", tokenURL, nil)
135 if err != nil {
136 return nil, fmt.Errorf("unable to create token request: %w", err)
137 }
138 tokenReq.Header.Set("Accept", "application/json")
139 tokenReq.Header.Set("Authorization", fmt.Sprintf("token %s", oauthToken))
140
141 // Use approved integration ID to bypass client check
142 tokenReq.Header.Set("Copilot-Integration-Id", "vscode-chat")
143 tokenReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1")
144
145 client := &http.Client{}
146 tokenResp, err := client.Do(tokenReq)
147 if err != nil {
148 return nil, fmt.Errorf("unable to make token request: %w", err)
149 }
150 defer tokenResp.Body.Close() //nolint:errcheck
151
152 tokenBody, err := io.ReadAll(tokenResp.Body)
153 if err != nil {
154 return nil, fmt.Errorf("unable to read token response body: %w", err)
155 }
156
157 if tokenResp.StatusCode != http.StatusOK {
158 return nil, fmt.Errorf("unexpected status code from token endpoint: %d", tokenResp.StatusCode)
159 }
160
161 var tokenData APITokenResponse
162 if err := json.Unmarshal(tokenBody, &tokenData); err != nil {
163 return nil, fmt.Errorf("unable to unmarshal token response: %w", err)
164 }
165
166 // Convert to APIToken
167 expiresAt := time.Unix(tokenData.ExpiresAt, 0)
168 apiToken := APIToken{
169 APIKey: tokenData.Token,
170 ExpiresAt: expiresAt,
171 APIEndpoint: tokenData.Endpoints.API,
172 }
173
174 // Step 2: Use the dynamic endpoint from the token to fetch models
175 modelsURL := apiToken.APIEndpoint + "/models"
176 modelsReq, err := http.NewRequestWithContext(ctx, "GET", modelsURL, nil)
177 if err != nil {
178 return nil, fmt.Errorf("unable to create models request: %w", err)
179 }
180 modelsReq.Header.Set("Accept", "application/json")
181 modelsReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken.APIKey))
182 modelsReq.Header.Set("Copilot-Integration-Id", "vscode-chat")
183 modelsReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1")
184
185 modelsResp, err := client.Do(modelsReq)
186 if err != nil {
187 return nil, fmt.Errorf("unable to make models request: %w", err)
188 }
189 defer modelsResp.Body.Close() //nolint:errcheck
190
191 modelsBody, err := io.ReadAll(modelsResp.Body)
192 if err != nil {
193 return nil, fmt.Errorf("unable to read models response body: %w", err)
194 }
195
196 if modelsResp.StatusCode != http.StatusOK {
197 return nil, fmt.Errorf("unexpected status code from models endpoint: %d", modelsResp.StatusCode)
198 }
199
200 // for debugging
201 _ = os.MkdirAll("tmp", 0o700)
202 _ = os.WriteFile("tmp/copilot-response.json", modelsBody, 0o600)
203
204 var data Response
205 if err := json.Unmarshal(modelsBody, &data); err != nil {
206 return nil, fmt.Errorf("unable to unmarshal json: %w", err)
207 }
208 return data.Data, nil
209}
210
211func modelsToCatwalk(m []Model) []catwalk.Model {
212 models := make([]catwalk.Model, 0, len(m))
213 for _, model := range m {
214 models = append(models, modelToCatwalk(model))
215 }
216 return models
217}
218
219func modelToCatwalk(m Model) catwalk.Model {
220 return catwalk.Model{
221 ID: m.ID,
222 Name: m.Name,
223 DefaultMaxTokens: int64(m.Capabilities.Limits.MaxOutputTokens),
224 ContextWindow: int64(m.Capabilities.Limits.MaxContextWindowTokens),
225 SupportsImages: m.Capabilities.Supports.Vision,
226 }
227}
228
229func copilotToken() string {
230 if token := os.Getenv("COPILOT_TOKEN"); token != "" {
231 return token
232 }
233 return tokenFromDisk()
234}
235
236func tokenFromDisk() string {
237 data, err := os.ReadFile(tokenFilePath())
238 if err != nil {
239 return ""
240 }
241 var content map[string]struct {
242 User string `json:"user"`
243 OAuthToken string `json:"oauth_token"`
244 GitHubAppID string `json:"githubAppId"`
245 }
246 if err := json.Unmarshal(data, &content); err != nil {
247 return ""
248 }
249 if app, ok := content["github.com:Iv1.b507a08c87ecfe98"]; ok {
250 return app.OAuthToken
251 }
252 return ""
253}
254
255func tokenFilePath() string {
256 switch runtime.GOOS {
257 case "windows":
258 return filepath.Join(os.Getenv("LOCALAPPDATA"), "github-copilot/apps.json")
259 default:
260 return filepath.Join(os.Getenv("HOME"), ".config/github-copilot/apps.json")
261 }
262}