on main help sign in
feat: add ability to disable providers auto-update from catwalk
commit230e4de83c23292ab42598dbffa327405bcb3c42 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 10 Sep 2025, 17:44 (1 year ago) parentc7d3335
9 files changed+116−62
internal/config/config.go+8−7 internal/config/load.go+10−5 internal/config/provider.go+80−40 internal/config/provider_empty_test.go+3−3 internal/config/provider_test.go+3−3 internal/tui/components/chat/splash/splash.go+2−1 internal/tui/components/dialogs/models/list.go+3−2 internal/tui/components/dialogs/models/models.go+2−1 schema.json+5
internal/config/config.go+8−7
@@ -137,13 +137,14 @@ type Permissions struct {
137137}
138138
139139type Options struct {
140− ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
141− TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
142− Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
143− DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
144− DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
145− DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
146− DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
140+ ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
141+ TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
142+ Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
143+ DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
144+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
145+ DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
146+ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
147+ DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
147148}
148149
149150type MCPs map[string]MCPConfig
internal/config/load.go+10−5
@@ -10,6 +10,7 @@ import (
1010 "path/filepath"
1111 "runtime"
1212 "slices"
13+ "strconv"
1314 "strings"
1415
1516 "github.com/charmbracelet/catwalk/pkg/catwalk"
@@ -66,9 +67,9 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
6667 )
6768
6869 // Load known providers, this loads the config from catwalk
69− providers, err := Providers()
70− if err != nil || len(providers) == 0 {
71− return nil, fmt.Errorf("failed to load providers: %w", err)
70+ providers, err := Providers(cfg)
71+ if err != nil {
72+ return nil, err
7273 }
7374 cfg.knownProviders = providers
7475
@@ -76,7 +77,7 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
7677 // Configure providers
7778 valueResolver := NewShellVariableResolver(env)
7879 cfg.resolver = valueResolver
79− if err := cfg.configureProviders(env, valueResolver, providers); err != nil {
80+ if err := cfg.configureProviders(env, valueResolver, cfg.knownProviders); err != nil {
8081 return nil, fmt.Errorf("failed to configure providers: %w", err)
8182 }
8283
@@ -85,7 +86,7 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
8586 return cfg, nil
8687 }
8788
88− if err := cfg.configureSelectedModels(providers); err != nil {
89+ if err := cfg.configureSelectedModels(cfg.knownProviders); err != nil {
8990 return nil, fmt.Errorf("failed to configure selected models: %w", err)
9091 }
9192 cfg.SetupAgents()
@@ -340,6 +341,10 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
340341 c.Options.ContextPaths = append(defaultContextPaths, c.Options.ContextPaths...)
341342 slices.Sort(c.Options.ContextPaths)
342343 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
344+
345+ if str, ok := os.LookupEnv("CRUSH_DISABLE_PROVIDER_AUTO_UPDATE"); ok {
346+ c.Options.DisableProviderAutoUpdate, _ = strconv.ParseBool(str)
347+ }
343348}
344349
345350var defaultLSPFileTypes = map[string][]string{
internal/config/provider.go+80−40
@@ -12,6 +12,7 @@ import (
1212 "time"
1313
1414 "github.com/charmbracelet/catwalk/pkg/catwalk"
15+ "github.com/charmbracelet/catwalk/pkg/embedded"
1516 "github.com/charmbracelet/crush/internal/home"
1617)
1718
@@ -22,6 +23,7 @@ type ProviderClient interface {
2223var (
2324 providerOnce sync.Once
2425 providerList []catwalk.Provider
26+ providerErr error
2527)
2628
2729// file to cache provider data
@@ -75,55 +77,93 @@ func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
7577 return providers, nil
7678}
7779
78−func Providers() ([]catwalk.Provider, error) {
79− catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
80− client := catwalk.NewWithURL(catwalkURL)
81− path := providerCacheFileData()
82− return loadProvidersOnce(client, path)
83−}
84−
85−func loadProvidersOnce(client ProviderClient, path string) ([]catwalk.Provider, error) {
86− var err error
80+func Providers(cfg *Config) ([]catwalk.Provider, error) {
8781 providerOnce.Do(func() {
88− providerList, err = loadProviders(client, path)
82+ catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
83+ client := catwalk.NewWithURL(catwalkURL)
84+ path := providerCacheFileData()
85+
86+ autoUpdateDisabled := cfg.Options.DisableProviderAutoUpdate
87+ providerList, providerErr = loadProviders(autoUpdateDisabled, client, path)
8988 })
90− if err != nil {
91− return nil, err
92− }
93− return providerList, nil
89+ return providerList, providerErr
9490}
9591
96−func loadProviders(client ProviderClient, path string) (providerList []catwalk.Provider, err error) {
97− // if cache is not stale, load from it
98− stale, exists := isCacheStale(path)
99− if !stale {
100− slog.Info("Using cached provider data", "path", path)
101− providerList, err = loadProvidersFromCache(path)
102− if len(providerList) > 0 && err == nil {
103− go func() {
104− slog.Info("Updating provider cache in background", "path", path)
105− updated, uerr := client.GetProviders()
106− if len(updated) > 0 && uerr == nil {
107− _ = saveProvidersInCache(path, updated)
108− }
109− }()
110− return
92+func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) {
93+ cacheIsStale, cacheExists := isCacheStale(path)
94+
95+ catwalkGetAndSave := func() ([]catwalk.Provider, error) {
96+ providers, err := client.GetProviders()
97+ if err != nil {
98+ return nil, fmt.Errorf("failed to fetch providers from catwalk: %w", err)
99+ }
100+ if len(providers) == 0 {
101+ return nil, fmt.Errorf("empty providers list from catwalk")
102+ }
103+ if err := saveProvidersInCache(path, providers); err != nil {
104+ return nil, err
111105 }
106+ return providers, nil
112107 }
113108
114− slog.Info("Getting live provider data", "path", path)
115− providerList, err = client.GetProviders()
116− if len(providerList) > 0 && err == nil {
117− err = saveProvidersInCache(path, providerList)
118− return
109+ backgroundCacheUpdate := func() {
110+ go func() {
111+ slog.Info("Updating providers cache in background", "path", path)
112+
113+ providers, err := client.GetProviders()
114+ if err != nil {
115+ slog.Error("Failed to fetch providers in background from Catwalk", "error", err)
116+ return
117+ }
118+ if len(providers) == 0 {
119+ slog.Error("Empty providers list from Catwalk")
120+ return
121+ }
122+ if err := saveProvidersInCache(path, providers); err != nil {
123+ slog.Error("Failed to update providers.json in background", "error", err)
124+ }
125+ }()
119126 }
120− if !exists {
121− err = fmt.Errorf("failed to load providers")
122− return
127+
128+ switch {
129+ case autoUpdateDisabled:
130+ slog.Warn("Providers auto-update is disabled")
131+
132+ if cacheExists {
133+ slog.Warn("Using locally cached providers")
134+ return loadProvidersFromCache(path)
135+ }
136+
137+ slog.Warn("Saving embedded providers to cache")
138+ providers := embedded.GetAll()
139+ if err := saveProvidersInCache(path, providers); err != nil {
140+ return nil, err
141+ }
142+ return providers, nil
143+
144+ case cacheExists && !cacheIsStale:
145+ slog.Info("Recent providers cache is available.", "path", path)
146+
147+ providers, err := loadProvidersFromCache(path)
148+ if err != nil {
149+ return nil, err
150+ }
151+ if len(providers) == 0 {
152+ return catwalkGetAndSave()
153+ }
154+ backgroundCacheUpdate()
155+ return providers, nil
156+
157+ default:
158+ slog.Info("Cache is not available or is stale. Fetching providers from Catwalk.", "path", path)
159+
160+ providers, err := catwalkGetAndSave()
161+ if err != nil {
162+ catwalkUrl := fmt.Sprintf("%s/providers", cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL))
163+ return nil, fmt.Errorf("crush was unable to fetch an updated list of providers from %s. Consider setting CRUSH_DISABLE_PROVIDER_AUTO_UPDATE=1 to use embedded version from the time of this Crush release. %w", catwalkUrl, err)
164+ }
165+ return providers, nil
123166 }
124− slog.Info("Loading provider data from cache", "path", path)
125− providerList, err = loadProvidersFromCache(path)
126− return
127167}
128168
129169func isCacheStale(path string) (stale, exists bool) {
internal/config/provider_empty_test.go+3−3
@@ -19,8 +19,8 @@ func TestProvider_loadProvidersEmptyResult(t *testing.T) {
1919 client := &emptyProviderClient{}
2020 tmpPath := t.TempDir() + "/providers.json"
2121
22− providers, err := loadProviders(client, tmpPath)
23− require.EqualError(t, err, "failed to load providers")
22+ providers, err := loadProviders(false, client, tmpPath)
23+ require.Contains(t, err.Error(), "crush was unable to fetch an updated list of providers")
2424 require.Empty(t, providers)
2525 require.Len(t, providers, 0)
2626
@@ -39,7 +39,7 @@ func TestProvider_loadProvidersEmptyCache(t *testing.T) {
3939 require.NoError(t, os.WriteFile(tmpPath, data, 0o644))
4040
4141 // Should refresh and get real providers instead of using empty cache
42− providers, err := loadProviders(client, tmpPath)
42+ providers, err := loadProviders(false, client, tmpPath)
4343 require.NoError(t, err)
4444 require.NotNil(t, providers)
4545 require.Len(t, providers, 1)
internal/config/provider_test.go+3−3
@@ -28,7 +28,7 @@ func (m *mockProviderClient) GetProviders() ([]catwalk.Provider, error) {
2828func TestProvider_loadProvidersNoIssues(t *testing.T) {
2929 client := &mockProviderClient{shouldFail: false}
3030 tmpPath := t.TempDir() + "/providers.json"
31− providers, err := loadProviders(client, tmpPath)
31+ providers, err := loadProviders(false, client, tmpPath)
3232 require.NoError(t, err)
3333 require.NotNil(t, providers)
3434 require.Len(t, providers, 1)
@@ -57,7 +57,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
5757 if err != nil {
5858 t.Fatalf("Failed to write old providers to file: %v", err)
5959 }
60− providers, err := loadProviders(client, tmpPath)
60+ providers, err := loadProviders(false, client, tmpPath)
6161 require.NoError(t, err)
6262 require.NotNil(t, providers)
6363 require.Len(t, providers, 1)
@@ -67,7 +67,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
6767func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
6868 client := &mockProviderClient{shouldFail: true}
6969 tmpPath := t.TempDir() + "/providers.json"
70− providers, err := loadProviders(client, tmpPath)
70+ providers, err := loadProviders(false, client, tmpPath)
7171 require.Error(t, err)
7272 require.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
7373}
internal/tui/components/chat/splash/splash.go+2−1
@@ -397,7 +397,8 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
397397}
398398
399399func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
400− providers, err := config.Providers()
400+ cfg := config.Get()
401+ providers, err := config.Providers(cfg)
401402 if err != nil {
402403 return nil, err
403404 }
internal/tui/components/dialogs/models/list.go+3−2
@@ -49,7 +49,8 @@ func NewModelListComponent(keyMap list.KeyMap, inputPlaceholder string, shouldRe
4949func (m *ModelListComponent) Init() tea.Cmd {
5050 var cmds []tea.Cmd
5151 if len(m.providers) == 0 {
52− providers, err := config.Providers()
52+ cfg := config.Get()
53+ providers, err := config.Providers(cfg)
5354 filteredProviders := []catwalk.Provider{}
5455 for _, p := range providers {
5556 hasAPIKeyEnv := strings.HasPrefix(p.APIKey, "$")
@@ -119,7 +120,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
119120
120121 // First, add any configured providers that are not in the known providers list
121122 // These should appear at the top of the list
122− knownProviders, err := config.Providers()
123+ knownProviders, err := config.Providers(cfg)
123124 if err != nil {
124125 return util.ReportError(err)
125126 }
internal/tui/components/dialogs/models/models.go+2−1
@@ -352,7 +352,8 @@ func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
352352}
353353
354354func (m *modelDialogCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
355− providers, err := config.Providers()
355+ cfg := config.Get()
356+ providers, err := config.Providers(cfg)
356357 if err != nil {
357358 return nil, err
358359 }
schema.json+5
@@ -278,6 +278,11 @@
278278 },
279279 "type": "array",
280280 "description": "Tools to disable"
281+ },
282+ "disable_provider_auto_update": {
283+ "type": "boolean",
284+ "description": "Disable providers auto-update",
285+ "default": false
281286 }
282287 },
283288 "additionalProperties": false,