package provider import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // DiscoverAnthropic lists model ids visible to key on api.anthropic.com. // The API returns a paginated list; we page through until has_more is false. func DiscoverAnthropic(ctx context.Context, apiKey, baseURL string) ([]Model, error) { if baseURL == "" { baseURL = anthropicDefaultBaseURL } client := &http.Client{Timeout: 15 * time.Second} var out []Model after := "" for { url := strings.TrimRight(baseURL, "/") + "/v1/models?limit=1000" if after != "" { url += "&after_id=" + after } req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } req.Header.Set("x-api-key", apiKey) req.Header.Set("anthropic-version", anthropicAPIVersion) resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("anthropic discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var page struct { Data []struct { ID string `json:"id"` DisplayName string `json:"display_name"` } `json:"data"` HasMore bool `json:"has_more"` LastID string `json:"last_id"` } if err := json.Unmarshal(body, &page); err != nil { return nil, fmt.Errorf("anthropic discover parse: %w", err) } for _, d := range page.Data { out = append(out, Model{ Provider: "anthropic", ID: d.ID, DisplayName: d.DisplayName, Source: "live", }) } if !page.HasMore || page.LastID == "" { break } after = page.LastID } return out, nil } // DiscoverOpenAI lists model ids visible to key on api.openai.com. func DiscoverOpenAI(ctx context.Context, apiKey, baseURL string) ([]Model, error) { if baseURL == "" { baseURL = openaiDefaultBaseURL } client := &http.Client{Timeout: 15 * time.Second} url := strings.TrimRight(baseURL, "/") + "/v1/models" req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } req.Header.Set("authorization", "Bearer "+apiKey) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return nil, fmt.Errorf("openai discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var page struct { Data []struct { ID string `json:"id"` } `json:"data"` } if err := json.Unmarshal(body, &page); err != nil { return nil, fmt.Errorf("openai discover parse: %w", err) } var out []Model for _, d := range page.Data { // Keep only chat-capable families. OpenAI's /v1/models returns // everything including embeddings, TTS, DALL-E, etc. if !looksLikeChatModel(d.ID) { continue } out = append(out, Model{ Provider: "openai", ID: d.ID, DisplayName: d.ID, Source: "live", }) } return out, nil } // looksLikeChatModel returns true for OpenAI ids that can plausibly be // used with the chat/completions endpoint. Errs on the side of inclusion. func looksLikeChatModel(id string) bool { switch { case strings.HasPrefix(id, "gpt-"): return true case strings.HasPrefix(id, "o1"): return true case strings.HasPrefix(id, "o3"): return true case strings.HasPrefix(id, "o4"): return true case strings.HasPrefix(id, "o5"): return true case strings.HasPrefix(id, "chatgpt-"): return true } return false }