1package provider
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9)
10
11// DiscoverCopilotAvailableModels lists the account's selectable tool-capable
12// models. It does not enable model policies or persist account-specific data.
13func DiscoverCopilotAvailableModels(ctx context.Context, pat string) ([]string, error) {
14 token, err := copilotCache.get(ctx, pat)
15 if err != nil {
16 return nil, err
17 }
18 baseURL := token.baseURL
19 if baseURL == "" {
20 baseURL = copilotDefaultBaseURL
21 }
22 client := &http.Client{Transport: &copilotRefreshTransport{inner: http.DefaultTransport, pat: pat}}
23 req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/models", nil)
24 if err != nil {
25 return nil, err
26 }
27 req.Header.Set("Accept", "application/json")
28 req.Header.Set("X-GitHub-Api-Version", "2026-06-01")
29 resp, err := client.Do(req)
30 if err != nil {
31 return nil, fmt.Errorf("copilot model discovery: %w", err)
32 }
33 defer resp.Body.Close()
34 if resp.StatusCode != http.StatusOK {
35 return nil, fmt.Errorf("copilot model discovery: HTTP %d", resp.StatusCode)
36 }
37 body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
38 if err != nil {
39 return nil, fmt.Errorf("copilot model discovery: %w", err)
40 }
41 // The fallback is deliberately restricted to the individual endpoint.
42 individual := baseURL == copilotDefaultBaseURL
43 return parseCopilotAvailableModels(body, individual)
44}
45
46func parseCopilotAvailableModels(body []byte, allowPolicyFallback bool) ([]string, error) {
47 var catalog struct {
48 Data []struct {
49 ID string `json:"id"`
50 Picker bool `json:"model_picker_enabled"`
51 Policy struct {
52 State string `json:"state"`
53 } `json:"policy"`
54 Capabilities struct {
55 Supports struct {
56 ToolCalls *bool `json:"tool_calls"`
57 } `json:"supports"`
58 } `json:"capabilities"`
59 } `json:"data"`
60 }
61 if err := json.Unmarshal(body, &catalog); err != nil || catalog.Data == nil {
62 return nil, fmt.Errorf("copilot model discovery: invalid models response")
63 }
64 picker := make([]string, 0)
65 enabled := make([]string, 0)
66 seen := make(map[string]bool)
67 for _, m := range catalog.Data {
68 if m.ID == "" || seen[m.ID] || (m.Capabilities.Supports.ToolCalls != nil && !*m.Capabilities.Supports.ToolCalls) {
69 continue
70 }
71 seen[m.ID] = true
72 if m.Picker && m.Policy.State != "disabled" {
73 picker = append(picker, m.ID)
74 }
75 if m.Policy.State == "enabled" {
76 enabled = append(enabled, m.ID)
77 }
78 }
79 if len(picker) == 0 && allowPolicyFallback {
80 return enabled, nil
81 }
82 return picker, nil
83}