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