on main help sign in
Merge pull request #284 from charmbracelet/tools-bg
adcf0e1Kujtim Hoxha
go.mod+2−1
@@ -42,11 +42,12 @@ require (
42 github.com/tidwall/sjson v1.2.5
42 github.com/tidwall/sjson v1.2.5
43 github.com/u-root/u-root v0.14.1-0.20250724181933-b01901710169
43 github.com/u-root/u-root v0.14.1-0.20250724181933-b01901710169
44 github.com/zeebo/xxh3 v1.0.2
44 github.com/zeebo/xxh3 v1.0.2
45− golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
46 gopkg.in/natefinch/lumberjack.v2 v2.2.1
45 gopkg.in/natefinch/lumberjack.v2 v2.2.1
47 mvdan.cc/sh/v3 v3.11.0
46 mvdan.cc/sh/v3 v3.11.0
48)
47)
49
48
49+require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
50+
50require (
51require (
51 cloud.google.com/go v0.116.0 // indirect
52 cloud.google.com/go v0.116.0 // indirect
52 cloud.google.com/go/auth v0.13.0 // indirect
53 cloud.google.com/go/auth v0.13.0 // indirect
internal/app/app.go+1−1
@@ -84,7 +84,7 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
84 app.setupEvents()
84 app.setupEvents()
85
85
86 // Initialize LSP clients in the background.
86 // Initialize LSP clients in the background.
87− go app.initLSPClients(ctx)
87+ app.initLSPClients(ctx)
88
88
89 // TODO: remove the concept of agent config, most likely.
89 // TODO: remove the concept of agent config, most likely.
90 if cfg.IsConfigured() {
90 if cfg.IsConfigured() {
internal/config/config.go+10−13
@@ -3,6 +3,7 @@ package config
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6+ "log/slog"
6 "net/http"
7 "net/http"
7 "os"
8 "os"
8 "slices"
9 "slices"
@@ -10,9 +11,9 @@ import (
10 "time"
11 "time"
11
12
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
14+ "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/env"
15 "github.com/charmbracelet/crush/internal/env"
14 "github.com/tidwall/sjson"
16 "github.com/tidwall/sjson"
15− "golang.org/x/exp/slog"
16)
17)
17
18
18const (
19const (
@@ -240,7 +241,7 @@ type Config struct {
240 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
241 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
241
242
242 // The providers that are configured
243 // The providers that are configured
243− Providers map[string]ProviderConfig `json:"providers,omitempty"`
244+ Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty"`
244
245
245 MCP MCPs `json:"mcp,omitempty"`
246 MCP MCPs `json:"mcp,omitempty"`
246
247
@@ -265,8 +266,8 @@ func (c *Config) WorkingDir() string {
265}
266}
266
267
267func (c *Config) EnabledProviders() []ProviderConfig {
268func (c *Config) EnabledProviders() []ProviderConfig {
268− enabled := make([]ProviderConfig, 0, len(c.Providers))
269+ var enabled []ProviderConfig
269− for _, p := range c.Providers {
270+ for _, p := range c.Providers.Seq2() {
270 if !p.Disable {
271 if !p.Disable {
271 enabled = append(enabled, p)
272 enabled = append(enabled, p)
272 }
273 }
@@ -280,7 +281,7 @@ func (c *Config) IsConfigured() bool {
280}
281}
281
282
282func (c *Config) GetModel(provider, model string) *catwalk.Model {
283func (c *Config) GetModel(provider, model string) *catwalk.Model {
283− if providerConfig, ok := c.Providers[provider]; ok {
284+ if providerConfig, ok := c.Providers.Get(provider); ok {
284 for _, m := range providerConfig.Models {
285 for _, m := range providerConfig.Models {
285 if m.ID == model {
286 if m.ID == model {
286 return &m
287 return &m
@@ -295,7 +296,7 @@ func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfi
295 if !ok {
296 if !ok {
296 return nil
297 return nil
297 }
298 }
298− if providerConfig, ok := c.Providers[model.Provider]; ok {
299+ if providerConfig, ok := c.Providers.Get(model.Provider); ok {
299 return &providerConfig
300 return &providerConfig
300 }
301 }
301 return nil
302 return nil
@@ -376,14 +377,10 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
376 return fmt.Errorf("failed to save API key to config file: %w", err)
377 return fmt.Errorf("failed to save API key to config file: %w", err)
377 }
378 }
378
379
379− if c.Providers == nil {
380− c.Providers = make(map[string]ProviderConfig)
381− }
382−
383− providerConfig, exists := c.Providers[providerID]
380+ providerConfig, exists := c.Providers.Get(providerID)
384 if exists {
381 if exists {
385 providerConfig.APIKey = apiKey
382 providerConfig.APIKey = apiKey
386− c.Providers[providerID] = providerConfig
383+ c.Providers.Set(providerID, providerConfig)
387 return nil
384 return nil
388 }
385 }
389
386
@@ -412,7 +409,7 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
412 return fmt.Errorf("provider with ID %s not found in known providers", providerID)
409 return fmt.Errorf("provider with ID %s not found in known providers", providerID)
413 }
410 }
414 // Store the updated provider config
411 // Store the updated provider config
415− c.Providers[providerID] = providerConfig
412+ c.Providers.Set(providerID, providerConfig)
416 return nil
413 return nil
417}
414}
418
415
internal/config/init.go+9−16
@@ -5,7 +5,6 @@ import (
5 "os"
5 "os"
6 "path/filepath"
6 "path/filepath"
7 "strings"
7 "strings"
8− "sync"
9 "sync/atomic"
8 "sync/atomic"
10)
9)
11
10
@@ -18,26 +17,20 @@ type ProjectInitFlag struct {
18}
17}
19
18
20// TODO: we need to remove the global config instance keeping it now just until everything is migrated
19// TODO: we need to remove the global config instance keeping it now just until everything is migrated
21−var (
22− instance atomic.Pointer[Config]
20+var instance atomic.Pointer[Config]
23− cwd string
24− once sync.Once // Ensures the initialization happens only once
25−)
26
21
27func Init(workingDir string, debug bool) (*Config, error) {
22func Init(workingDir string, debug bool) (*Config, error) {
28− var err error
29− once.Do(func() {
30− cwd = workingDir
23+ cfg, err := Load(workingDir, debug)
31− var cfg *Config
24+ if err != nil {
32− cfg, err = Load(cwd, debug)
25+ return nil, err
33− instance.Store(cfg)
34− })
26+ }
35−
27+ instance.Store(cfg)
36− return instance.Load(), err
28+ return instance.Load(), nil
37}
29}
38
30
39func Get() *Config {
31func Get() *Config {
40− return instance.Load()
32+ cfg := instance.Load()
33+ return cfg
41}
34}
42
35
43func ProjectNeedsInitialization() (bool, error) {
36func ProjectNeedsInitialization() (bool, error) {
internal/config/load.go+20−49
@@ -4,17 +4,17 @@ import (
4 "encoding/json"
4 "encoding/json"
5 "fmt"
5 "fmt"
6 "io"
6 "io"
7+ "log/slog"
7 "os"
8 "os"
8 "path/filepath"
9 "path/filepath"
9 "runtime"
10 "runtime"
10 "slices"
11 "slices"
11 "strings"
12 "strings"
12− "sync"
13
13
14 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/catwalk/pkg/catwalk"
15+ "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/env"
16 "github.com/charmbracelet/crush/internal/env"
16 "github.com/charmbracelet/crush/internal/log"
17 "github.com/charmbracelet/crush/internal/log"
17− "golang.org/x/exp/slog"
18)
18)
19
19
20const catwalkURL = "https://catwalk.charm.sh"
20const catwalkURL = "https://catwalk.charm.sh"
@@ -63,7 +63,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
63 )
63 )
64
64
65 // Load known providers, this loads the config from catwalk
65 // Load known providers, this loads the config from catwalk
66− providers, err := LoadProviders(catwalk.NewWithURL(catwalkURL))
66+ providers, err := Providers()
67 if err != nil || len(providers) == 0 {
67 if err != nil || len(providers) == 0 {
68 return nil, fmt.Errorf("failed to load providers: %w", err)
68 return nil, fmt.Errorf("failed to load providers: %w", err)
69 }
69 }
@@ -77,35 +77,6 @@ func Load(workingDir string, debug bool) (*Config, error) {
77 return nil, fmt.Errorf("failed to configure providers: %w", err)
77 return nil, fmt.Errorf("failed to configure providers: %w", err)
78 }
78 }
79
79
80− // Test provider connections in parallel
81− var testResults sync.Map
82− var wg sync.WaitGroup
83−
84− for _, p := range cfg.Providers {
85− if p.Type == catwalk.TypeOpenAI || p.Type == catwalk.TypeAnthropic {
86− wg.Add(1)
87− go func(provider ProviderConfig) {
88− defer wg.Done()
89− err := provider.TestConnection(cfg.resolver)
90− testResults.Store(provider.ID, err == nil)
91− if err != nil {
92− slog.Error("Provider connection test failed", "provider", provider.ID, "error", err)
93− }
94− }(p)
95− }
96− }
97− wg.Wait()
98−
99− // Remove failed providers
100− testResults.Range(func(key, value any) bool {
101− providerID := key.(string)
102− passed := value.(bool)
103− if !passed {
104− delete(cfg.Providers, providerID)
105− }
106− return true
107− })
108−
109 if !cfg.IsConfigured() {
80 if !cfg.IsConfigured() {
110 slog.Warn("No providers configured")
81 slog.Warn("No providers configured")
111 return cfg, nil
82 return cfg, nil
@@ -122,12 +93,12 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
122 knownProviderNames := make(map[string]bool)
93 knownProviderNames := make(map[string]bool)
123 for _, p := range knownProviders {
94 for _, p := range knownProviders {
124 knownProviderNames[string(p.ID)] = true
95 knownProviderNames[string(p.ID)] = true
125− config, configExists := c.Providers[string(p.ID)]
96+ config, configExists := c.Providers.Get(string(p.ID))
126 // if the user configured a known provider we need to allow it to override a couple of parameters
97 // if the user configured a known provider we need to allow it to override a couple of parameters
127 if configExists {
98 if configExists {
128 if config.Disable {
99 if config.Disable {
129 slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
100 slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
130− delete(c.Providers, string(p.ID))
101+ c.Providers.Del(string(p.ID))
131 continue
102 continue
132 }
103 }
133 if config.BaseURL != "" {
104 if config.BaseURL != "" {
@@ -183,7 +154,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
183 if !hasVertexCredentials(env) {
154 if !hasVertexCredentials(env) {
184 if configExists {
155 if configExists {
185 slog.Warn("Skipping Vertex AI provider due to missing credentials")
156 slog.Warn("Skipping Vertex AI provider due to missing credentials")
186− delete(c.Providers, string(p.ID))
157+ c.Providers.Del(string(p.ID))
187 }
158 }
188 continue
159 continue
189 }
160 }
@@ -194,7 +165,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
194 if err != nil || endpoint == "" {
165 if err != nil || endpoint == "" {
195 if configExists {
166 if configExists {
196 slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
167 slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
197− delete(c.Providers, string(p.ID))
168+ c.Providers.Del(string(p.ID))
198 }
169 }
199 continue
170 continue
200 }
171 }
@@ -204,7 +175,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
204 if !hasAWSCredentials(env) {
175 if !hasAWSCredentials(env) {
205 if configExists {
176 if configExists {
206 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
177 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
207− delete(c.Providers, string(p.ID))
178+ c.Providers.Del(string(p.ID))
208 }
179 }
209 continue
180 continue
210 }
181 }
@@ -219,16 +190,16 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
219 if v == "" || err != nil {
190 if v == "" || err != nil {
220 if configExists {
191 if configExists {
221 slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
192 slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
222− delete(c.Providers, string(p.ID))
193+ c.Providers.Del(string(p.ID))
223 }
194 }
224 continue
195 continue
225 }
196 }
226 }
197 }
227− c.Providers[string(p.ID)] = prepared
198+ c.Providers.Set(string(p.ID), prepared)
228 }
199 }
229
200
230 // validate the custom providers
201 // validate the custom providers
231− for id, providerConfig := range c.Providers {
202+ for id, providerConfig := range c.Providers.Seq2() {
232 if knownProviderNames[id] {
203 if knownProviderNames[id] {
233 continue
204 continue
234 }
205 }
@@ -245,7 +216,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
245
216
246 if providerConfig.Disable {
217 if providerConfig.Disable {
247 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
218 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
248− delete(c.Providers, id)
219+ c.Providers.Del(id)
249 continue
220 continue
250 }
221 }
251 if providerConfig.APIKey == "" {
222 if providerConfig.APIKey == "" {
@@ -253,17 +224,17 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
253 }
224 }
254 if providerConfig.BaseURL == "" {
225 if providerConfig.BaseURL == "" {
255 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
226 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
256− delete(c.Providers, id)
227+ c.Providers.Del(id)
257 continue
228 continue
258 }
229 }
259 if len(providerConfig.Models) == 0 {
230 if len(providerConfig.Models) == 0 {
260 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
231 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
261− delete(c.Providers, id)
232+ c.Providers.Del(id)
262 continue
233 continue
263 }
234 }
264 if providerConfig.Type != catwalk.TypeOpenAI {
235 if providerConfig.Type != catwalk.TypeOpenAI {
265 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
236 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
266− delete(c.Providers, id)
237+ c.Providers.Del(id)
267 continue
238 continue
268 }
239 }
269
240
@@ -274,11 +245,11 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
274 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
245 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
275 if baseURL == "" || err != nil {
246 if baseURL == "" || err != nil {
276 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
247 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
277− delete(c.Providers, id)
248+ c.Providers.Del(id)
278 continue
249 continue
279 }
250 }
280
251
281− c.Providers[id] = providerConfig
252+ c.Providers.Set(id, providerConfig)
282 }
253 }
283 return nil
254 return nil
284}
255}
@@ -298,7 +269,7 @@ func (c *Config) setDefaults(workingDir string) {
298 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
269 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
299 }
270 }
300 if c.Providers == nil {
271 if c.Providers == nil {
301− c.Providers = make(map[string]ProviderConfig)
272+ c.Providers = csync.NewMap[string, ProviderConfig]()
302 }
273 }
303 if c.Models == nil {
274 if c.Models == nil {
304 c.Models = make(map[SelectedModelType]SelectedModel)
275 c.Models = make(map[SelectedModelType]SelectedModel)
@@ -317,7 +288,7 @@ func (c *Config) setDefaults(workingDir string) {
317}
288}
318
289
319func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
290func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
320− if len(knownProviders) == 0 && len(c.Providers) == 0 {
291+ if len(knownProviders) == 0 && c.Providers.Len() == 0 {
321 err = fmt.Errorf("no providers configured, please configure at least one provider")
292 err = fmt.Errorf("no providers configured, please configure at least one provider")
322 return
293 return
323 }
294 }
@@ -325,7 +296,7 @@ func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (large
325 // Use the first provider enabled based on the known providers order
296 // Use the first provider enabled based on the known providers order
326 // if no provider found that is known use the first provider configured
297 // if no provider found that is known use the first provider configured
327 for _, p := range knownProviders {
298 for _, p := range knownProviders {
328− providerConfig, ok := c.Providers[string(p.ID)]
299+ providerConfig, ok := c.Providers.Get(string(p.ID))
329 if !ok || providerConfig.Disable {
300 if !ok || providerConfig.Disable {
330 continue
301 continue
331 }
302 }
internal/config/load_test.go+106−101
@@ -9,6 +9,7 @@ import (
9 "testing"
9 "testing"
10
10
11 "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/catwalk/pkg/catwalk"
12+ "github.com/charmbracelet/crush/internal/csync"
12 "github.com/charmbracelet/crush/internal/env"
13 "github.com/charmbracelet/crush/internal/env"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/assert"
14)
15)
@@ -29,9 +30,10 @@ func TestConfig_LoadFromReaders(t *testing.T) {
29
30
30 assert.NoError(t, err)
31 assert.NoError(t, err)
31 assert.NotNil(t, loadedConfig)
32 assert.NotNil(t, loadedConfig)
33+ assert.Equal(t, 1, loadedConfig.Providers.Len())
32− assert.Len(t, loadedConfig.Providers, 1)
34+ pc, _ := loadedConfig.Providers.Get("openai")
33− assert.Equal(t, "key2", loadedConfig.Providers["openai"].APIKey)
35+ assert.Equal(t, "key2", pc.APIKey)
34− assert.Equal(t, "https://api.openai.com/v2", loadedConfig.Providers["openai"].BaseURL)
36+ assert.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
35}
37}
36
38
37func TestConfig_setDefaults(t *testing.T) {
39func TestConfig_setDefaults(t *testing.T) {
@@ -73,10 +75,11 @@ func TestConfig_configureProviders(t *testing.T) {
73 resolver := NewEnvironmentVariableResolver(env)
75 resolver := NewEnvironmentVariableResolver(env)
74 err := cfg.configureProviders(env, resolver, knownProviders)
76 err := cfg.configureProviders(env, resolver, knownProviders)
75 assert.NoError(t, err)
77 assert.NoError(t, err)
76− assert.Len(t, cfg.Providers, 1)
78+ assert.Equal(t, 1, cfg.Providers.Len())
77
79
78 // We want to make sure that we keep the configured API key as a placeholder
80 // We want to make sure that we keep the configured API key as a placeholder
81+ pc, _ := cfg.Providers.Get("openai")
79− assert.Equal(t, "$OPENAI_API_KEY", cfg.Providers["openai"].APIKey)
82+ assert.Equal(t, "$OPENAI_API_KEY", pc.APIKey)
80}
83}
81
84
82func TestConfig_configureProvidersWithOverride(t *testing.T) {
85func TestConfig_configureProvidersWithOverride(t *testing.T) {
@@ -92,22 +95,21 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
92 }
95 }
93
96
94 cfg := &Config{
97 cfg := &Config{
95− Providers: map[string]ProviderConfig{
98+ Providers: csync.NewMap[string, ProviderConfig](),
96− "openai": {
99+ }
100+ cfg.Providers.Set("openai", ProviderConfig{
97− APIKey: "xyz",
101+ APIKey: "xyz",
98− BaseURL: "https://api.openai.com/v2",
102+ BaseURL: "https://api.openai.com/v2",
99− Models: []catwalk.Model{
103+ Models: []catwalk.Model{
100− {
104+ {
101− ID: "test-model",
105+ ID: "test-model",
102− Name: "Updated",
106+ Name: "Updated",
103− },
107+ },
104− {
108+ {
105− ID: "another-model",
109+ ID: "another-model",
106− },
107− },
108 },
110 },
109 },
111 },
110− }
112+ })
111 cfg.setDefaults("/tmp")
113 cfg.setDefaults("/tmp")
112
114
113 env := env.NewFromMap(map[string]string{
115 env := env.NewFromMap(map[string]string{
@@ -116,13 +118,14 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
116 resolver := NewEnvironmentVariableResolver(env)
118 resolver := NewEnvironmentVariableResolver(env)
117 err := cfg.configureProviders(env, resolver, knownProviders)
119 err := cfg.configureProviders(env, resolver, knownProviders)
118 assert.NoError(t, err)
120 assert.NoError(t, err)
119− assert.Len(t, cfg.Providers, 1)
121+ assert.Equal(t, 1, cfg.Providers.Len())
120
122
121 // We want to make sure that we keep the configured API key as a placeholder
123 // We want to make sure that we keep the configured API key as a placeholder
124+ pc, _ := cfg.Providers.Get("openai")
122− assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
125+ assert.Equal(t, "xyz", pc.APIKey)
123− assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
126+ assert.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
124− assert.Len(t, cfg.Providers["openai"].Models, 2)
127+ assert.Len(t, pc.Models, 2)
125− assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Name)
128+ assert.Equal(t, "Updated", pc.Models[0].Name)
126}
129}
127
130
128func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
131func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
@@ -138,7 +141,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
138 }
141 }
139
142
140 cfg := &Config{
143 cfg := &Config{
141− Providers: map[string]ProviderConfig{
144+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
142 "custom": {
145 "custom": {
143 APIKey: "xyz",
146 APIKey: "xyz",
144 BaseURL: "https://api.someendpoint.com/v2",
147 BaseURL: "https://api.someendpoint.com/v2",
@@ -148,7 +151,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
148 },
151 },
149 },
152 },
150 },
153 },
151− },
154+ }),
152 }
155 }
153 cfg.setDefaults("/tmp")
156 cfg.setDefaults("/tmp")
154 env := env.NewFromMap(map[string]string{
157 env := env.NewFromMap(map[string]string{
@@ -158,16 +161,17 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
158 err := cfg.configureProviders(env, resolver, knownProviders)
161 err := cfg.configureProviders(env, resolver, knownProviders)
159 assert.NoError(t, err)
162 assert.NoError(t, err)
160 // Should be to because of the env variable
163 // Should be to because of the env variable
161− assert.Len(t, cfg.Providers, 2)
164+ assert.Equal(t, cfg.Providers.Len(), 2)
162
165
163 // We want to make sure that we keep the configured API key as a placeholder
166 // We want to make sure that we keep the configured API key as a placeholder
167+ pc, _ := cfg.Providers.Get("custom")
164− assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
168+ assert.Equal(t, "xyz", pc.APIKey)
165 // Make sure we set the ID correctly
169 // Make sure we set the ID correctly
166− assert.Equal(t, "custom", cfg.Providers["custom"].ID)
170+ assert.Equal(t, "custom", pc.ID)
167− assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
171+ assert.Equal(t, "https://api.someendpoint.com/v2", pc.BaseURL)
168− assert.Len(t, cfg.Providers["custom"].Models, 1)
172+ assert.Len(t, pc.Models, 1)
169
173
170− _, ok := cfg.Providers["openai"]
174+ _, ok := cfg.Providers.Get("openai")
171 assert.True(t, ok, "OpenAI provider should still be present")
175 assert.True(t, ok, "OpenAI provider should still be present")
172}
176}
173
177
@@ -192,9 +196,9 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
192 resolver := NewEnvironmentVariableResolver(env)
196 resolver := NewEnvironmentVariableResolver(env)
193 err := cfg.configureProviders(env, resolver, knownProviders)
197 err := cfg.configureProviders(env, resolver, knownProviders)
194 assert.NoError(t, err)
198 assert.NoError(t, err)
195− assert.Len(t, cfg.Providers, 1)
199+ assert.Equal(t, cfg.Providers.Len(), 1)
196
200
197− bedrockProvider, ok := cfg.Providers["bedrock"]
201+ bedrockProvider, ok := cfg.Providers.Get("bedrock")
198 assert.True(t, ok, "Bedrock provider should be present")
202 assert.True(t, ok, "Bedrock provider should be present")
199 assert.Len(t, bedrockProvider.Models, 1)
203 assert.Len(t, bedrockProvider.Models, 1)
200 assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
204 assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
@@ -219,7 +223,7 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
219 err := cfg.configureProviders(env, resolver, knownProviders)
223 err := cfg.configureProviders(env, resolver, knownProviders)
220 assert.NoError(t, err)
224 assert.NoError(t, err)
221 // Provider should not be configured without credentials
225 // Provider should not be configured without credentials
222− assert.Len(t, cfg.Providers, 0)
226+ assert.Equal(t, cfg.Providers.Len(), 0)
223}
227}
224
228
225func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
229func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
@@ -267,9 +271,9 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
267 resolver := NewEnvironmentVariableResolver(env)
271 resolver := NewEnvironmentVariableResolver(env)
268 err := cfg.configureProviders(env, resolver, knownProviders)
272 err := cfg.configureProviders(env, resolver, knownProviders)
269 assert.NoError(t, err)
273 assert.NoError(t, err)
270− assert.Len(t, cfg.Providers, 1)
274+ assert.Equal(t, cfg.Providers.Len(), 1)
271
275
272− vertexProvider, ok := cfg.Providers["vertexai"]
276+ vertexProvider, ok := cfg.Providers.Get("vertexai")
273 assert.True(t, ok, "VertexAI provider should be present")
277 assert.True(t, ok, "VertexAI provider should be present")
274 assert.Len(t, vertexProvider.Models, 1)
278 assert.Len(t, vertexProvider.Models, 1)
275 assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
279 assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
@@ -300,7 +304,7 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
300 err := cfg.configureProviders(env, resolver, knownProviders)
304 err := cfg.configureProviders(env, resolver, knownProviders)
301 assert.NoError(t, err)
305 assert.NoError(t, err)
302 // Provider should not be configured without proper credentials
306 // Provider should not be configured without proper credentials
303− assert.Len(t, cfg.Providers, 0)
307+ assert.Equal(t, cfg.Providers.Len(), 0)
304}
308}
305
309
306func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
310func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
@@ -325,7 +329,7 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
325 err := cfg.configureProviders(env, resolver, knownProviders)
329 err := cfg.configureProviders(env, resolver, knownProviders)
326 assert.NoError(t, err)
330 assert.NoError(t, err)
327 // Provider should not be configured without project
331 // Provider should not be configured without project
328− assert.Len(t, cfg.Providers, 0)
332+ assert.Equal(t, cfg.Providers.Len(), 0)
329}
333}
330
334
331func TestConfig_configureProvidersSetProviderID(t *testing.T) {
335func TestConfig_configureProvidersSetProviderID(t *testing.T) {
@@ -348,16 +352,17 @@ func TestConfig_configureProvidersSetProviderID(t *testing.T) {
348 resolver := NewEnvironmentVariableResolver(env)
352 resolver := NewEnvironmentVariableResolver(env)
349 err := cfg.configureProviders(env, resolver, knownProviders)
353 err := cfg.configureProviders(env, resolver, knownProviders)
350 assert.NoError(t, err)
354 assert.NoError(t, err)
351− assert.Len(t, cfg.Providers, 1)
355+ assert.Equal(t, cfg.Providers.Len(), 1)
352
356
353 // Provider ID should be set
357 // Provider ID should be set
358+ pc, _ := cfg.Providers.Get("openai")
354− assert.Equal(t, "openai", cfg.Providers["openai"].ID)
359+ assert.Equal(t, "openai", pc.ID)
355}
360}
356
361
357func TestConfig_EnabledProviders(t *testing.T) {
362func TestConfig_EnabledProviders(t *testing.T) {
358 t.Run("all providers enabled", func(t *testing.T) {
363 t.Run("all providers enabled", func(t *testing.T) {
359 cfg := &Config{
364 cfg := &Config{
360− Providers: map[string]ProviderConfig{
365+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
361 "openai": {
366 "openai": {
362 ID: "openai",
367 ID: "openai",
363 APIKey: "key1",
368 APIKey: "key1",
@@ -368,7 +373,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
368 APIKey: "key2",
373 APIKey: "key2",
369 Disable: false,
374 Disable: false,
370 },
375 },
371− },
376+ }),
372 }
377 }
373
378
374 enabled := cfg.EnabledProviders()
379 enabled := cfg.EnabledProviders()
@@ -377,7 +382,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
377
382
378 t.Run("some providers disabled", func(t *testing.T) {
383 t.Run("some providers disabled", func(t *testing.T) {
379 cfg := &Config{
384 cfg := &Config{
380− Providers: map[string]ProviderConfig{
385+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
381 "openai": {
386 "openai": {
382 ID: "openai",
387 ID: "openai",
383 APIKey: "key1",
388 APIKey: "key1",
@@ -388,7 +393,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
388 APIKey: "key2",
393 APIKey: "key2",
389 Disable: true,
394 Disable: true,
390 },
395 },
391− },
396+ }),
392 }
397 }
393
398
394 enabled := cfg.EnabledProviders()
399 enabled := cfg.EnabledProviders()
@@ -398,7 +403,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
398
403
399 t.Run("empty providers map", func(t *testing.T) {
404 t.Run("empty providers map", func(t *testing.T) {
400 cfg := &Config{
405 cfg := &Config{
401− Providers: map[string]ProviderConfig{},
406+ Providers: csync.NewMap[string, ProviderConfig](),
402 }
407 }
403
408
404 enabled := cfg.EnabledProviders()
409 enabled := cfg.EnabledProviders()
@@ -409,13 +414,13 @@ func TestConfig_EnabledProviders(t *testing.T) {
409func TestConfig_IsConfigured(t *testing.T) {
414func TestConfig_IsConfigured(t *testing.T) {
410 t.Run("returns true when at least one provider is enabled", func(t *testing.T) {
415 t.Run("returns true when at least one provider is enabled", func(t *testing.T) {
411 cfg := &Config{
416 cfg := &Config{
412− Providers: map[string]ProviderConfig{
417+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
413 "openai": {
418 "openai": {
414 ID: "openai",
419 ID: "openai",
415 APIKey: "key1",
420 APIKey: "key1",
416 Disable: false,
421 Disable: false,
417 },
422 },
418− },
423+ }),
419 }
424 }
420
425
421 assert.True(t, cfg.IsConfigured())
426 assert.True(t, cfg.IsConfigured())
@@ -423,7 +428,7 @@ func TestConfig_IsConfigured(t *testing.T) {
423
428
424 t.Run("returns false when no providers are configured", func(t *testing.T) {
429 t.Run("returns false when no providers are configured", func(t *testing.T) {
425 cfg := &Config{
430 cfg := &Config{
426− Providers: map[string]ProviderConfig{},
431+ Providers: csync.NewMap[string, ProviderConfig](),
427 }
432 }
428
433
429 assert.False(t, cfg.IsConfigured())
434 assert.False(t, cfg.IsConfigured())
@@ -431,7 +436,7 @@ func TestConfig_IsConfigured(t *testing.T) {
431
436
432 t.Run("returns false when all providers are disabled", func(t *testing.T) {
437 t.Run("returns false when all providers are disabled", func(t *testing.T) {
433 cfg := &Config{
438 cfg := &Config{
434− Providers: map[string]ProviderConfig{
439+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
435 "openai": {
440 "openai": {
436 ID: "openai",
441 ID: "openai",
437 APIKey: "key1",
442 APIKey: "key1",
@@ -442,7 +447,7 @@ func TestConfig_IsConfigured(t *testing.T) {
442 APIKey: "key2",
447 APIKey: "key2",
443 Disable: true,
448 Disable: true,
444 },
449 },
445− },
450+ }),
446 }
451 }
447
452
448 assert.False(t, cfg.IsConfigured())
453 assert.False(t, cfg.IsConfigured())
@@ -462,11 +467,11 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
462 }
467 }
463
468
464 cfg := &Config{
469 cfg := &Config{
465− Providers: map[string]ProviderConfig{
470+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
466 "openai": {
471 "openai": {
467 Disable: true,
472 Disable: true,
468 },
473 },
469− },
474+ }),
470 }
475 }
471 cfg.setDefaults("/tmp")
476 cfg.setDefaults("/tmp")
472
477
@@ -478,15 +483,15 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
478 assert.NoError(t, err)
483 assert.NoError(t, err)
479
484
480 // Provider should be removed from config when disabled
485 // Provider should be removed from config when disabled
481− assert.Len(t, cfg.Providers, 0)
486+ assert.Equal(t, cfg.Providers.Len(), 0)
482− _, exists := cfg.Providers["openai"]
487+ _, exists := cfg.Providers.Get("openai")
483 assert.False(t, exists)
488 assert.False(t, exists)
484}
489}
485
490
486func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
491func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
487 t.Run("custom provider with missing API key is allowed, but not known providers", func(t *testing.T) {
492 t.Run("custom provider with missing API key is allowed, but not known providers", func(t *testing.T) {
488 cfg := &Config{
493 cfg := &Config{
489− Providers: map[string]ProviderConfig{
494+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
490 "custom": {
495 "custom": {
491 BaseURL: "https://api.custom.com/v1",
496 BaseURL: "https://api.custom.com/v1",
492 Models: []catwalk.Model{{
497 Models: []catwalk.Model{{
@@ -496,7 +501,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
496 "openai": {
501 "openai": {
497 APIKey: "$MISSING",
502 APIKey: "$MISSING",
498 },
503 },
499− },
504+ }),
500 }
505 }
501 cfg.setDefaults("/tmp")
506 cfg.setDefaults("/tmp")
502
507
@@ -505,21 +510,21 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
505 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
510 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
506 assert.NoError(t, err)
511 assert.NoError(t, err)
507
512
508− assert.Len(t, cfg.Providers, 1)
513+ assert.Equal(t, cfg.Providers.Len(), 1)
509− _, exists := cfg.Providers["custom"]
514+ _, exists := cfg.Providers.Get("custom")
510 assert.True(t, exists)
515 assert.True(t, exists)
511 })
516 })
512
517
513 t.Run("custom provider with missing BaseURL is removed", func(t *testing.T) {
518 t.Run("custom provider with missing BaseURL is removed", func(t *testing.T) {
514 cfg := &Config{
519 cfg := &Config{
515− Providers: map[string]ProviderConfig{
520+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
516 "custom": {
521 "custom": {
517 APIKey: "test-key",
522 APIKey: "test-key",
518 Models: []catwalk.Model{{
523 Models: []catwalk.Model{{
519 ID: "test-model",
524 ID: "test-model",
520 }},
525 }},
521 },
526 },
522− },
527+ }),
523 }
528 }
524 cfg.setDefaults("/tmp")
529 cfg.setDefaults("/tmp")
525
530
@@ -528,20 +533,20 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
528 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
533 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
529 assert.NoError(t, err)
534 assert.NoError(t, err)
530
535
531− assert.Len(t, cfg.Providers, 0)
536+ assert.Equal(t, cfg.Providers.Len(), 0)
532− _, exists := cfg.Providers["custom"]
537+ _, exists := cfg.Providers.Get("custom")
533 assert.False(t, exists)
538 assert.False(t, exists)
534 })
539 })
535
540
536 t.Run("custom provider with no models is removed", func(t *testing.T) {
541 t.Run("custom provider with no models is removed", func(t *testing.T) {
537 cfg := &Config{
542 cfg := &Config{
538− Providers: map[string]ProviderConfig{
543+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
539 "custom": {
544 "custom": {
540 APIKey: "test-key",
545 APIKey: "test-key",
541 BaseURL: "https://api.custom.com/v1",
546 BaseURL: "https://api.custom.com/v1",
542 Models: []catwalk.Model{},
547 Models: []catwalk.Model{},
543 },
548 },
544− },
549+ }),
545 }
550 }
546 cfg.setDefaults("/tmp")
551 cfg.setDefaults("/tmp")
547
552
@@ -550,14 +555,14 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
550 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
555 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
551 assert.NoError(t, err)
556 assert.NoError(t, err)
552
557
553− assert.Len(t, cfg.Providers, 0)
558+ assert.Equal(t, cfg.Providers.Len(), 0)
554− _, exists := cfg.Providers["custom"]
559+ _, exists := cfg.Providers.Get("custom")
555 assert.False(t, exists)
560 assert.False(t, exists)
556 })
561 })
557
562
558 t.Run("custom provider with unsupported type is removed", func(t *testing.T) {
563 t.Run("custom provider with unsupported type is removed", func(t *testing.T) {
559 cfg := &Config{
564 cfg := &Config{
560− Providers: map[string]ProviderConfig{
565+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
561 "custom": {
566 "custom": {
562 APIKey: "test-key",
567 APIKey: "test-key",
563 BaseURL: "https://api.custom.com/v1",
568 BaseURL: "https://api.custom.com/v1",
@@ -566,7 +571,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
566 ID: "test-model",
571 ID: "test-model",
567 }},
572 }},
568 },
573 },
569− },
574+ }),
570 }
575 }
571 cfg.setDefaults("/tmp")
576 cfg.setDefaults("/tmp")
572
577
@@ -575,14 +580,14 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
575 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
580 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
576 assert.NoError(t, err)
581 assert.NoError(t, err)
577
582
578− assert.Len(t, cfg.Providers, 0)
583+ assert.Equal(t, cfg.Providers.Len(), 0)
579− _, exists := cfg.Providers["custom"]
584+ _, exists := cfg.Providers.Get("custom")
580 assert.False(t, exists)
585 assert.False(t, exists)
581 })
586 })
582
587
583 t.Run("valid custom provider is kept and ID is set", func(t *testing.T) {
588 t.Run("valid custom provider is kept and ID is set", func(t *testing.T) {
584 cfg := &Config{
589 cfg := &Config{
585− Providers: map[string]ProviderConfig{
590+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
586 "custom": {
591 "custom": {
587 APIKey: "test-key",
592 APIKey: "test-key",
588 BaseURL: "https://api.custom.com/v1",
593 BaseURL: "https://api.custom.com/v1",
@@ -591,7 +596,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
591 ID: "test-model",
596 ID: "test-model",
592 }},
597 }},
593 },
598 },
594− },
599+ }),
595 }
600 }
596 cfg.setDefaults("/tmp")
601 cfg.setDefaults("/tmp")
597
602
@@ -600,8 +605,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
600 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
605 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
601 assert.NoError(t, err)
606 assert.NoError(t, err)
602
607
603− assert.Len(t, cfg.Providers, 1)
608+ assert.Equal(t, cfg.Providers.Len(), 1)
604− customProvider, exists := cfg.Providers["custom"]
609+ customProvider, exists := cfg.Providers.Get("custom")
605 assert.True(t, exists)
610 assert.True(t, exists)
606 assert.Equal(t, "custom", customProvider.ID)
611 assert.Equal(t, "custom", customProvider.ID)
607 assert.Equal(t, "test-key", customProvider.APIKey)
612 assert.Equal(t, "test-key", customProvider.APIKey)
@@ -610,7 +615,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
610
615
611 t.Run("disabled custom provider is removed", func(t *testing.T) {
616 t.Run("disabled custom provider is removed", func(t *testing.T) {
612 cfg := &Config{
617 cfg := &Config{
613− Providers: map[string]ProviderConfig{
618+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
614 "custom": {
619 "custom": {
615 APIKey: "test-key",
620 APIKey: "test-key",
616 BaseURL: "https://api.custom.com/v1",
621 BaseURL: "https://api.custom.com/v1",
@@ -620,7 +625,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
620 ID: "test-model",
625 ID: "test-model",
621 }},
626 }},
622 },
627 },
623− },
628+ }),
624 }
629 }
625 cfg.setDefaults("/tmp")
630 cfg.setDefaults("/tmp")
626
631
@@ -629,8 +634,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
629 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
634 err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
630 assert.NoError(t, err)
635 assert.NoError(t, err)
631
636
632− assert.Len(t, cfg.Providers, 0)
637+ assert.Equal(t, cfg.Providers.Len(), 0)
633− _, exists := cfg.Providers["custom"]
638+ _, exists := cfg.Providers.Get("custom")
634 assert.False(t, exists)
639 assert.False(t, exists)
635 })
640 })
636}
641}
@@ -649,11 +654,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
649 }
654 }
650
655
651 cfg := &Config{
656 cfg := &Config{
652− Providers: map[string]ProviderConfig{
657+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
653 "vertexai": {
658 "vertexai": {
654 BaseURL: "custom-url",
659 BaseURL: "custom-url",
655 },
660 },
656− },
661+ }),
657 }
662 }
658 cfg.setDefaults("/tmp")
663 cfg.setDefaults("/tmp")
659
664
@@ -664,8 +669,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
664 err := cfg.configureProviders(env, resolver, knownProviders)
669 err := cfg.configureProviders(env, resolver, knownProviders)
665 assert.NoError(t, err)
670 assert.NoError(t, err)
666
671
667− assert.Len(t, cfg.Providers, 0)
672+ assert.Equal(t, cfg.Providers.Len(), 0)
668− _, exists := cfg.Providers["vertexai"]
673+ _, exists := cfg.Providers.Get("vertexai")
669 assert.False(t, exists)
674 assert.False(t, exists)
670 })
675 })
671
676
@@ -682,11 +687,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
682 }
687 }
683
688
684 cfg := &Config{
689 cfg := &Config{
685− Providers: map[string]ProviderConfig{
690+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
686 "bedrock": {
691 "bedrock": {
687 BaseURL: "custom-url",
692 BaseURL: "custom-url",
688 },
693 },
689− },
694+ }),
690 }
695 }
691 cfg.setDefaults("/tmp")
696 cfg.setDefaults("/tmp")
692
697
@@ -695,8 +700,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
695 err := cfg.configureProviders(env, resolver, knownProviders)
700 err := cfg.configureProviders(env, resolver, knownProviders)
696 assert.NoError(t, err)
701 assert.NoError(t, err)
697
702
698− assert.Len(t, cfg.Providers, 0)
703+ assert.Equal(t, cfg.Providers.Len(), 0)
699− _, exists := cfg.Providers["bedrock"]
704+ _, exists := cfg.Providers.Get("bedrock")
700 assert.False(t, exists)
705 assert.False(t, exists)
701 })
706 })
702
707
@@ -713,11 +718,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
713 }
718 }
714
719
715 cfg := &Config{
720 cfg := &Config{
716− Providers: map[string]ProviderConfig{
721+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
717 "openai": {
722 "openai": {
718 BaseURL: "custom-url",
723 BaseURL: "custom-url",
719 },
724 },
720− },
725+ }),
721 }
726 }
722 cfg.setDefaults("/tmp")
727 cfg.setDefaults("/tmp")
723
728
@@ -726,8 +731,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
726 err := cfg.configureProviders(env, resolver, knownProviders)
731 err := cfg.configureProviders(env, resolver, knownProviders)
727 assert.NoError(t, err)
732 assert.NoError(t, err)
728
733
729− assert.Len(t, cfg.Providers, 0)
734+ assert.Equal(t, cfg.Providers.Len(), 0)
730− _, exists := cfg.Providers["openai"]
735+ _, exists := cfg.Providers.Get("openai")
731 assert.False(t, exists)
736 assert.False(t, exists)
732 })
737 })
733
738
@@ -744,11 +749,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
744 }
749 }
745
750
746 cfg := &Config{
751 cfg := &Config{
747− Providers: map[string]ProviderConfig{
752+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
748 "openai": {
753 "openai": {
749 APIKey: "test-key",
754 APIKey: "test-key",
750 },
755 },
751− },
756+ }),
752 }
757 }
753 cfg.setDefaults("/tmp")
758 cfg.setDefaults("/tmp")
754
759
@@ -759,8 +764,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
759 err := cfg.configureProviders(env, resolver, knownProviders)
764 err := cfg.configureProviders(env, resolver, knownProviders)
760 assert.NoError(t, err)
765 assert.NoError(t, err)
761
766
762− assert.Len(t, cfg.Providers, 1)
767+ assert.Equal(t, cfg.Providers.Len(), 1)
763− _, exists := cfg.Providers["openai"]
768+ _, exists := cfg.Providers.Get("openai")
764 assert.True(t, exists)
769 assert.True(t, exists)
765 })
770 })
766}
771}
@@ -883,7 +888,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
883 }
888 }
884
889
885 cfg := &Config{
890 cfg := &Config{
886− Providers: map[string]ProviderConfig{
891+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
887 "custom": {
892 "custom": {
888 APIKey: "test-key",
893 APIKey: "test-key",
889 BaseURL: "https://api.custom.com/v1",
894 BaseURL: "https://api.custom.com/v1",
@@ -894,7 +899,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
894 },
899 },
895 },
900 },
896 },
901 },
897− },
902+ }),
898 }
903 }
899 cfg.setDefaults("/tmp")
904 cfg.setDefaults("/tmp")
900 env := env.NewFromMap(map[string]string{})
905 env := env.NewFromMap(map[string]string{})
@@ -932,13 +937,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
932 }
937 }
933
938
934 cfg := &Config{
939 cfg := &Config{
935− Providers: map[string]ProviderConfig{
940+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
936 "custom": {
941 "custom": {
937 APIKey: "test-key",
942 APIKey: "test-key",
938 BaseURL: "https://api.custom.com/v1",
943 BaseURL: "https://api.custom.com/v1",
939 Models: []catwalk.Model{},
944 Models: []catwalk.Model{},
940 },
945 },
941− },
946+ }),
942 }
947 }
943 cfg.setDefaults("/tmp")
948 cfg.setDefaults("/tmp")
944 env := env.NewFromMap(map[string]string{})
949 env := env.NewFromMap(map[string]string{})
@@ -969,7 +974,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
969 }
974 }
970
975
971 cfg := &Config{
976 cfg := &Config{
972− Providers: map[string]ProviderConfig{
977+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
973 "custom": {
978 "custom": {
974 APIKey: "test-key",
979 APIKey: "test-key",
975 BaseURL: "https://api.custom.com/v1",
980 BaseURL: "https://api.custom.com/v1",
@@ -980,7 +985,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
980 },
985 },
981 },
986 },
982 },
987 },
983− },
988+ }),
984 }
989 }
985 cfg.setDefaults("/tmp")
990 cfg.setDefaults("/tmp")
986 env := env.NewFromMap(map[string]string{})
991 env := env.NewFromMap(map[string]string{})
internal/config/provider.go+59−25
@@ -2,10 +2,13 @@ package config
2
2
3import (
3import (
4 "encoding/json"
4 "encoding/json"
5+ "fmt"
6+ "log/slog"
5 "os"
7 "os"
6 "path/filepath"
8 "path/filepath"
7 "runtime"
9 "runtime"
8 "sync"
10 "sync"
11+ "time"
9
12
10 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
11)
14)
@@ -41,57 +44,88 @@ func providerCacheFileData() string {
41}
44}
42
45
43func saveProvidersInCache(path string, providers []catwalk.Provider) error {
46func saveProvidersInCache(path string, providers []catwalk.Provider) error {
44− dir := filepath.Dir(path)
47+ slog.Info("Saving cached provider data", "path", path)
45− if err := os.MkdirAll(dir, 0o755); err != nil {
48+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
46− return err
49+ return fmt.Errorf("failed to create directory for provider cache: %w", err)
47 }
50 }
48
51
49 data, err := json.MarshalIndent(providers, "", " ")
52 data, err := json.MarshalIndent(providers, "", " ")
50 if err != nil {
53 if err != nil {
51− return err
54+ return fmt.Errorf("failed to marshal provider data: %w", err)
52 }
55 }
53
56
54− return os.WriteFile(path, data, 0o644)
57+ if err := os.WriteFile(path, data, 0o644); err != nil {
58+ return fmt.Errorf("failed to write provider data to cache: %w", err)
59+ }
60+ return nil
55}
61}
56
62
57func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
63func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
58 data, err := os.ReadFile(path)
64 data, err := os.ReadFile(path)
59 if err != nil {
65 if err != nil {
60− return nil, err
66+ return nil, fmt.Errorf("failed to read provider cache file: %w", err)
61 }
67 }
62
68
63 var providers []catwalk.Provider
69 var providers []catwalk.Provider
64− err = json.Unmarshal(data, &providers)
70+ if err := json.Unmarshal(data, &providers); err != nil {
65− return providers, err
71+ return nil, fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
66−}
67−
68−func loadProviders(path string, client ProviderClient) ([]catwalk.Provider, error) {
69− providers, err := client.GetProviders()
70− if err != nil {
71− fallbackToCache, err := loadProvidersFromCache(path)
72− if err != nil {
73− return nil, err
74− }
75− providers = fallbackToCache
76− } else {
77− if err := saveProvidersInCache(path, providerList); err != nil {
78− return nil, err
79− }
80 }
72 }
81 return providers, nil
73 return providers, nil
82}
74}
83
75
84func Providers() ([]catwalk.Provider, error) {
76func Providers() ([]catwalk.Provider, error) {
85− return LoadProviders(catwalk.NewWithURL(catwalkURL))
77+ client := catwalk.NewWithURL(catwalkURL)
78+ path := providerCacheFileData()
79+ return loadProvidersOnce(client, path)
86}
80}
87
81
88−func LoadProviders(client ProviderClient) ([]catwalk.Provider, error) {
82+func loadProvidersOnce(client ProviderClient, path string) ([]catwalk.Provider, error) {
89 var err error
83 var err error
90 providerOnce.Do(func() {
84 providerOnce.Do(func() {
91− providerList, err = loadProviders(providerCacheFileData(), client)
85+ providerList, err = loadProviders(client, path)
92 })
86 })
93 if err != nil {
87 if err != nil {
94 return nil, err
88 return nil, err
95 }
89 }
96 return providerList, nil
90 return providerList, nil
97}
91}
92+
93+func loadProviders(client ProviderClient, path string) (providerList []catwalk.Provider, err error) {
94+ // if cache is not stale, load from it
95+ stale, exists := isCacheStale(path)
96+ if !stale {
97+ slog.Info("Using cached provider data", "path", path)
98+ providerList, err = loadProvidersFromCache(path)
99+ if len(providerList) > 0 && err == nil {
100+ go func() {
101+ slog.Info("Updating provider cache in background")
102+ updated, uerr := client.GetProviders()
103+ if len(updated) > 0 && uerr == nil {
104+ _ = saveProvidersInCache(path, updated)
105+ }
106+ }()
107+ return
108+ }
109+ }
110+
111+ slog.Info("Getting live provider data")
112+ providerList, err = client.GetProviders()
113+ if len(providerList) > 0 && err == nil {
114+ err = saveProvidersInCache(path, providerList)
115+ return
116+ }
117+ if !exists {
118+ err = fmt.Errorf("failed to load providers")
119+ return
120+ }
121+ providerList, err = loadProvidersFromCache(path)
122+ return
123+}
124+
125+func isCacheStale(path string) (stale, exists bool) {
126+ info, err := os.Stat(path)
127+ if err != nil {
128+ return true, false
129+ }
130+ return time.Since(info.ModTime()) > 24*time.Hour, true
131+}
internal/config/provider_empty_test.gonew file+47
@@ -0,0 +1,47 @@
1+package config
2+
3+import (
4+ "encoding/json"
5+ "os"
6+ "testing"
7+
8+ "github.com/charmbracelet/catwalk/pkg/catwalk"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type emptyProviderClient struct{}
13+
14+func (m *emptyProviderClient) GetProviders() ([]catwalk.Provider, error) {
15+ return []catwalk.Provider{}, nil
16+}
17+
18+func TestProvider_loadProvidersEmptyResult(t *testing.T) {
19+ client := &emptyProviderClient{}
20+ tmpPath := t.TempDir() + "/providers.json"
21+
22+ providers, err := loadProviders(client, tmpPath)
23+ require.EqualError(t, err, "failed to load providers")
24+ require.Empty(t, providers)
25+ require.Len(t, providers, 0)
26+
27+ // Check that no cache file was created for empty results
28+ require.NoFileExists(t, tmpPath, "Cache file should not exist for empty results")
29+}
30+
31+func TestProvider_loadProvidersEmptyCache(t *testing.T) {
32+ client := &mockProviderClient{shouldFail: false}
33+ tmpPath := t.TempDir() + "/providers.json"
34+
35+ // Create an empty cache file
36+ emptyProviders := []catwalk.Provider{}
37+ data, err := json.Marshal(emptyProviders)
38+ require.NoError(t, err)
39+ require.NoError(t, os.WriteFile(tmpPath, data, 0o644))
40+
41+ // Should refresh and get real providers instead of using empty cache
42+ providers, err := loadProviders(client, tmpPath)
43+ require.NoError(t, err)
44+ require.NotNil(t, providers)
45+ require.Len(t, providers, 1)
46+ require.Equal(t, "Mock", providers[0].Name)
47+}
internal/config/provider_test.go+3−3
@@ -28,7 +28,7 @@ func (m *mockProviderClient) GetProviders() ([]catwalk.Provider, error) {
28func TestProvider_loadProvidersNoIssues(t *testing.T) {
28func TestProvider_loadProvidersNoIssues(t *testing.T) {
29 client := &mockProviderClient{shouldFail: false}
29 client := &mockProviderClient{shouldFail: false}
30 tmpPath := t.TempDir() + "/providers.json"
30 tmpPath := t.TempDir() + "/providers.json"
31− providers, err := loadProviders(tmpPath, client)
31+ providers, err := loadProviders(client, tmpPath)
32 assert.NoError(t, err)
32 assert.NoError(t, err)
33 assert.NotNil(t, providers)
33 assert.NotNil(t, providers)
34 assert.Len(t, providers, 1)
34 assert.Len(t, providers, 1)
@@ -57,7 +57,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
57 if err != nil {
57 if err != nil {
58 t.Fatalf("Failed to write old providers to file: %v", err)
58 t.Fatalf("Failed to write old providers to file: %v", err)
59 }
59 }
60− providers, err := loadProviders(tmpPath, client)
60+ providers, err := loadProviders(client, tmpPath)
61 assert.NoError(t, err)
61 assert.NoError(t, err)
62 assert.NotNil(t, providers)
62 assert.NotNil(t, providers)
63 assert.Len(t, providers, 1)
63 assert.Len(t, providers, 1)
@@ -67,7 +67,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
67func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
67func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
68 client := &mockProviderClient{shouldFail: true}
68 client := &mockProviderClient{shouldFail: true}
69 tmpPath := t.TempDir() + "/providers.json"
69 tmpPath := t.TempDir() + "/providers.json"
70− providers, err := loadProviders(tmpPath, client)
70+ providers, err := loadProviders(client, tmpPath)
71 assert.Error(t, err)
71 assert.Error(t, err)
72 assert.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
72 assert.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
73}
73}
internal/csync/maps.gonew file+92
@@ -0,0 +1,92 @@
1+package csync
2+
3+import (
4+ "encoding/json"
5+ "iter"
6+ "maps"
7+ "sync"
8+)
9+
10+// Map is a concurrent map implementation that provides thread-safe access.
11+type Map[K comparable, V any] struct {
12+ inner map[K]V
13+ mu sync.RWMutex
14+}
15+
16+// NewMap creates a new thread-safe map with the specified key and value types.
17+func NewMap[K comparable, V any]() *Map[K, V] {
18+ return &Map[K, V]{
19+ inner: make(map[K]V),
20+ }
21+}
22+
23+// NewMapFrom creates a new thread-safe map from an existing map.
24+func NewMapFrom[K comparable, V any](m map[K]V) *Map[K, V] {
25+ return &Map[K, V]{
26+ inner: m,
27+ }
28+}
29+
30+// Set sets the value for the specified key in the map.
31+func (m *Map[K, V]) Set(key K, value V) {
32+ m.mu.Lock()
33+ defer m.mu.Unlock()
34+ m.inner[key] = value
35+}
36+
37+// Del deletes the specified key from the map.
38+func (m *Map[K, V]) Del(key K) {
39+ m.mu.Lock()
40+ defer m.mu.Unlock()
41+ delete(m.inner, key)
42+}
43+
44+// Get gets the value for the specified key from the map.
45+func (m *Map[K, V]) Get(key K) (V, bool) {
46+ m.mu.RLock()
47+ defer m.mu.RUnlock()
48+ v, ok := m.inner[key]
49+ return v, ok
50+}
51+
52+// Len returns the number of items in the map.
53+func (m *Map[K, V]) Len() int {
54+ m.mu.RLock()
55+ defer m.mu.RUnlock()
56+ return len(m.inner)
57+}
58+
59+// Seq2 returns an iter.Seq2 that yields key-value pairs from the map.
60+func (m *Map[K, V]) Seq2() iter.Seq2[K, V] {
61+ dst := make(map[K]V)
62+ m.mu.RLock()
63+ maps.Copy(dst, m.inner)
64+ m.mu.RUnlock()
65+ return func(yield func(K, V) bool) {
66+ for k, v := range dst {
67+ if !yield(k, v) {
68+ return
69+ }
70+ }
71+ }
72+}
73+
74+var (
75+ _ json.Unmarshaler = &Map[string, any]{}
76+ _ json.Marshaler = &Map[string, any]{}
77+)
78+
79+// UnmarshalJSON implements json.Unmarshaler.
80+func (m *Map[K, V]) UnmarshalJSON(data []byte) error {
81+ m.mu.Lock()
82+ defer m.mu.Unlock()
83+ m.inner = make(map[K]V)
84+ return json.Unmarshal(data, &m.inner)
85+}
86+
87+// MarshalJSON implements json.Marshaler.
88+func (m *Map[K, V]) MarshalJSON() ([]byte, error) {
89+ m.mu.RLock()
90+ defer m.mu.RUnlock()
91+ return json.Marshal(m.inner)
92+}
internal/csync/maps_test.gonew file+452
@@ -0,0 +1,452 @@
1+package csync
2+
3+import (
4+ "encoding/json"
5+ "maps"
6+ "sync"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+)
11+
12+func TestNewMap(t *testing.T) {
13+ t.Parallel()
14+
15+ m := NewMap[string, int]()
16+ assert.NotNil(t, m)
17+ assert.NotNil(t, m.inner)
18+ assert.Equal(t, 0, m.Len())
19+}
20+
21+func TestNewMapFrom(t *testing.T) {
22+ t.Parallel()
23+
24+ original := map[string]int{
25+ "key1": 1,
26+ "key2": 2,
27+ }
28+
29+ m := NewMapFrom(original)
30+ assert.NotNil(t, m)
31+ assert.Equal(t, original, m.inner)
32+ assert.Equal(t, 2, m.Len())
33+
34+ value, ok := m.Get("key1")
35+ assert.True(t, ok)
36+ assert.Equal(t, 1, value)
37+}
38+
39+func TestMap_Set(t *testing.T) {
40+ t.Parallel()
41+
42+ m := NewMap[string, int]()
43+
44+ m.Set("key1", 42)
45+ value, ok := m.Get("key1")
46+ assert.True(t, ok)
47+ assert.Equal(t, 42, value)
48+ assert.Equal(t, 1, m.Len())
49+
50+ m.Set("key1", 100)
51+ value, ok = m.Get("key1")
52+ assert.True(t, ok)
53+ assert.Equal(t, 100, value)
54+ assert.Equal(t, 1, m.Len())
55+}
56+
57+func TestMap_Get(t *testing.T) {
58+ t.Parallel()
59+
60+ m := NewMap[string, int]()
61+
62+ value, ok := m.Get("nonexistent")
63+ assert.False(t, ok)
64+ assert.Equal(t, 0, value)
65+
66+ m.Set("key1", 42)
67+ value, ok = m.Get("key1")
68+ assert.True(t, ok)
69+ assert.Equal(t, 42, value)
70+}
71+
72+func TestMap_Del(t *testing.T) {
73+ t.Parallel()
74+
75+ m := NewMap[string, int]()
76+ m.Set("key1", 42)
77+ m.Set("key2", 100)
78+
79+ assert.Equal(t, 2, m.Len())
80+
81+ m.Del("key1")
82+ _, ok := m.Get("key1")
83+ assert.False(t, ok)
84+ assert.Equal(t, 1, m.Len())
85+
86+ value, ok := m.Get("key2")
87+ assert.True(t, ok)
88+ assert.Equal(t, 100, value)
89+
90+ m.Del("nonexistent")
91+ assert.Equal(t, 1, m.Len())
92+}
93+
94+func TestMap_Len(t *testing.T) {
95+ t.Parallel()
96+
97+ m := NewMap[string, int]()
98+ assert.Equal(t, 0, m.Len())
99+
100+ m.Set("key1", 1)
101+ assert.Equal(t, 1, m.Len())
102+
103+ m.Set("key2", 2)
104+ assert.Equal(t, 2, m.Len())
105+
106+ m.Del("key1")
107+ assert.Equal(t, 1, m.Len())
108+
109+ m.Del("key2")
110+ assert.Equal(t, 0, m.Len())
111+}
112+
113+func TestMap_Seq2(t *testing.T) {
114+ t.Parallel()
115+
116+ m := NewMap[string, int]()
117+ m.Set("key1", 1)
118+ m.Set("key2", 2)
119+ m.Set("key3", 3)
120+
121+ collected := maps.Collect(m.Seq2())
122+
123+ assert.Equal(t, 3, len(collected))
124+ assert.Equal(t, 1, collected["key1"])
125+ assert.Equal(t, 2, collected["key2"])
126+ assert.Equal(t, 3, collected["key3"])
127+}
128+
129+func TestMap_Seq2_EarlyReturn(t *testing.T) {
130+ t.Parallel()
131+
132+ m := NewMap[string, int]()
133+ m.Set("key1", 1)
134+ m.Set("key2", 2)
135+ m.Set("key3", 3)
136+
137+ count := 0
138+ for range m.Seq2() {
139+ count++
140+ if count == 2 {
141+ break
142+ }
143+ }
144+
145+ assert.Equal(t, 2, count)
146+}
147+
148+func TestMap_Seq2_EmptyMap(t *testing.T) {
149+ t.Parallel()
150+
151+ m := NewMap[string, int]()
152+
153+ count := 0
154+ for range m.Seq2() {
155+ count++
156+ }
157+
158+ assert.Equal(t, 0, count)
159+}
160+
161+func TestMap_MarshalJSON(t *testing.T) {
162+ t.Parallel()
163+
164+ m := NewMap[string, int]()
165+ m.Set("key1", 1)
166+ m.Set("key2", 2)
167+
168+ data, err := json.Marshal(m)
169+ assert.NoError(t, err)
170+
171+ result := &Map[string, int]{}
172+ err = json.Unmarshal(data, result)
173+ assert.NoError(t, err)
174+ assert.Equal(t, 2, result.Len())
175+ v1, _ := result.Get("key1")
176+ v2, _ := result.Get("key2")
177+ assert.Equal(t, 1, v1)
178+ assert.Equal(t, 2, v2)
179+}
180+
181+func TestMap_MarshalJSON_EmptyMap(t *testing.T) {
182+ t.Parallel()
183+
184+ m := NewMap[string, int]()
185+
186+ data, err := json.Marshal(m)
187+ assert.NoError(t, err)
188+ assert.Equal(t, "{}", string(data))
189+}
190+
191+func TestMap_UnmarshalJSON(t *testing.T) {
192+ t.Parallel()
193+
194+ jsonData := `{"key1": 1, "key2": 2}`
195+
196+ m := NewMap[string, int]()
197+ err := json.Unmarshal([]byte(jsonData), m)
198+ assert.NoError(t, err)
199+
200+ assert.Equal(t, 2, m.Len())
201+ value, ok := m.Get("key1")
202+ assert.True(t, ok)
203+ assert.Equal(t, 1, value)
204+
205+ value, ok = m.Get("key2")
206+ assert.True(t, ok)
207+ assert.Equal(t, 2, value)
208+}
209+
210+func TestMap_UnmarshalJSON_EmptyJSON(t *testing.T) {
211+ t.Parallel()
212+
213+ jsonData := `{}`
214+
215+ m := NewMap[string, int]()
216+ err := json.Unmarshal([]byte(jsonData), m)
217+ assert.NoError(t, err)
218+ assert.Equal(t, 0, m.Len())
219+}
220+
221+func TestMap_UnmarshalJSON_InvalidJSON(t *testing.T) {
222+ t.Parallel()
223+
224+ jsonData := `{"key1": 1, "key2":}`
225+
226+ m := NewMap[string, int]()
227+ err := json.Unmarshal([]byte(jsonData), m)
228+ assert.Error(t, err)
229+}
230+
231+func TestMap_UnmarshalJSON_OverwritesExistingData(t *testing.T) {
232+ t.Parallel()
233+
234+ m := NewMap[string, int]()
235+ m.Set("existing", 999)
236+
237+ jsonData := `{"key1": 1, "key2": 2}`
238+ err := json.Unmarshal([]byte(jsonData), m)
239+ assert.NoError(t, err)
240+
241+ assert.Equal(t, 2, m.Len())
242+ _, ok := m.Get("existing")
243+ assert.False(t, ok)
244+
245+ value, ok := m.Get("key1")
246+ assert.True(t, ok)
247+ assert.Equal(t, 1, value)
248+}
249+
250+func TestMap_JSONRoundTrip(t *testing.T) {
251+ t.Parallel()
252+
253+ original := NewMap[string, int]()
254+ original.Set("key1", 1)
255+ original.Set("key2", 2)
256+ original.Set("key3", 3)
257+
258+ data, err := json.Marshal(original)
259+ assert.NoError(t, err)
260+
261+ restored := NewMap[string, int]()
262+ err = json.Unmarshal(data, restored)
263+ assert.NoError(t, err)
264+
265+ assert.Equal(t, original.Len(), restored.Len())
266+
267+ for k, v := range original.Seq2() {
268+ restoredValue, ok := restored.Get(k)
269+ assert.True(t, ok)
270+ assert.Equal(t, v, restoredValue)
271+ }
272+}
273+
274+func TestMap_ConcurrentAccess(t *testing.T) {
275+ t.Parallel()
276+
277+ m := NewMap[int, int]()
278+ const numGoroutines = 100
279+ const numOperations = 100
280+
281+ var wg sync.WaitGroup
282+ wg.Add(numGoroutines)
283+
284+ for i := range numGoroutines {
285+ go func(id int) {
286+ defer wg.Done()
287+ for j := range numOperations {
288+ key := id*numOperations + j
289+ m.Set(key, key*2)
290+ value, ok := m.Get(key)
291+ assert.True(t, ok)
292+ assert.Equal(t, key*2, value)
293+ }
294+ }(i)
295+ }
296+
297+ wg.Wait()
298+
299+ assert.Equal(t, numGoroutines*numOperations, m.Len())
300+}
301+
302+func TestMap_ConcurrentReadWrite(t *testing.T) {
303+ t.Parallel()
304+
305+ m := NewMap[int, int]()
306+ const numReaders = 50
307+ const numWriters = 50
308+ const numOperations = 100
309+
310+ for i := range 1000 {
311+ m.Set(i, i)
312+ }
313+
314+ var wg sync.WaitGroup
315+ wg.Add(numReaders + numWriters)
316+
317+ for range numReaders {
318+ go func() {
319+ defer wg.Done()
320+ for j := range numOperations {
321+ key := j % 1000
322+ value, ok := m.Get(key)
323+ if ok {
324+ assert.Equal(t, key, value)
325+ }
326+ _ = m.Len()
327+ }
328+ }()
329+ }
330+
331+ for i := range numWriters {
332+ go func(id int) {
333+ defer wg.Done()
334+ for j := range numOperations {
335+ key := 1000 + id*numOperations + j
336+ m.Set(key, key)
337+ if j%10 == 0 {
338+ m.Del(key)
339+ }
340+ }
341+ }(i)
342+ }
343+
344+ wg.Wait()
345+}
346+
347+func TestMap_ConcurrentSeq2(t *testing.T) {
348+ t.Parallel()
349+
350+ m := NewMap[int, int]()
351+ for i := range 100 {
352+ m.Set(i, i*2)
353+ }
354+
355+ var wg sync.WaitGroup
356+ const numIterators = 10
357+
358+ wg.Add(numIterators)
359+ for range numIterators {
360+ go func() {
361+ defer wg.Done()
362+ count := 0
363+ for k, v := range m.Seq2() {
364+ assert.Equal(t, k*2, v)
365+ count++
366+ }
367+ assert.Equal(t, 100, count)
368+ }()
369+ }
370+
371+ wg.Wait()
372+}
373+
374+func TestMap_TypeSafety(t *testing.T) {
375+ t.Parallel()
376+
377+ stringIntMap := NewMap[string, int]()
378+ stringIntMap.Set("key", 42)
379+ value, ok := stringIntMap.Get("key")
380+ assert.True(t, ok)
381+ assert.Equal(t, 42, value)
382+
383+ intStringMap := NewMap[int, string]()
384+ intStringMap.Set(42, "value")
385+ strValue, ok := intStringMap.Get(42)
386+ assert.True(t, ok)
387+ assert.Equal(t, "value", strValue)
388+
389+ structMap := NewMap[string, struct{ Name string }]()
390+ structMap.Set("key", struct{ Name string }{Name: "test"})
391+ structValue, ok := structMap.Get("key")
392+ assert.True(t, ok)
393+ assert.Equal(t, "test", structValue.Name)
394+}
395+
396+func TestMap_InterfaceCompliance(t *testing.T) {
397+ t.Parallel()
398+
399+ var _ json.Marshaler = &Map[string, any]{}
400+ var _ json.Unmarshaler = &Map[string, any]{}
401+}
402+
403+func BenchmarkMap_Set(b *testing.B) {
404+ m := NewMap[int, int]()
405+
406+ for i := 0; b.Loop(); i++ {
407+ m.Set(i, i*2)
408+ }
409+}
410+
411+func BenchmarkMap_Get(b *testing.B) {
412+ m := NewMap[int, int]()
413+ for i := range 1000 {
414+ m.Set(i, i*2)
415+ }
416+
417+ for i := 0; b.Loop(); i++ {
418+ m.Get(i % 1000)
419+ }
420+}
421+
422+func BenchmarkMap_Seq2(b *testing.B) {
423+ m := NewMap[int, int]()
424+ for i := range 1000 {
425+ m.Set(i, i*2)
426+ }
427+
428+ for b.Loop() {
429+ for range m.Seq2() {
430+ }
431+ }
432+}
433+
434+func BenchmarkMap_ConcurrentReadWrite(b *testing.B) {
435+ m := NewMap[int, int]()
436+ for i := range 1000 {
437+ m.Set(i, i*2)
438+ }
439+
440+ b.ResetTimer()
441+ b.RunParallel(func(pb *testing.PB) {
442+ i := 0
443+ for pb.Next() {
444+ if i%2 == 0 {
445+ m.Get(i % 1000)
446+ } else {
447+ m.Set(i+1000, i*2)
448+ }
449+ i++
450+ }
451+ })
452+}
internal/csync/slices.gonew file+36
@@ -0,0 +1,36 @@
1+package csync
2+
3+import (
4+ "iter"
5+ "sync"
6+)
7+
8+// LazySlice is a thread-safe lazy-loaded slice.
9+type LazySlice[K any] struct {
10+ inner []K
11+ wg sync.WaitGroup
12+}
13+
14+// NewLazySlice creates a new slice and runs the [load] function in a goroutine
15+// to populate it.
16+func NewLazySlice[K any](load func() []K) *LazySlice[K] {
17+ s := &LazySlice[K]{}
18+ s.wg.Add(1)
19+ go func() {
20+ s.inner = load()
21+ s.wg.Done()
22+ }()
23+ return s
24+}
25+
26+// Seq returns an iterator that yields elements from the slice.
27+func (s *LazySlice[K]) Seq() iter.Seq[K] {
28+ s.wg.Wait()
29+ return func(yield func(K) bool) {
30+ for _, v := range s.inner {
31+ if !yield(v) {
32+ return
33+ }
34+ }
35+ }
36+}
internal/csync/slices_test.gonew file+87
@@ -0,0 +1,87 @@
1+package csync
2+
3+import (
4+ "sync/atomic"
5+ "testing"
6+ "time"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestLazySlice_Seq(t *testing.T) {
12+ t.Parallel()
13+
14+ data := []string{"a", "b", "c"}
15+ s := NewLazySlice(func() []string {
16+ // TODO: use synctest when new Go is out.
17+ time.Sleep(10 * time.Millisecond) // Small delay to ensure loading happens
18+ return data
19+ })
20+
21+ var result []string
22+ for v := range s.Seq() {
23+ result = append(result, v)
24+ }
25+
26+ assert.Equal(t, data, result)
27+}
28+
29+func TestLazySlice_SeqWaitsForLoading(t *testing.T) {
30+ t.Parallel()
31+
32+ var loaded atomic.Bool
33+ data := []string{"x", "y", "z"}
34+
35+ s := NewLazySlice(func() []string {
36+ // TODO: use synctest when new Go is out.
37+ time.Sleep(100 * time.Millisecond)
38+ loaded.Store(true)
39+ return data
40+ })
41+
42+ assert.False(t, loaded.Load(), "should not be loaded immediately")
43+
44+ var result []string
45+ for v := range s.Seq() {
46+ result = append(result, v)
47+ }
48+
49+ assert.True(t, loaded.Load(), "should be loaded after Seq")
50+ assert.Equal(t, data, result)
51+}
52+
53+func TestLazySlice_EmptySlice(t *testing.T) {
54+ t.Parallel()
55+
56+ s := NewLazySlice(func() []string {
57+ return []string{}
58+ })
59+
60+ var result []string
61+ for v := range s.Seq() {
62+ result = append(result, v)
63+ }
64+
65+ assert.Empty(t, result)
66+}
67+
68+func TestLazySlice_EarlyBreak(t *testing.T) {
69+ t.Parallel()
70+
71+ data := []string{"a", "b", "c", "d", "e"}
72+ s := NewLazySlice(func() []string {
73+ // TODO: use synctest when new Go is out.
74+ time.Sleep(10 * time.Millisecond) // Small delay to ensure loading happens
75+ return data
76+ })
77+
78+ var result []string
79+ for v := range s.Seq() {
80+ result = append(result, v)
81+ if len(result) == 2 {
82+ break
83+ }
84+ }
85+
86+ assert.Equal(t, []string{"a", "b"}, result)
87+}
internal/llm/agent/agent.go+51−43
@@ -12,6 +12,7 @@ import (
12
12
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
15+ "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/provider"
18 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -68,7 +69,8 @@ type agent struct {
68 sessions session.Service
69 sessions session.Service
69 messages message.Service
70 messages message.Service
70
71
71− tools []tools.BaseTool
72+ tools *csync.LazySlice[tools.BaseTool]
73+
72 provider provider.Provider
74 provider provider.Provider
73 providerID string
75 providerID string
74
76
@@ -95,25 +97,8 @@ func NewAgent(
95) (Service, error) {
97) (Service, error) {
96 ctx := context.Background()
98 ctx := context.Background()
97 cfg := config.Get()
99 cfg := config.Get()
98− otherTools := GetMCPTools(ctx, permissions, cfg)
99− if len(lspClients) > 0 {
100− otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
101− }
102−
103− cwd := cfg.WorkingDir()
104− allTools := []tools.BaseTool{
105− tools.NewBashTool(permissions, cwd),
106− tools.NewDownloadTool(permissions, cwd),
107− tools.NewEditTool(lspClients, permissions, history, cwd),
108− tools.NewFetchTool(permissions, cwd),
109− tools.NewGlobTool(cwd),
110− tools.NewGrepTool(cwd),
111− tools.NewLsTool(cwd),
112− tools.NewSourcegraphTool(),
113− tools.NewViewTool(lspClients, cwd),
114− tools.NewWriteTool(lspClients, permissions, history, cwd),
115− }
116
100
101+ var agentTool tools.BaseTool
117 if agentCfg.ID == "coder" {
102 if agentCfg.ID == "coder" {
118 taskAgentCfg := config.Get().Agents["task"]
103 taskAgentCfg := config.Get().Agents["task"]
119 if taskAgentCfg.ID == "" {
104 if taskAgentCfg.ID == "" {
@@ -124,17 +109,9 @@ func NewAgent(
124 return nil, fmt.Errorf("failed to create task agent: %w", err)
109 return nil, fmt.Errorf("failed to create task agent: %w", err)
125 }
110 }
126
111
127− allTools = append(
128− allTools,
129− NewAgentTool(
112+ agentTool = NewAgentTool(taskAgent, sessions, messages)
130− taskAgent,
131− sessions,
132− messages,
133− ),
134− )
135 }
113 }
136
114
137− allTools = append(allTools, otherTools...)
138 providerCfg := config.Get().GetProviderForModel(agentCfg.Model)
115 providerCfg := config.Get().GetProviderForModel(agentCfg.Model)
139 if providerCfg == nil {
116 if providerCfg == nil {
140 return nil, fmt.Errorf("provider for agent %s not found in config", agentCfg.Name)
117 return nil, fmt.Errorf("provider for agent %s not found in config", agentCfg.Name)
@@ -191,32 +168,63 @@ func NewAgent(
191 return nil, err
168 return nil, err
192 }
169 }
193
170
194− agentTools := []tools.BaseTool{}
171+ toolFn := func() []tools.BaseTool {
172+ slog.Info("Initializing agent tools", "agent", agentCfg.ID)
173+ defer func() {
174+ slog.Info("Initialized agent tools", "agent", agentCfg.ID)
175+ }()
176+
177+ cwd := cfg.WorkingDir()
178+ allTools := []tools.BaseTool{
179+ tools.NewBashTool(permissions, cwd),
180+ tools.NewDownloadTool(permissions, cwd),
181+ tools.NewEditTool(lspClients, permissions, history, cwd),
182+ tools.NewFetchTool(permissions, cwd),
183+ tools.NewGlobTool(cwd),
184+ tools.NewGrepTool(cwd),
185+ tools.NewLsTool(cwd),
186+ tools.NewSourcegraphTool(),
187+ tools.NewViewTool(lspClients, cwd),
188+ tools.NewWriteTool(lspClients, permissions, history, cwd),
189+ }
190+
191+ mcpTools := GetMCPTools(ctx, permissions, cfg)
192+ allTools = append(allTools, mcpTools...)
193+
194+ if len(lspClients) > 0 {
195+ allTools = append(allTools, tools.NewDiagnosticsTool(lspClients))
196+ }
197+
198+ if agentTool != nil {
199+ allTools = append(allTools, agentTool)
200+ }
201+
195− if agentCfg.AllowedTools == nil {
202+ if agentCfg.AllowedTools == nil {
196− agentTools = allTools
203+ return allTools
197− } else {
204+ }
205+
206+ var filteredTools []tools.BaseTool
198 for _, tool := range allTools {
207 for _, tool := range allTools {
199 if slices.Contains(agentCfg.AllowedTools, tool.Name()) {
208 if slices.Contains(agentCfg.AllowedTools, tool.Name()) {
200− agentTools = append(agentTools, tool)
209+ filteredTools = append(filteredTools, tool)
201 }
210 }
202 }
211 }
212+ return filteredTools
203 }
213 }
204
214
205− agent := &agent{
215+ return &agent{
206 Broker: pubsub.NewBroker[AgentEvent](),
216 Broker: pubsub.NewBroker[AgentEvent](),
207 agentCfg: agentCfg,
217 agentCfg: agentCfg,
208 provider: agentProvider,
218 provider: agentProvider,
209 providerID: string(providerCfg.ID),
219 providerID: string(providerCfg.ID),
210 messages: messages,
220 messages: messages,
211 sessions: sessions,
221 sessions: sessions,
212− tools: agentTools,
213 titleProvider: titleProvider,
222 titleProvider: titleProvider,
214 summarizeProvider: summarizeProvider,
223 summarizeProvider: summarizeProvider,
215 summarizeProviderID: string(smallModelProviderCfg.ID),
224 summarizeProviderID: string(smallModelProviderCfg.ID),
216 activeRequests: sync.Map{},
225 activeRequests: sync.Map{},
217− }
226+ tools: csync.NewLazySlice(toolFn),
218−
227+ }, nil
219− return agent, nil
220}
228}
221
229
222func (a *agent) Model() catwalk.Model {
230func (a *agent) Model() catwalk.Model {
@@ -284,7 +292,7 @@ func (a *agent) generateTitle(ctx context.Context, sessionID string, content str
284 Parts: parts,
292 Parts: parts,
285 },
293 },
286 },
294 },
287− make([]tools.BaseTool, 0),
295+ nil,
288 )
296 )
289
297
290 var finalResponse *provider.ProviderResponse
298 var finalResponse *provider.ProviderResponse
@@ -438,7 +446,7 @@ func (a *agent) createUserMessage(ctx context.Context, sessionID, content string
438
446
439func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
447func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
440 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
448 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
441− eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools)
449+ eventChan := a.provider.StreamResponse(ctx, msgHistory, slices.Collect(a.tools.Seq()))
442
450
443 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
451 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
444 Role: message.Assistant,
452 Role: message.Assistant,
@@ -487,7 +495,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
487 default:
495 default:
488 // Continue processing
496 // Continue processing
489 var tool tools.BaseTool
497 var tool tools.BaseTool
490− for _, availableTool := range a.tools {
498+ for availableTool := range a.tools.Seq() {
491 if availableTool.Info().Name == toolCall.Name {
499 if availableTool.Info().Name == toolCall.Name {
492 tool = availableTool
500 tool = availableTool
493 break
501 break
@@ -737,7 +745,7 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
737 response := a.summarizeProvider.StreamResponse(
745 response := a.summarizeProvider.StreamResponse(
738 summarizeCtx,
746 summarizeCtx,
739 msgsWithPrompt,
747 msgsWithPrompt,
740− make([]tools.BaseTool, 0),
748+ nil,
741 )
749 )
742 var finalResponse *provider.ProviderResponse
750 var finalResponse *provider.ProviderResponse
743 for r := range response {
751 for r := range response {
@@ -899,7 +907,7 @@ func (a *agent) UpdateModel() error {
899 smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
907 smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
900 var smallModelProviderCfg config.ProviderConfig
908 var smallModelProviderCfg config.ProviderConfig
901
909
902− for _, p := range cfg.Providers {
910+ for _, p := range cfg.Providers.Seq2() {
903 if p.ID == smallModelCfg.Provider {
911 if p.ID == smallModelCfg.Provider {
904 smallModelProviderCfg = p
912 smallModelProviderCfg = p
905 break
913 break
internal/llm/provider/gemini.go+2−6
@@ -188,9 +188,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
188 Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
188 Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
189 },
189 },
190 }
190 }
191− if len(tools) > 0 {
192− config.Tools = g.convertTools(tools)
191+ config.Tools = g.convertTools(tools)
193− }
194 chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
192 chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
195
193
196 attempts := 0
194 attempts := 0
@@ -290,9 +288,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
290 Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
288 Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
291 },
289 },
292 }
290 }
293− if len(tools) > 0 {
294− config.Tools = g.convertTools(tools)
291+ config.Tools = g.convertTools(tools)
295− }
296 chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
292 chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
297
293
298 attempts := 0
294 attempts := 0
internal/llm/provider/openai_test.go+1−2
@@ -11,7 +11,6 @@ import (
11
11
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/message"
14 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
15 "github.com/openai/openai-go"
17 "github.com/openai/openai-go/option"
16 "github.com/openai/openai-go/option"
@@ -79,7 +78,7 @@ func TestOpenAIClientStreamChoices(t *testing.T) {
79 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
78 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
80 defer cancel()
79 defer cancel()
81
80
82− eventsChan := client.stream(ctx, messages, []tools.BaseTool{})
81+ eventsChan := client.stream(ctx, messages, nil)
83
82
84 // Collect events - this will panic without the bounds check
83 // Collect events - this will panic without the bounds check
85 for event := range eventsChan {
84 for event := range eventsChan {
internal/tui/components/chat/splash/splash.go+1−1
@@ -425,7 +425,7 @@ func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.
425
425
426func (s *splashCmp) isProviderConfigured(providerID string) bool {
426func (s *splashCmp) isProviderConfigured(providerID string) bool {
427 cfg := config.Get()
427 cfg := config.Get()
428− if _, ok := cfg.Providers[providerID]; ok {
428+ if _, ok := cfg.Providers.Get(providerID); ok {
429 return true
429 return true
430 }
430 }
431 return false
431 return false
internal/tui/components/dialogs/models/list.go+3−3
@@ -103,7 +103,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
103 if err != nil {
103 if err != nil {
104 return util.ReportError(err)
104 return util.ReportError(err)
105 }
105 }
106− for providerID, providerConfig := range cfg.Providers {
106+ for providerID, providerConfig := range cfg.Providers.Seq2() {
107 if providerConfig.Disable {
107 if providerConfig.Disable {
108 continue
108 continue
109 }
109 }
@@ -164,7 +164,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
164 }
164 }
165
165
166 // Check if this provider is configured and not disabled
166 // Check if this provider is configured and not disabled
167− if providerConfig, exists := cfg.Providers[string(provider.ID)]; exists && providerConfig.Disable {
167+ if providerConfig, exists := cfg.Providers.Get(string(provider.ID)); exists && providerConfig.Disable {
168 continue
168 continue
169 }
169 }
170
170
@@ -174,7 +174,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
174 }
174 }
175
175
176 section := commands.NewItemSection(name)
176 section := commands.NewItemSection(name)
177− if _, ok := cfg.Providers[string(provider.ID)]; ok {
177+ if _, ok := cfg.Providers.Get(string(provider.ID)); ok {
178 section.SetInfo(configured)
178 section.SetInfo(configured)
179 }
179 }
180 modelItems = append(modelItems, section)
180 modelItems = append(modelItems, section)
internal/tui/components/dialogs/models/models.go+1−1
@@ -357,7 +357,7 @@ func (m *modelDialogCmp) modelTypeRadio() string {
357
357
358func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
358func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
359 cfg := config.Get()
359 cfg := config.Get()
360− if _, ok := cfg.Providers[providerID]; ok {
360+ if _, ok := cfg.Providers.Get(providerID); ok {
361 return true
361 return true
362 }
362 }
363 return false
363 return false