v0.50.0 c4ec31f

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

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 477

↑↓ moveenter openesc close