v0.3.91 9a5c565

probe.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close