1package auth
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
8 "strings"
9 "time"
10)
11
12// ProbeAPIKey verifies that key is valid for provider by making a
13// lightweight authenticated request. Returns nil on success.
14func ProbeAPIKey(ctx context.Context, provider, key string) error {
15 if key == "" {
16 return fmt.Errorf("empty key")
17 }
18 c := &http.Client{Timeout: 15 * time.Second}
19 var req *http.Request
20 var err error
21
22 switch provider {
23 case "anthropic":
24 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.anthropic.com/v1/models", nil)
25 if err != nil {
26 return err
27 }
28 req.Header.Set("x-api-key", key)
29 req.Header.Set("anthropic-version", "2023-06-01")
30 case "openai":
31 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.openai.com/v1/models", nil)
32 if err != nil {
33 return err
34 }
35 req.Header.Set("authorization", "Bearer "+key)
36 case "kimi":
37 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.kimi.com/coding/v1/models", nil)
38 if err != nil {
39 return err
40 }
41 req.Header.Set("authorization", "Bearer "+key)
42 case "deepseek":
43 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.deepseek.com/v1/models", nil)
44 if err != nil {
45 return err
46 }
47 req.Header.Set("authorization", "Bearer "+key)
48 case "google":
49 // Google Generative Language: list models with the API key.
50 // Accepts the key via x-goog-api-key header (preferred over
51 // the ?key= query param so it doesn't show up in proxy logs).
52 req, err = http.NewRequestWithContext(ctx, "GET", "https://generativelanguage.googleapis.com/v1beta/models", nil)
53 if err != nil {
54 return err
55 }
56 req.Header.Set("x-goog-api-key", key)
57 // OpenAI-compatible third parties: a GET /v1/models with bearer auth
58 // is enough to validate the key. Branches kept explicit (rather than a
59 // generic default) so the URL list is searchable and reviewable.
60 case "moonshotai":
61 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.moonshot.ai/v1/models", nil)
62 if err != nil {
63 return err
64 }
65 req.Header.Set("authorization", "Bearer "+key)
66 case "moonshotai-cn":
67 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.moonshot.cn/v1/models", nil)
68 if err != nil {
69 return err
70 }
71 req.Header.Set("authorization", "Bearer "+key)
72 case "groq":
73 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.groq.com/openai/v1/models", nil)
74 if err != nil {
75 return err
76 }
77 req.Header.Set("authorization", "Bearer "+key)
78 case "cerebras":
79 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.cerebras.ai/v1/models", nil)
80 if err != nil {
81 return err
82 }
83 req.Header.Set("authorization", "Bearer "+key)
84 case "xai":
85 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.x.ai/v1/models", nil)
86 if err != nil {
87 return err
88 }
89 req.Header.Set("authorization", "Bearer "+key)
90 case "together":
91 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.together.ai/v1/models", nil)
92 if err != nil {
93 return err
94 }
95 req.Header.Set("authorization", "Bearer "+key)
96 case "openrouter":
97 req, err = http.NewRequestWithContext(ctx, "GET", "https://openrouter.ai/api/v1/models", nil)
98 if err != nil {
99 return err
100 }
101 req.Header.Set("authorization", "Bearer "+key)
102 case "huggingface":
103 req, err = http.NewRequestWithContext(ctx, "GET", "https://router.huggingface.co/v1/models", nil)
104 if err != nil {
105 return err
106 }
107 req.Header.Set("authorization", "Bearer "+key)
108 case "zai":
109 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.z.ai/api/coding/paas/v4/models", nil)
110 if err != nil {
111 return err
112 }
113 req.Header.Set("authorization", "Bearer "+key)
114 case "mistral":
115 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.mistral.ai/v1/models", nil)
116 if err != nil {
117 return err
118 }
119 req.Header.Set("authorization", "Bearer "+key)
120 case "xiaomi":
121 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.xiaomimimo.com/v1/models", nil)
122 if err != nil {
123 return err
124 }
125 req.Header.Set("authorization", "Bearer "+key)
126 case "xiaomi-token-plan-ams":
127 req, err = http.NewRequestWithContext(ctx, "GET", "https://token-plan-ams.xiaomimimo.com/v1/models", nil)
128 if err != nil {
129 return err
130 }
131 req.Header.Set("authorization", "Bearer "+key)
132 case "xiaomi-token-plan-cn":
133 req, err = http.NewRequestWithContext(ctx, "GET", "https://token-plan-cn.xiaomimimo.com/v1/models", nil)
134 if err != nil {
135 return err
136 }
137 req.Header.Set("authorization", "Bearer "+key)
138 case "xiaomi-token-plan-sgp":
139 req, err = http.NewRequestWithContext(ctx, "GET", "https://token-plan-sgp.xiaomimimo.com/v1/models", nil)
140 if err != nil {
141 return err
142 }
143 req.Header.Set("authorization", "Bearer "+key)
144 case "minimax":
145 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.minimax.io/v1/models", nil)
146 if err != nil {
147 return err
148 }
149 req.Header.Set("authorization", "Bearer "+key)
150 case "minimax-cn":
151 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.minimaxi.com/v1/models", nil)
152 if err != nil {
153 return err
154 }
155 req.Header.Set("authorization", "Bearer "+key)
156 case "fireworks":
157 req, err = http.NewRequestWithContext(ctx, "GET", "https://api.fireworks.ai/inference/v1/models", nil)
158 if err != nil {
159 return err
160 }
161 req.Header.Set("authorization", "Bearer "+key)
162 case "vercel-ai-gateway":
163 req, err = http.NewRequestWithContext(ctx, "GET", "https://ai-gateway.vercel.sh/v1/models", nil)
164 if err != nil {
165 return err
166 }
167 req.Header.Set("authorization", "Bearer "+key)
168 case "opencode":
169 req, err = http.NewRequestWithContext(ctx, "GET", "https://opencode.ai/zen/v1/models", nil)
170 if err != nil {
171 return err
172 }
173 req.Header.Set("authorization", "Bearer "+key)
174 case "opencode-go":
175 req, err = http.NewRequestWithContext(ctx, "GET", "https://opencode.ai/zen/go/v1/models", nil)
176 if err != nil {
177 return err
178 }
179 req.Header.Set("authorization", "Bearer "+key)
180 case "azure-openai-responses":
181 return nil
182 case "amazon-bedrock":
183 return nil
184 case "google-vertex":
185 return nil
186 case "cloudflare-workers-ai", "cloudflare-ai-gateway":
187 return nil
188 case "github-copilot":
189 return nil
190 default:
191 return fmt.Errorf("unknown provider %q", provider)
192 }
193
194 if strings.Contains(req.URL.String(), "{CLOUDFLARE_ACCOUNT_ID}") {
195 if acct := os.Getenv("CLOUDFLARE_ACCOUNT_ID"); acct != "" {
196 u := strings.ReplaceAll(req.URL.String(), "{CLOUDFLARE_ACCOUNT_ID}", acct)
197 req.URL, _ = req.URL.Parse(u)
198 }
199 }
200 resp, err := c.Do(req)
201 if err != nil {
202 return fmt.Errorf("probe %s: %w", provider, err)
203 }
204 defer resp.Body.Close()
205 if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
206 return fmt.Errorf("%s rejected the key (http %d)", provider, resp.StatusCode)
207 }
208 if resp.StatusCode >= 400 {
209 return fmt.Errorf("%s http %d", provider, resp.StatusCode)
210 }
211 return nil
212}