v0.50.1 a637010

discover_custom.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"net/http"
9	"net/url"
10	"strings"
11	"time"
12)
13
14// Conservative metadata for models that a list endpoint describes by ID
15// only. Explicit models.json entries override these values.
16const (
17	customDiscoveryContextWindow = 32768
18	customDiscoveryMaxOutput     = 4096
19	customDiscoveryTimeout       = 3 * time.Second
20	customDiscoveryMaxBody       = 8 << 20
21)
22
23// customModelsURL derives the list endpoint from a custom provider base URL.
24// A base that already ends in an API version segment such as "/v1" gets
25// "/models" appended; a bare host gets "/v1/models".
26func customModelsURL(baseURL string) (string, error) {
27	u, err := url.Parse(strings.TrimSpace(baseURL))
28	if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
29		return "", fmt.Errorf("custom provider base URL must be an http or https URL")
30	}
31	u.Path = strings.TrimRight(u.Path, "/")
32	u.RawPath = ""
33	u.RawQuery = ""
34	u.Fragment = ""
35	u.User = nil
36	base := u.String()
37	if versionSegmentSuffix.MatchString(base) {
38		return base + "/models", nil
39	}
40	return base + "/v1/models", nil
41}
42
43// DiscoverCustomProvider lists models from a user-defined provider's
44// OpenAI-style /models endpoint using the process default HTTP transport, so
45// configured proxy settings apply. Pass an empty apiKey for keyless endpoints:
46// no Authorization header is sent.
47func DiscoverCustomProvider(ctx context.Context, providerID, baseURL, apiKey string) ([]Model, error) {
48	return DiscoverCustomProviderWithClient(ctx, nil, providerID, baseURL, apiKey)
49}
50
51// DiscoverCustomProviderWithClient is DiscoverCustomProvider with a caller
52// supplied HTTP client, for example one from NewHTTPClient when the user scoped
53// insecure TLS to custom endpoints. A nil client uses the default transport.
54// The request is bounded by a short timeout and an 8 MiB response cap
55// regardless of the client's own settings.
56//
57// Only IDs are trusted from the response; context and output limits use
58// conservative defaults because self-hosted gateways rarely report them
59// consistently. Entries whose ID mentions embeddings are skipped. Discovered
60// models carry the provider's base URL so they route like models.json entries.
61func DiscoverCustomProviderWithClient(ctx context.Context, client *http.Client, providerID, baseURL, apiKey string) ([]Model, error) {
62	providerID = strings.TrimSpace(providerID)
63	if providerID == "" {
64		return nil, fmt.Errorf("custom provider discovery requires a provider id")
65	}
66	endpoint, err := customModelsURL(baseURL)
67	if err != nil {
68		return nil, err
69	}
70	ctx, cancel := context.WithTimeout(ctx, customDiscoveryTimeout)
71	defer cancel()
72	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
73	if err != nil {
74		return nil, err
75	}
76	req.Header.Set("Accept", "application/json")
77	if apiKey != "" {
78		req.Header.Set("Authorization", "Bearer "+apiKey)
79	}
80	if client == nil {
81		client = &http.Client{}
82	}
83	resp, err := client.Do(req)
84	if err != nil {
85		if ctx.Err() != nil {
86			return nil, ctx.Err()
87		}
88		// The transport error can echo the URL; keep the message generic.
89		return nil, fmt.Errorf("%s discovery request failed", providerID)
90	}
91	defer resp.Body.Close()
92	if resp.StatusCode != http.StatusOK {
93		return nil, fmt.Errorf("%s discovery HTTP %d", providerID, resp.StatusCode)
94	}
95	body, err := io.ReadAll(io.LimitReader(resp.Body, customDiscoveryMaxBody+1))
96	if err != nil {
97		return nil, fmt.Errorf("%s discovery response could not be read", providerID)
98	}
99	if len(body) > customDiscoveryMaxBody {
100		return nil, fmt.Errorf("%s discovery response too large", providerID)
101	}
102	var page struct {
103		Data []struct {
104			ID string `json:"id"`
105		} `json:"data"`
106	}
107	if err := json.Unmarshal(body, &page); err != nil {
108		return nil, fmt.Errorf("%s discovery returned invalid model data", providerID)
109	}
110	if page.Data == nil {
111		return nil, fmt.Errorf("%s discovery response is missing data", providerID)
112	}
113	models := make([]Model, 0, len(page.Data))
114	seen := map[string]bool{}
115	for _, d := range page.Data {
116		id := strings.TrimSpace(d.ID)
117		if id == "" || seen[id] || strings.Contains(strings.ToLower(id), "embed") {
118			continue
119		}
120		seen[id] = true
121		models = append(models, Model{
122			Provider:      providerID,
123			ID:            id,
124			DisplayName:   id,
125			ContextWindow: customDiscoveryContextWindow,
126			MaxOutput:     customDiscoveryMaxOutput,
127			BaseURL:       strings.TrimRight(strings.TrimSpace(baseURL), "/"),
128			Source:        "live",
129		})
130	}
131	return models, nil
132}

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