main 4509ec7

customdiscovery.go RawHistoryFolder

1package agent
2
3import (
4	"context"
5	"errors"
6	"net/http"
7	"os"
8	"sort"
9	"strings"
10	"time"
11
12	"github.com/bnema/zut/packages/provider"
13)
14
15// discoverableCustomProviders returns the models.json providers that opted
16// into live discovery, sorted for deterministic refresh order.
17func discoverableCustomProviders() []string {
18	var out []string
19	for name := range provider.CustomProviders() {
20		if isDiscoverableCustomProvider(name) {
21			out = append(out, name)
22		}
23	}
24	sort.Strings(out)
25	return out
26}
27
28// isDiscoverableCustomProvider reports whether name is a custom provider
29// with live discovery enabled. Its catalog entries are transient, so config
30// repair must not treat a missing model as stale.
31func isDiscoverableCustomProvider(name string) bool {
32	cfg, ok := provider.CustomProviders()[name]
33	return ok && cfg.Discover && strings.TrimSpace(cfg.BaseURL) != "" && !isBuiltinProvider(name)
34}
35
36// CustomDiscoveryConfigured reports whether any custom provider has live
37// discovery enabled. It reads only loaded configuration, never the network.
38func CustomDiscoveryConfigured() bool {
39	return len(discoverableCustomProviders()) > 0
40}
41
42// customProviderKey resolves the optional bearer key for a custom provider.
43// A missing key is not an error because local servers commonly accept
44// unauthenticated requests. skip is true when the only key is command-backed
45// and mode forbids running the command. A provider configured with
46// auth "none" never consults the environment, auth.json, or key commands.
47func customProviderKey(ctx context.Context, name string, mode apiKeyCommandMode) (key string, skip bool, err error) {
48	if cfg, ok := provider.CustomProviders()[name]; ok && cfg.NoAuth && !isBuiltinProvider(name) {
49		return "", false, nil
50	}
51	if v := os.Getenv(normalizeCustomProviderEnvVar(name) + "_API_KEY"); v != "" {
52		return v, false, nil
53	}
54	creds, err := AuthStoreFor().Load()
55	if err != nil {
56		return "", false, err
57	}
58	stored, ok := creds.AdditionalAPIKeyCreds[name]
59	if !ok {
60		return "", false, nil
61	}
62	if stored.APIKey == "" && stored.APIKeyCommand != nil && mode == apiKeyCommandSkip {
63		return "", true, nil
64	}
65	key, _, err = resolveStoredAPIKey(ctx, name, stored, mode)
66	return key, false, err
67}
68
69// customDiscoveryHTTPClient keeps normal TLS verification for discovery,
70// independent of the inference-only insecure setting, and honors proxy envs.
71func customDiscoveryHTTPClient() *http.Client {
72	return provider.NewHTTPClient(false)
73}
74
75// RefreshCustomProviderModels lists models from every discovery-enabled
76// custom provider and merges them into the transient managed catalog.
77// A failing provider keeps its previous snapshot; errors are joined after
78// all providers were attempted. It may run a stored api-key command, so
79// only call it for an explicit user action or model launch.
80func RefreshCustomProviderModels(ctx context.Context) error {
81	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
82	defer cancel()
83	return refreshCustomProviderModels(ctx, apiKeyCommandExecute)
84}
85
86// refreshCustomProvidersBackground is the startup variant: it never runs an
87// api-key command and skips providers whose only key is command-backed.
88func refreshCustomProvidersBackground() {
89	if !CustomDiscoveryConfigured() {
90		return
91	}
92	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
93	defer cancel()
94	_ = refreshCustomProviderModels(ctx, apiKeyCommandSkip)
95}
96
97func refreshCustomProviderModels(ctx context.Context, mode apiKeyCommandMode) error {
98	var errs []error
99	var client *http.Client
100	for _, name := range discoverableCustomProviders() {
101		if client == nil {
102			client = customDiscoveryHTTPClient()
103		}
104		if err := refreshCustomProviderModelWithClient(ctx, client, name, mode); err != nil {
105			errs = append(errs, err)
106		}
107	}
108	return errors.Join(errs...)
109}
110
111// refreshCustomProviderModel refreshes exactly one discovery-enabled provider.
112// Other providers' endpoints and key commands are never touched. A provider
113// that did not opt in is a no-op.
114func refreshCustomProviderModel(ctx context.Context, name string, mode apiKeyCommandMode) error {
115	if !isDiscoverableCustomProvider(name) {
116		return nil
117	}
118	return refreshCustomProviderModelWithClient(ctx, customDiscoveryHTTPClient(), name, mode)
119}
120
121func refreshCustomProviderModelWithClient(ctx context.Context, client *http.Client, name string, mode apiKeyCommandMode) error {
122	cfg := provider.CustomProviders()[name]
123	key, skip, err := customProviderKey(ctx, name, mode)
124	if err != nil || skip {
125		return err
126	}
127	models, err := provider.DiscoverCustomProviderWithClient(ctx, client, name, cfg.BaseURL, key)
128	if err != nil {
129		return err // keep the previous snapshot
130	}
131	provider.SetManagedModelsForProvider(name, models)
132	return nil
133}

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