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 // Exclude versioned/duplicate models and keep only the main version of each.
94 aliasedVersions := make(map[string]bool, len(copilotModels))
95 for _, m := range copilotModels {
96 if m.ID != m.Version {
97 aliasedVersions[m.Version] = true
98 }
99 }
100
101 copilotModels = slices.DeleteFunc(copilotModels, func(m Model) bool {
102 return aliasedVersions[m.ID] || versionedModelRegexp.MatchString(m.ID) || strings.Contains(m.ID, "embedding") || strings.HasPrefix(m.ID, "accounts/msft/routers") || strings.HasPrefix(m.ID, "oswe-vscode") || m.ID == "gpt-4-o-preview"
103 })
104
105 // Deduplicate by ID (API can return duplicates)
106 seen := make(map[string]bool, len(copilotModels))
107 copilotModels = slices.DeleteFunc(copilotModels, func(m Model) bool {
108 if seen[m.ID] {
109 return true
110 }
111 seen[m.ID] = true
112 return false
113 })
114
115 catwalkModels := modelsToCatwalk(copilotModels)
116 slices.SortStableFunc(catwalkModels, func(a, b catwalk.Model) int {
117 return strings.Compare(a.ID, b.ID)
118 })
119
120 provider := catwalk.Provider{
121 ID: catwalk.InferenceProviderCopilot,
122 Name: "GitHub Copilot",
123 Models: catwalkModels,
124 APIEndpoint: "https://api.githubcopilot.com",
125 Type: catwalk.TypeOpenAICompat,
126 DefaultLargeModelID: "claude-sonnet-4.6",
127 DefaultSmallModelID: "claude-haiku-4.5",
128 }
129 data, err := json.MarshalIndent(provider, "", " ")
130 if err != nil {
131 return fmt.Errorf("unable to marshal json: %w", err)
132 }
133 data = append(data, '\n')
134 if err := os.WriteFile("internal/providers/configs/copilot.json", data, 0o600); err != nil {
135 return fmt.Errorf("unable to write copilog.json: %w", err)
136 }
137 return nil
138}
139
140func fetchCopilotModels() ([]Model, error) {
141 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
142 defer cancel()
143
144 oauthToken := copilotToken()
145 if oauthToken == "" {
146 return nil, fmt.Errorf("no OAuth token available")
147 }
148
149 // Step 1: Fetch API token from the token endpoint
150 tokenURL := "https://api.github.com/copilot_internal/v2/token" //nolint:gosec
151 tokenReq, err := http.NewRequestWithContext(ctx, "GET", tokenURL, nil)
152 if err != nil {
153 return nil, fmt.Errorf("unable to create token request: %w", err)
154 }
155 tokenReq.Header.Set("Accept", "application/json")
156 tokenReq.Header.Set("Authorization", fmt.Sprintf("token %s", oauthToken))
157
158 // Use approved integration ID to bypass client check
159 tokenReq.Header.Set("Copilot-Integration-Id", "vscode-chat")
160 tokenReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1")
161
162 client := &http.Client{}
163 tokenResp, err := client.Do(tokenReq)
164 if err != nil {
165 return nil, fmt.Errorf("unable to make token request: %w", err)
166 }
167 defer tokenResp.Body.Close() //nolint:errcheck
168
169 tokenBody, err := io.ReadAll(tokenResp.Body)
170 if err != nil {
171 return nil, fmt.Errorf("unable to read token response body: %w", err)
172 }
173
174 if tokenResp.StatusCode != http.StatusOK {
175 return nil, fmt.Errorf("unexpected status code from token endpoint: %d", tokenResp.StatusCode)
176 }
177
178 var tokenData APITokenResponse
179 if err := json.Unmarshal(tokenBody, &tokenData); err != nil {
180 return nil, fmt.Errorf("unable to unmarshal token response: %w", err)
181 }
182
183 // Convert to APIToken
184 expiresAt := time.Unix(tokenData.ExpiresAt, 0)
185 apiToken := APIToken{
186 APIKey: tokenData.Token,
187 ExpiresAt: expiresAt,
188 APIEndpoint: tokenData.Endpoints.API,
189 }
190
191 // Step 2: Use the dynamic endpoint from the token to fetch models
192 modelsURL := apiToken.APIEndpoint + "/models"
193 modelsReq, err := http.NewRequestWithContext(ctx, "GET", modelsURL, nil)
194 if err != nil {
195 return nil, fmt.Errorf("unable to create models request: %w", err)
196 }
197 modelsReq.Header.Set("Accept", "application/json")
198 modelsReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken.APIKey))
199 modelsReq.Header.Set("Copilot-Integration-Id", "vscode-chat")
200 modelsReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1")
201
202 modelsResp, err := client.Do(modelsReq)
203 if err != nil {
204 return nil, fmt.Errorf("unable to make models request: %w", err)
205 }
206 defer modelsResp.Body.Close() //nolint:errcheck
207
208 modelsBody, err := io.ReadAll(modelsResp.Body)
209 if err != nil {
210 return nil, fmt.Errorf("unable to read models response body: %w", err)
211 }
212
213 if modelsResp.StatusCode != http.StatusOK {
214 return nil, fmt.Errorf("unexpected status code from models endpoint: %d", modelsResp.StatusCode)
215 }
216
217 // for debugging
218 _ = os.MkdirAll("tmp", 0o700)
219 _ = os.WriteFile("tmp/copilot-response.json", modelsBody, 0o600)
220
221 var data Response
222 if err := json.Unmarshal(modelsBody, &data); err != nil {
223 return nil, fmt.Errorf("unable to unmarshal json: %w", err)
224 }
225 return data.Data, nil
226}
227
228func modelsToCatwalk(m []Model) []catwalk.Model {
229 models := make([]catwalk.Model, 0, len(m))
230 for _, model := range m {
231 models = append(models, modelToCatwalk(model))
232 }
233 return models
234}
235
236func modelToCatwalk(m Model) catwalk.Model {
237 return catwalk.Model{
238 ID: m.ID,
239 Name: m.Name,
240 DefaultMaxTokens: int64(m.Capabilities.Limits.MaxOutputTokens),
241 ContextWindow: int64(m.Capabilities.Limits.MaxContextWindowTokens),
242 SupportsImages: m.Capabilities.Supports.Vision,
243 }
244}
245
246func copilotToken() string {
247 if token := os.Getenv("COPILOT_TOKEN"); token != "" {
248 return token
249 }
250 return tokenFromDisk()
251}
252
253func tokenFromDisk() string {
254 data, err := os.ReadFile(tokenFilePath())
255 if err != nil {
256 return ""
257 }
258 var content map[string]struct {
259 User string `json:"user"`
260 OAuthToken string `json:"oauth_token"`
261 GitHubAppID string `json:"githubAppId"`
262 }
263 if err := json.Unmarshal(data, &content); err != nil {
264 return ""
265 }
266 if app, ok := content["github.com:Iv1.b507a08c87ecfe98"]; ok {
267 return app.OAuthToken
268 }
269 if app, ok := content["github.com:Iv1.b507a08c87ecfe98:zed"]; ok {
270 return app.OAuthToken
271 }
272 return ""
273}
274
275func tokenFilePath() string {
276 switch runtime.GOOS {
277 case "windows":
278 return filepath.Join(os.Getenv("LOCALAPPDATA"), "github-copilot/apps.json")
279 default:
280 return filepath.Join(os.Getenv("HOME"), ".config/github-copilot/apps.json")
281 }
282}