v0.1.90 3d7f80e

discover.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"net/http"
9	"strings"
10	"time"
11)
12
13// DiscoverAnthropic lists model ids visible to key on api.anthropic.com.
14// The API returns a paginated list; we page through until has_more is false.
15func DiscoverAnthropic(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
16	if baseURL == "" {
17		baseURL = anthropicDefaultBaseURL
18	}
19	client := &http.Client{Timeout: 15 * time.Second}
20	var out []Model
21	after := ""
22	for {
23		url := strings.TrimRight(baseURL, "/") + "/v1/models?limit=1000"
24		if after != "" {
25			url += "&after_id=" + after
26		}
27		req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
28		if err != nil {
29			return nil, err
30		}
31		req.Header.Set("x-api-key", apiKey)
32		req.Header.Set("anthropic-version", anthropicAPIVersion)
33		resp, err := client.Do(req)
34		if err != nil {
35			return nil, err
36		}
37		body, _ := io.ReadAll(resp.Body)
38		resp.Body.Close()
39		if resp.StatusCode != 200 {
40			return nil, fmt.Errorf("anthropic discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
41		}
42		var page struct {
43			Data []struct {
44				ID          string `json:"id"`
45				DisplayName string `json:"display_name"`
46			} `json:"data"`
47			HasMore bool   `json:"has_more"`
48			LastID  string `json:"last_id"`
49		}
50		if err := json.Unmarshal(body, &page); err != nil {
51			return nil, fmt.Errorf("anthropic discover parse: %w", err)
52		}
53		for _, d := range page.Data {
54			out = append(out, Model{
55				Provider:    "anthropic",
56				ID:          d.ID,
57				DisplayName: d.DisplayName,
58				Source:      "live",
59			})
60		}
61		if !page.HasMore || page.LastID == "" {
62			break
63		}
64		after = page.LastID
65	}
66	return out, nil
67}
68
69// DiscoverOpenAI lists model ids visible to key on api.openai.com.
70func DiscoverOpenAI(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
71	if baseURL == "" {
72		baseURL = openaiDefaultBaseURL
73	}
74	client := &http.Client{Timeout: 15 * time.Second}
75	url := strings.TrimRight(baseURL, "/") + "/v1/models"
76	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
77	if err != nil {
78		return nil, err
79	}
80	req.Header.Set("authorization", "Bearer "+apiKey)
81	resp, err := client.Do(req)
82	if err != nil {
83		return nil, err
84	}
85	defer resp.Body.Close()
86	body, _ := io.ReadAll(resp.Body)
87	if resp.StatusCode != 200 {
88		return nil, fmt.Errorf("openai discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
89	}
90	var page struct {
91		Data []struct {
92			ID string `json:"id"`
93		} `json:"data"`
94	}
95	if err := json.Unmarshal(body, &page); err != nil {
96		return nil, fmt.Errorf("openai discover parse: %w", err)
97	}
98	var out []Model
99	for _, d := range page.Data {
100		// Keep only chat-capable families. OpenAI's /v1/models returns
101		// everything including embeddings, TTS, DALL-E, etc.
102		if !looksLikeChatModel(d.ID) {
103			continue
104		}
105		out = append(out, Model{
106			Provider:    "openai",
107			ID:          d.ID,
108			DisplayName: d.ID,
109			Source:      "live",
110		})
111	}
112	return out, nil
113}
114
115// DiscoverGoogle lists Gemini model ids visible to key on
116// generativelanguage.googleapis.com. The API paginates with
117// nextPageToken; we follow it until exhausted.
118func DiscoverGoogle(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
119	if baseURL == "" {
120		baseURL = geminiDefaultBaseURL
121	}
122	client := &http.Client{Timeout: 15 * time.Second}
123	var out []Model
124	pageToken := ""
125	for {
126		url := strings.TrimRight(baseURL, "/") + "/v1beta/models?pageSize=200"
127		if pageToken != "" {
128			url += "&pageToken=" + pageToken
129		}
130		req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
131		if err != nil {
132			return nil, err
133		}
134		req.Header.Set("x-goog-api-key", apiKey)
135		resp, err := client.Do(req)
136		if err != nil {
137			return nil, err
138		}
139		body, _ := io.ReadAll(resp.Body)
140		resp.Body.Close()
141		if resp.StatusCode != 200 {
142			return nil, fmt.Errorf("google discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
143		}
144		var page struct {
145			Models []struct {
146				Name                       string   `json:"name"` // "models/gemini-2.5-pro"
147				DisplayName                string   `json:"displayName"`
148				SupportedGenerationMethods []string `json:"supportedGenerationMethods"`
149				InputTokenLimit            int      `json:"inputTokenLimit"`
150				OutputTokenLimit           int      `json:"outputTokenLimit"`
151			} `json:"models"`
152			NextPageToken string `json:"nextPageToken"`
153		}
154		if err := json.Unmarshal(body, &page); err != nil {
155			return nil, fmt.Errorf("google discover parse: %w", err)
156		}
157		for _, m := range page.Models {
158			// Strip the "models/" prefix Gemini uses on resource names.
159			id := strings.TrimPrefix(m.Name, "models/")
160			if !looksLikeGeminiChatModel(id, m.SupportedGenerationMethods) {
161				continue
162			}
163			display := m.DisplayName
164			if display == "" {
165				display = id
166			}
167			out = append(out, Model{
168				Provider:      "google",
169				ID:            id,
170				DisplayName:   display,
171				ContextWindow: m.InputTokenLimit,
172				MaxOutput:     m.OutputTokenLimit,
173				Source:        "live",
174			})
175		}
176		if page.NextPageToken == "" {
177			break
178		}
179		pageToken = page.NextPageToken
180	}
181	return out, nil
182}
183
184// looksLikeGeminiChatModel filters Gemini's /v1beta/models output
185// down to entries usable with streamGenerateContent. The API also
186// lists embedding models, AQA, and other non-chat artefacts.
187func looksLikeGeminiChatModel(id string, methods []string) bool {
188	if !strings.HasPrefix(id, "gemini-") && !strings.HasPrefix(id, "gemma-") {
189		return false
190	}
191	if strings.Contains(id, "embedding") || strings.Contains(id, "aqa") {
192		return false
193	}
194	if len(methods) > 0 {
195		ok := false
196		for _, m := range methods {
197			if m == "generateContent" || m == "streamGenerateContent" {
198				ok = true
199				break
200			}
201		}
202		if !ok {
203			return false
204		}
205	}
206	return true
207}
208
209// looksLikeChatModel returns true for OpenAI ids that can plausibly be
210// used with the chat/completions endpoint. Errs on the side of inclusion.
211func looksLikeChatModel(id string) bool {
212	switch {
213	case strings.HasPrefix(id, "gpt-"):
214		return true
215	case strings.HasPrefix(id, "o1"):
216		return true
217	case strings.HasPrefix(id, "o3"):
218		return true
219	case strings.HasPrefix(id, "o4"):
220		return true
221	case strings.HasPrefix(id, "o5"):
222		return true
223	case strings.HasPrefix(id, "chatgpt-"):
224		return true
225	}
226	return false
227}

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