1package provider
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10 "time"
11)
12
13// DiscoverAnthropic lists model ids visible to key on api.anthropic.com.
14// The API returns a paginated list; we page through until has_more is false.
15func DiscoverAnthropic(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
16 if baseURL == "" {
17 baseURL = anthropicDefaultBaseURL
18 }
19 client := &http.Client{Timeout: 15 * time.Second}
20 var out []Model
21 after := ""
22 for {
23 url := strings.TrimRight(baseURL, "/") + "/v1/models?limit=1000"
24 if after != "" {
25 url += "&after_id=" + after
26 }
27 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
28 if err != nil {
29 return nil, err
30 }
31 req.Header.Set("x-api-key", apiKey)
32 req.Header.Set("anthropic-version", anthropicAPIVersion)
33 resp, err := client.Do(req)
34 if err != nil {
35 return nil, err
36 }
37 body, _ := io.ReadAll(resp.Body)
38 resp.Body.Close()
39 if resp.StatusCode != 200 {
40 return nil, fmt.Errorf("anthropic discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
41 }
42 var page struct {
43 Data []struct {
44 ID string `json:"id"`
45 DisplayName string `json:"display_name"`
46 } `json:"data"`
47 HasMore bool `json:"has_more"`
48 LastID string `json:"last_id"`
49 }
50 if err := json.Unmarshal(body, &page); err != nil {
51 return nil, fmt.Errorf("anthropic discover parse: %w", err)
52 }
53 for _, d := range page.Data {
54 out = append(out, Model{
55 Provider: "anthropic",
56 ID: d.ID,
57 DisplayName: d.DisplayName,
58 Source: "live",
59 })
60 }
61 if !page.HasMore || page.LastID == "" {
62 break
63 }
64 after = page.LastID
65 }
66 return out, nil
67}
68
69// DiscoverOpenAI lists model ids visible to key on api.openai.com.
70func DiscoverOpenAI(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
71 if baseURL == "" {
72 baseURL = openaiDefaultBaseURL
73 }
74 client := &http.Client{Timeout: 15 * time.Second}
75 url := strings.TrimRight(baseURL, "/") + "/v1/models"
76 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
77 if err != nil {
78 return nil, err
79 }
80 req.Header.Set("authorization", "Bearer "+apiKey)
81 resp, err := client.Do(req)
82 if err != nil {
83 return nil, err
84 }
85 defer resp.Body.Close()
86 body, _ := io.ReadAll(resp.Body)
87 if resp.StatusCode != 200 {
88 return nil, fmt.Errorf("openai discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
89 }
90 var page struct {
91 Data []struct {
92 ID string `json:"id"`
93 } `json:"data"`
94 }
95 if err := json.Unmarshal(body, &page); err != nil {
96 return nil, fmt.Errorf("openai discover parse: %w", err)
97 }
98 var out []Model
99 for _, d := range page.Data {
100 // Keep only chat-capable families. OpenAI's /v1/models returns
101 // everything including embeddings, TTS, DALL-E, etc.
102 if !looksLikeChatModel(d.ID) {
103 continue
104 }
105 out = append(out, Model{
106 Provider: "openai",
107 ID: d.ID,
108 DisplayName: d.ID,
109 Source: "live",
110 })
111 }
112 return out, nil
113}
114
115// looksLikeChatModel returns true for OpenAI ids that can plausibly be
116// used with the chat/completions endpoint. Errs on the side of inclusion.
117func looksLikeChatModel(id string) bool {
118 switch {
119 case strings.HasPrefix(id, "gpt-"):
120 return true
121 case strings.HasPrefix(id, "o1"):
122 return true
123 case strings.HasPrefix(id, "o3"):
124 return true
125 case strings.HasPrefix(id, "o4"):
126 return true
127 case strings.HasPrefix(id, "o5"):
128 return true
129 case strings.HasPrefix(id, "chatgpt-"):
130 return true
131 }
132 return false
133}