v0.1.95 5293277

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 "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}

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