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}