on main help sign in
feat: improve providers startup (#287)

* feat: check providers in background Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * test: test csync.Map * fix: improvements --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

commitdf56ffd4b1875a04c7b157ac5308decc6e0bab8d authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateTue 22 Jul 2025, 16:52 (1 year ago) parente25a42c
11 files changed+841−187
csync/slices.go+34 csync/slices_test.go+86 internal/config/config.go+9−12 internal/config/load.go+45−40 internal/config/load_test.go+106−101 internal/csync/maps.go+84 internal/csync/maps_test.go+450 internal/llm/agent/agent.go+22−29 internal/tui/components/chat/splash/splash.go+1−1 internal/tui/components/dialogs/models/list.go+3−3 internal/tui/components/dialogs/models/models.go+1−1
csync/slices.gonew file+34
@@ -0,0 +1,34 @@
1+package csync
2+
3+import (
4+ "iter"
5+ "sync"
6+)
7+
8+type LazySlice[K any] struct {
9+ inner []K
10+ mu sync.Mutex
11+}
12+
13+func NewLazySlice[K any](load func() []K) *LazySlice[K] {
14+ s := &LazySlice[K]{}
15+ s.mu.Lock()
16+ go func() {
17+ s.inner = load()
18+ s.mu.Unlock()
19+ }()
20+ return s
21+}
22+
23+func (s *LazySlice[K]) Iter() iter.Seq[K] {
24+ s.mu.Lock()
25+ inner := s.inner
26+ s.mu.Unlock()
27+ return func(yield func(K) bool) {
28+ for _, v := range inner {
29+ if !yield(v) {
30+ return
31+ }
32+ }
33+ }
34+}
csync/slices_test.gonew file+86
@@ -0,0 +1,86 @@
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_Iter(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.Iter() {
23+ result = append(result, v)
24+ }
25+
26+ assert.Equal(t, data, result)
27+}
28+
29+func TestLazySlice_IterWaitsForLoading(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.Iter() {
46+ result = append(result, v)
47+ }
48+
49+ assert.True(t, loaded.Load(), "should be loaded after Iter")
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.Iter() {
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+ time.Sleep(10 * time.Millisecond) // Small delay to ensure loading happens
74+ return data
75+ })
76+
77+ var result []string
78+ for v := range s.Iter() {
79+ result = append(result, v)
80+ if len(result) == 2 {
81+ break
82+ }
83+ }
84+
85+ assert.Equal(t, []string{"a", "b"}, result)
86+}
internal/config/config.go+9−12
@@ -9,6 +9,7 @@ import (
99 "strings"
1010 "time"
1111
12+ "github.com/charmbracelet/crush/internal/csync"
1213 "github.com/charmbracelet/crush/internal/env"
1314 "github.com/charmbracelet/crush/internal/fur/provider"
1415 "github.com/tidwall/sjson"
@@ -236,7 +237,7 @@ type Config struct {
236237 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
237238
238239 // The providers that are configured
239− Providers map[string]ProviderConfig `json:"providers,omitempty"`
240+ Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty"`
240241
241242 MCP MCPs `json:"mcp,omitempty"`
242243
@@ -259,8 +260,8 @@ func (c *Config) WorkingDir() string {
259260}
260261
261262func (c *Config) EnabledProviders() []ProviderConfig {
262− enabled := make([]ProviderConfig, 0, len(c.Providers))
263− for _, p := range c.Providers {
263+ var enabled []ProviderConfig
264+ for _, p := range c.Providers.Seq2() {
264265 if !p.Disable {
265266 enabled = append(enabled, p)
266267 }
@@ -274,7 +275,7 @@ func (c *Config) IsConfigured() bool {
274275}
275276
276277func (c *Config) GetModel(provider, model string) *provider.Model {
277− if providerConfig, ok := c.Providers[provider]; ok {
278+ if providerConfig, ok := c.Providers.Get(provider); ok {
278279 for _, m := range providerConfig.Models {
279280 if m.ID == model {
280281 return &m
@@ -289,7 +290,7 @@ func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfi
289290 if !ok {
290291 return nil
291292 }
292− if providerConfig, ok := c.Providers[model.Provider]; ok {
293+ if providerConfig, ok := c.Providers.Get(model.Provider); ok {
293294 return &providerConfig
294295 }
295296 return nil
@@ -370,14 +371,10 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
370371 return fmt.Errorf("failed to save API key to config file: %w", err)
371372 }
372373
373− if c.Providers == nil {
374− c.Providers = make(map[string]ProviderConfig)
375− }
376−
377− providerConfig, exists := c.Providers[providerID]
374+ providerConfig, exists := c.Providers.Get(providerID)
378375 if exists {
379376 providerConfig.APIKey = apiKey
380− c.Providers[providerID] = providerConfig
377+ c.Providers.Set(providerID, providerConfig)
381378 return nil
382379 }
383380
@@ -406,7 +403,7 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
406403 return fmt.Errorf("provider with ID %s not found in known providers", providerID)
407404 }
408405 // Store the updated provider config
409− c.Providers[providerID] = providerConfig
406+ c.Providers.Set(providerID, providerConfig)
410407 return nil
411408}
412409
internal/config/load.go+45−40
@@ -11,6 +11,7 @@ import (
1111 "strings"
1212 "sync"
1313
14+ "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/env"
1516 "github.com/charmbracelet/crush/internal/fur/client"
1617 "github.com/charmbracelet/crush/internal/fur/provider"
@@ -80,30 +81,34 @@ func Load(workingDir string, debug bool) (*Config, error) {
8081 var testResults sync.Map
8182 var wg sync.WaitGroup
8283
83− for _, p := range cfg.Providers {
84− if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
85− wg.Add(1)
86− go func(provider ProviderConfig) {
87− defer wg.Done()
88− err := provider.TestConnection(cfg.resolver)
89− testResults.Store(provider.ID, err == nil)
90− if err != nil {
91− slog.Error("Provider connection test failed", "provider", provider.ID, "error", err)
92− }
93− }(p)
94− }
95− }
96− wg.Wait()
97−
98− // Remove failed providers
99− testResults.Range(func(key, value any) bool {
100− providerID := key.(string)
101− passed := value.(bool)
102− if !passed {
103− delete(cfg.Providers, providerID)
84+ go func() {
85+ slog.Info("Testing provider connections")
86+ defer slog.Info("Provider connection tests completed")
87+ for _, p := range cfg.Providers.Seq2() {
88+ if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
89+ wg.Add(1)
90+ go func(provider ProviderConfig) {
91+ defer wg.Done()
92+ err := provider.TestConnection(cfg.resolver)
93+ testResults.Store(provider.ID, err == nil)
94+ if err != nil {
95+ slog.Error("Provider connection test failed", "provider", provider.ID, "error", err)
96+ }
97+ }(p)
98+ }
10499 }
105− return true
106− })
100+ wg.Wait()
101+
102+ // Remove failed providers
103+ testResults.Range(func(key, value any) bool {
104+ providerID := key.(string)
105+ passed := value.(bool)
106+ if !passed {
107+ cfg.Providers.Del(providerID)
108+ }
109+ return true
110+ })
111+ }()
107112
108113 if !cfg.IsConfigured() {
109114 slog.Warn("No providers configured")
@@ -121,12 +126,12 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
121126 knownProviderNames := make(map[string]bool)
122127 for _, p := range knownProviders {
123128 knownProviderNames[string(p.ID)] = true
124− config, configExists := c.Providers[string(p.ID)]
129+ config, configExists := c.Providers.Get(string(p.ID))
125130 // if the user configured a known provider we need to allow it to override a couple of parameters
126131 if configExists {
127132 if config.Disable {
128133 slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
129− delete(c.Providers, string(p.ID))
134+ c.Providers.Del(string(p.ID))
130135 continue
131136 }
132137 if config.BaseURL != "" {
@@ -182,7 +187,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
182187 if !hasVertexCredentials(env) {
183188 if configExists {
184189 slog.Warn("Skipping Vertex AI provider due to missing credentials")
185− delete(c.Providers, string(p.ID))
190+ c.Providers.Del(string(p.ID))
186191 }
187192 continue
188193 }
@@ -193,7 +198,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
193198 if err != nil || endpoint == "" {
194199 if configExists {
195200 slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
196− delete(c.Providers, string(p.ID))
201+ c.Providers.Del(string(p.ID))
197202 }
198203 continue
199204 }
@@ -203,7 +208,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
203208 if !hasAWSCredentials(env) {
204209 if configExists {
205210 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
206− delete(c.Providers, string(p.ID))
211+ c.Providers.Del(string(p.ID))
207212 }
208213 continue
209214 }
@@ -218,16 +223,16 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
218223 if v == "" || err != nil {
219224 if configExists {
220225 slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
221− delete(c.Providers, string(p.ID))
226+ c.Providers.Del(string(p.ID))
222227 }
223228 continue
224229 }
225230 }
226− c.Providers[string(p.ID)] = prepared
231+ c.Providers.Set(string(p.ID), prepared)
227232 }
228233
229234 // validate the custom providers
230− for id, providerConfig := range c.Providers {
235+ for id, providerConfig := range c.Providers.Seq2() {
231236 if knownProviderNames[id] {
232237 continue
233238 }
@@ -244,7 +249,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
244249
245250 if providerConfig.Disable {
246251 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
247− delete(c.Providers, id)
252+ c.Providers.Del(id)
248253 continue
249254 }
250255 if providerConfig.APIKey == "" {
@@ -252,17 +257,17 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
252257 }
253258 if providerConfig.BaseURL == "" {
254259 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
255− delete(c.Providers, id)
260+ c.Providers.Del(id)
256261 continue
257262 }
258263 if len(providerConfig.Models) == 0 {
259264 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
260− delete(c.Providers, id)
265+ c.Providers.Del(id)
261266 continue
262267 }
263268 if providerConfig.Type != provider.TypeOpenAI {
264269 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
265− delete(c.Providers, id)
270+ c.Providers.Del(id)
266271 continue
267272 }
268273
@@ -273,11 +278,11 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
273278 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
274279 if baseURL == "" || err != nil {
275280 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
276− delete(c.Providers, id)
281+ c.Providers.Del(id)
277282 continue
278283 }
279284
280− c.Providers[id] = providerConfig
285+ c.Providers.Set(id, providerConfig)
281286 }
282287 return nil
283288}
@@ -297,7 +302,7 @@ func (c *Config) setDefaults(workingDir string) {
297302 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
298303 }
299304 if c.Providers == nil {
300− c.Providers = make(map[string]ProviderConfig)
305+ c.Providers = csync.NewMap[string, ProviderConfig]()
301306 }
302307 if c.Models == nil {
303308 c.Models = make(map[SelectedModelType]SelectedModel)
@@ -316,7 +321,7 @@ func (c *Config) setDefaults(workingDir string) {
316321}
317322
318323func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
319− if len(knownProviders) == 0 && len(c.Providers) == 0 {
324+ if len(knownProviders) == 0 { // TODO:}&& len(c.Providers) == 0 {
320325 err = fmt.Errorf("no providers configured, please configure at least one provider")
321326 return
322327 }
@@ -324,7 +329,7 @@ func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (larg
324329 // Use the first provider enabled based on the known providers order
325330 // if no provider found that is known use the first provider configured
326331 for _, p := range knownProviders {
327− providerConfig, ok := c.Providers[string(p.ID)]
332+ providerConfig, ok := c.Providers.Get(string(p.ID))
328333 if !ok || providerConfig.Disable {
329334 continue
330335 }
internal/config/load_test.go+106−101
@@ -8,6 +8,7 @@ import (
88 "strings"
99 "testing"
1010
11+ "github.com/charmbracelet/crush/internal/csync"
1112 "github.com/charmbracelet/crush/internal/env"
1213 "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/stretchr/testify/assert"
@@ -29,9 +30,10 @@ func TestConfig_LoadFromReaders(t *testing.T) {
2930
3031 assert.NoError(t, err)
3132 assert.NotNil(t, loadedConfig)
32− assert.Len(t, loadedConfig.Providers, 1)
33− assert.Equal(t, "key2", loadedConfig.Providers["openai"].APIKey)
34− assert.Equal(t, "https://api.openai.com/v2", loadedConfig.Providers["openai"].BaseURL)
33+ assert.Equal(t, 1, loadedConfig.Providers.Len())
34+ pc, _ := loadedConfig.Providers.Get("openai")
35+ assert.Equal(t, "key2", pc.APIKey)
36+ assert.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
3537}
3638
3739func TestConfig_setDefaults(t *testing.T) {
@@ -73,10 +75,11 @@ func TestConfig_configureProviders(t *testing.T) {
7375 resolver := NewEnvironmentVariableResolver(env)
7476 err := cfg.configureProviders(env, resolver, knownProviders)
7577 assert.NoError(t, err)
76− assert.Len(t, cfg.Providers, 1)
78+ assert.Equal(t, 1, cfg.Providers.Len())
7779
7880 // We want to make sure that we keep the configured API key as a placeholder
79− assert.Equal(t, "$OPENAI_API_KEY", cfg.Providers["openai"].APIKey)
81+ pc, _ := cfg.Providers.Get("openai")
82+ assert.Equal(t, "$OPENAI_API_KEY", pc.APIKey)
8083}
8184
8285func TestConfig_configureProvidersWithOverride(t *testing.T) {
@@ -92,22 +95,21 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
9295 }
9396
9497 cfg := &Config{
95− Providers: map[string]ProviderConfig{
96− "openai": {
97− APIKey: "xyz",
98− BaseURL: "https://api.openai.com/v2",
99− Models: []provider.Model{
100− {
101− ID: "test-model",
102− Model: "Updated",
103− },
104− {
105− ID: "another-model",
106− },
107− },
98+ Providers: csync.NewMap[string, ProviderConfig](),
99+ }
100+ cfg.Providers.Set("openai", ProviderConfig{
101+ APIKey: "xyz",
102+ BaseURL: "https://api.openai.com/v2",
103+ Models: []provider.Model{
104+ {
105+ ID: "test-model",
106+ Model: "Updated",
107+ },
108+ {
109+ ID: "another-model",
108110 },
109111 },
110− }
112+ })
111113 cfg.setDefaults("/tmp")
112114
113115 env := env.NewFromMap(map[string]string{
@@ -116,13 +118,14 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
116118 resolver := NewEnvironmentVariableResolver(env)
117119 err := cfg.configureProviders(env, resolver, knownProviders)
118120 assert.NoError(t, err)
119− assert.Len(t, cfg.Providers, 1)
121+ assert.Equal(t, 1, cfg.Providers.Len())
120122
121123 // We want to make sure that we keep the configured API key as a placeholder
122− assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
123− assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
124− assert.Len(t, cfg.Providers["openai"].Models, 2)
125− assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Model)
124+ pc, _ := cfg.Providers.Get("openai")
125+ assert.Equal(t, "xyz", pc.APIKey)
126+ assert.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
127+ assert.Len(t, pc.Models, 2)
128+ assert.Equal(t, "Updated", pc.Models[0].Model)
126129}
127130
128131func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
@@ -138,7 +141,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
138141 }
139142
140143 cfg := &Config{
141− Providers: map[string]ProviderConfig{
144+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
142145 "custom": {
143146 APIKey: "xyz",
144147 BaseURL: "https://api.someendpoint.com/v2",
@@ -148,7 +151,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
148151 },
149152 },
150153 },
151− },
154+ }),
152155 }
153156 cfg.setDefaults("/tmp")
154157 env := env.NewFromMap(map[string]string{
@@ -158,16 +161,17 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
158161 err := cfg.configureProviders(env, resolver, knownProviders)
159162 assert.NoError(t, err)
160163 // Should be to because of the env variable
161− assert.Len(t, cfg.Providers, 2)
164+ assert.Equal(t, cfg.Providers.Len(), 2)
162165
163166 // We want to make sure that we keep the configured API key as a placeholder
164− assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
167+ pc, _ := cfg.Providers.Get("custom")
168+ assert.Equal(t, "xyz", pc.APIKey)
165169 // Make sure we set the ID correctly
166− assert.Equal(t, "custom", cfg.Providers["custom"].ID)
167− assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
168− assert.Len(t, cfg.Providers["custom"].Models, 1)
170+ assert.Equal(t, "custom", pc.ID)
171+ assert.Equal(t, "https://api.someendpoint.com/v2", pc.BaseURL)
172+ assert.Len(t, pc.Models, 1)
169173
170− _, ok := cfg.Providers["openai"]
174+ _, ok := cfg.Providers.Get("openai")
171175 assert.True(t, ok, "OpenAI provider should still be present")
172176}
173177
@@ -192,9 +196,9 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
192196 resolver := NewEnvironmentVariableResolver(env)
193197 err := cfg.configureProviders(env, resolver, knownProviders)
194198 assert.NoError(t, err)
195− assert.Len(t, cfg.Providers, 1)
199+ assert.Equal(t, cfg.Providers.Len(), 1)
196200
197− bedrockProvider, ok := cfg.Providers["bedrock"]
201+ bedrockProvider, ok := cfg.Providers.Get("bedrock")
198202 assert.True(t, ok, "Bedrock provider should be present")
199203 assert.Len(t, bedrockProvider.Models, 1)
200204 assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
@@ -219,7 +223,7 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
219223 err := cfg.configureProviders(env, resolver, knownProviders)
220224 assert.NoError(t, err)
221225 // Provider should not be configured without credentials
222− assert.Len(t, cfg.Providers, 0)
226+ assert.Equal(t, cfg.Providers.Len(), 0)
223227}
224228
225229func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
@@ -267,9 +271,9 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
267271 resolver := NewEnvironmentVariableResolver(env)
268272 err := cfg.configureProviders(env, resolver, knownProviders)
269273 assert.NoError(t, err)
270− assert.Len(t, cfg.Providers, 1)
274+ assert.Equal(t, cfg.Providers.Len(), 1)
271275
272− vertexProvider, ok := cfg.Providers["vertexai"]
276+ vertexProvider, ok := cfg.Providers.Get("vertexai")
273277 assert.True(t, ok, "VertexAI provider should be present")
274278 assert.Len(t, vertexProvider.Models, 1)
275279 assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
@@ -300,7 +304,7 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
300304 err := cfg.configureProviders(env, resolver, knownProviders)
301305 assert.NoError(t, err)
302306 // Provider should not be configured without proper credentials
303− assert.Len(t, cfg.Providers, 0)
307+ assert.Equal(t, cfg.Providers.Len(), 0)
304308}
305309
306310func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
@@ -325,7 +329,7 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
325329 err := cfg.configureProviders(env, resolver, knownProviders)
326330 assert.NoError(t, err)
327331 // Provider should not be configured without project
328− assert.Len(t, cfg.Providers, 0)
332+ assert.Equal(t, cfg.Providers.Len(), 0)
329333}
330334
331335func TestConfig_configureProvidersSetProviderID(t *testing.T) {
@@ -348,16 +352,17 @@ func TestConfig_configureProvidersSetProviderID(t *testing.T) {
348352 resolver := NewEnvironmentVariableResolver(env)
349353 err := cfg.configureProviders(env, resolver, knownProviders)
350354 assert.NoError(t, err)
351− assert.Len(t, cfg.Providers, 1)
355+ assert.Equal(t, cfg.Providers.Len(), 1)
352356
353357 // Provider ID should be set
354− assert.Equal(t, "openai", cfg.Providers["openai"].ID)
358+ pc, _ := cfg.Providers.Get("openai")
359+ assert.Equal(t, "openai", pc.ID)
355360}
356361
357362func TestConfig_EnabledProviders(t *testing.T) {
358363 t.Run("all providers enabled", func(t *testing.T) {
359364 cfg := &Config{
360− Providers: map[string]ProviderConfig{
365+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
361366 "openai": {
362367 ID: "openai",
363368 APIKey: "key1",
@@ -368,7 +373,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
368373 APIKey: "key2",
369374 Disable: false,
370375 },
371− },
376+ }),
372377 }
373378
374379 enabled := cfg.EnabledProviders()
@@ -377,7 +382,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
377382
378383 t.Run("some providers disabled", func(t *testing.T) {
379384 cfg := &Config{
380− Providers: map[string]ProviderConfig{
385+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
381386 "openai": {
382387 ID: "openai",
383388 APIKey: "key1",
@@ -388,7 +393,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
388393 APIKey: "key2",
389394 Disable: true,
390395 },
391− },
396+ }),
392397 }
393398
394399 enabled := cfg.EnabledProviders()
@@ -398,7 +403,7 @@ func TestConfig_EnabledProviders(t *testing.T) {
398403
399404 t.Run("empty providers map", func(t *testing.T) {
400405 cfg := &Config{
401− Providers: map[string]ProviderConfig{},
406+ Providers: csync.NewMap[string, ProviderConfig](),
402407 }
403408
404409 enabled := cfg.EnabledProviders()
@@ -409,13 +414,13 @@ func TestConfig_EnabledProviders(t *testing.T) {
409414func TestConfig_IsConfigured(t *testing.T) {
410415 t.Run("returns true when at least one provider is enabled", func(t *testing.T) {
411416 cfg := &Config{
412− Providers: map[string]ProviderConfig{
417+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
413418 "openai": {
414419 ID: "openai",
415420 APIKey: "key1",
416421 Disable: false,
417422 },
418− },
423+ }),
419424 }
420425
421426 assert.True(t, cfg.IsConfigured())
@@ -423,7 +428,7 @@ func TestConfig_IsConfigured(t *testing.T) {
423428
424429 t.Run("returns false when no providers are configured", func(t *testing.T) {
425430 cfg := &Config{
426− Providers: map[string]ProviderConfig{},
431+ Providers: csync.NewMap[string, ProviderConfig](),
427432 }
428433
429434 assert.False(t, cfg.IsConfigured())
@@ -431,7 +436,7 @@ func TestConfig_IsConfigured(t *testing.T) {
431436
432437 t.Run("returns false when all providers are disabled", func(t *testing.T) {
433438 cfg := &Config{
434− Providers: map[string]ProviderConfig{
439+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
435440 "openai": {
436441 ID: "openai",
437442 APIKey: "key1",
@@ -442,7 +447,7 @@ func TestConfig_IsConfigured(t *testing.T) {
442447 APIKey: "key2",
443448 Disable: true,
444449 },
445− },
450+ }),
446451 }
447452
448453 assert.False(t, cfg.IsConfigured())
@@ -462,11 +467,11 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
462467 }
463468
464469 cfg := &Config{
465− Providers: map[string]ProviderConfig{
470+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
466471 "openai": {
467472 Disable: true,
468473 },
469− },
474+ }),
470475 }
471476 cfg.setDefaults("/tmp")
472477
@@ -478,15 +483,15 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
478483 assert.NoError(t, err)
479484
480485 // Provider should be removed from config when disabled
481− assert.Len(t, cfg.Providers, 0)
482− _, exists := cfg.Providers["openai"]
486+ assert.Equal(t, cfg.Providers.Len(), 0)
487+ _, exists := cfg.Providers.Get("openai")
483488 assert.False(t, exists)
484489}
485490
486491func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
487492 t.Run("custom provider with missing API key is allowed, but not known providers", func(t *testing.T) {
488493 cfg := &Config{
489− Providers: map[string]ProviderConfig{
494+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
490495 "custom": {
491496 BaseURL: "https://api.custom.com/v1",
492497 Models: []provider.Model{{
@@ -496,7 +501,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
496501 "openai": {
497502 APIKey: "$MISSING",
498503 },
499− },
504+ }),
500505 }
501506 cfg.setDefaults("/tmp")
502507
@@ -505,21 +510,21 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
505510 err := cfg.configureProviders(env, resolver, []provider.Provider{})
506511 assert.NoError(t, err)
507512
508− assert.Len(t, cfg.Providers, 1)
509− _, exists := cfg.Providers["custom"]
513+ assert.Equal(t, cfg.Providers.Len(), 1)
514+ _, exists := cfg.Providers.Get("custom")
510515 assert.True(t, exists)
511516 })
512517
513518 t.Run("custom provider with missing BaseURL is removed", func(t *testing.T) {
514519 cfg := &Config{
515− Providers: map[string]ProviderConfig{
520+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
516521 "custom": {
517522 APIKey: "test-key",
518523 Models: []provider.Model{{
519524 ID: "test-model",
520525 }},
521526 },
522− },
527+ }),
523528 }
524529 cfg.setDefaults("/tmp")
525530
@@ -528,20 +533,20 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
528533 err := cfg.configureProviders(env, resolver, []provider.Provider{})
529534 assert.NoError(t, err)
530535
531− assert.Len(t, cfg.Providers, 0)
532− _, exists := cfg.Providers["custom"]
536+ assert.Equal(t, cfg.Providers.Len(), 0)
537+ _, exists := cfg.Providers.Get("custom")
533538 assert.False(t, exists)
534539 })
535540
536541 t.Run("custom provider with no models is removed", func(t *testing.T) {
537542 cfg := &Config{
538− Providers: map[string]ProviderConfig{
543+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
539544 "custom": {
540545 APIKey: "test-key",
541546 BaseURL: "https://api.custom.com/v1",
542547 Models: []provider.Model{},
543548 },
544− },
549+ }),
545550 }
546551 cfg.setDefaults("/tmp")
547552
@@ -550,14 +555,14 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
550555 err := cfg.configureProviders(env, resolver, []provider.Provider{})
551556 assert.NoError(t, err)
552557
553− assert.Len(t, cfg.Providers, 0)
554− _, exists := cfg.Providers["custom"]
558+ assert.Equal(t, cfg.Providers.Len(), 0)
559+ _, exists := cfg.Providers.Get("custom")
555560 assert.False(t, exists)
556561 })
557562
558563 t.Run("custom provider with unsupported type is removed", func(t *testing.T) {
559564 cfg := &Config{
560− Providers: map[string]ProviderConfig{
565+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
561566 "custom": {
562567 APIKey: "test-key",
563568 BaseURL: "https://api.custom.com/v1",
@@ -566,7 +571,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
566571 ID: "test-model",
567572 }},
568573 },
569− },
574+ }),
570575 }
571576 cfg.setDefaults("/tmp")
572577
@@ -575,14 +580,14 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
575580 err := cfg.configureProviders(env, resolver, []provider.Provider{})
576581 assert.NoError(t, err)
577582
578− assert.Len(t, cfg.Providers, 0)
579− _, exists := cfg.Providers["custom"]
583+ assert.Equal(t, cfg.Providers.Len(), 0)
584+ _, exists := cfg.Providers.Get("custom")
580585 assert.False(t, exists)
581586 })
582587
583588 t.Run("valid custom provider is kept and ID is set", func(t *testing.T) {
584589 cfg := &Config{
585− Providers: map[string]ProviderConfig{
590+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
586591 "custom": {
587592 APIKey: "test-key",
588593 BaseURL: "https://api.custom.com/v1",
@@ -591,7 +596,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
591596 ID: "test-model",
592597 }},
593598 },
594− },
599+ }),
595600 }
596601 cfg.setDefaults("/tmp")
597602
@@ -600,8 +605,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
600605 err := cfg.configureProviders(env, resolver, []provider.Provider{})
601606 assert.NoError(t, err)
602607
603− assert.Len(t, cfg.Providers, 1)
604− customProvider, exists := cfg.Providers["custom"]
608+ assert.Equal(t, cfg.Providers.Len(), 1)
609+ customProvider, exists := cfg.Providers.Get("custom")
605610 assert.True(t, exists)
606611 assert.Equal(t, "custom", customProvider.ID)
607612 assert.Equal(t, "test-key", customProvider.APIKey)
@@ -610,7 +615,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
610615
611616 t.Run("disabled custom provider is removed", func(t *testing.T) {
612617 cfg := &Config{
613− Providers: map[string]ProviderConfig{
618+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
614619 "custom": {
615620 APIKey: "test-key",
616621 BaseURL: "https://api.custom.com/v1",
@@ -620,7 +625,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
620625 ID: "test-model",
621626 }},
622627 },
623− },
628+ }),
624629 }
625630 cfg.setDefaults("/tmp")
626631
@@ -629,8 +634,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
629634 err := cfg.configureProviders(env, resolver, []provider.Provider{})
630635 assert.NoError(t, err)
631636
632− assert.Len(t, cfg.Providers, 0)
633− _, exists := cfg.Providers["custom"]
637+ assert.Equal(t, cfg.Providers.Len(), 0)
638+ _, exists := cfg.Providers.Get("custom")
634639 assert.False(t, exists)
635640 })
636641}
@@ -649,11 +654,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
649654 }
650655
651656 cfg := &Config{
652− Providers: map[string]ProviderConfig{
657+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
653658 "vertexai": {
654659 BaseURL: "custom-url",
655660 },
656− },
661+ }),
657662 }
658663 cfg.setDefaults("/tmp")
659664
@@ -664,8 +669,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
664669 err := cfg.configureProviders(env, resolver, knownProviders)
665670 assert.NoError(t, err)
666671
667− assert.Len(t, cfg.Providers, 0)
668− _, exists := cfg.Providers["vertexai"]
672+ assert.Equal(t, cfg.Providers.Len(), 0)
673+ _, exists := cfg.Providers.Get("vertexai")
669674 assert.False(t, exists)
670675 })
671676
@@ -682,11 +687,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
682687 }
683688
684689 cfg := &Config{
685− Providers: map[string]ProviderConfig{
690+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
686691 "bedrock": {
687692 BaseURL: "custom-url",
688693 },
689− },
694+ }),
690695 }
691696 cfg.setDefaults("/tmp")
692697
@@ -695,8 +700,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
695700 err := cfg.configureProviders(env, resolver, knownProviders)
696701 assert.NoError(t, err)
697702
698− assert.Len(t, cfg.Providers, 0)
699− _, exists := cfg.Providers["bedrock"]
703+ assert.Equal(t, cfg.Providers.Len(), 0)
704+ _, exists := cfg.Providers.Get("bedrock")
700705 assert.False(t, exists)
701706 })
702707
@@ -713,11 +718,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
713718 }
714719
715720 cfg := &Config{
716− Providers: map[string]ProviderConfig{
721+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
717722 "openai": {
718723 BaseURL: "custom-url",
719724 },
720− },
725+ }),
721726 }
722727 cfg.setDefaults("/tmp")
723728
@@ -726,8 +731,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
726731 err := cfg.configureProviders(env, resolver, knownProviders)
727732 assert.NoError(t, err)
728733
729− assert.Len(t, cfg.Providers, 0)
730− _, exists := cfg.Providers["openai"]
734+ assert.Equal(t, cfg.Providers.Len(), 0)
735+ _, exists := cfg.Providers.Get("openai")
731736 assert.False(t, exists)
732737 })
733738
@@ -744,11 +749,11 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
744749 }
745750
746751 cfg := &Config{
747− Providers: map[string]ProviderConfig{
752+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
748753 "openai": {
749754 APIKey: "test-key",
750755 },
751− },
756+ }),
752757 }
753758 cfg.setDefaults("/tmp")
754759
@@ -759,8 +764,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
759764 err := cfg.configureProviders(env, resolver, knownProviders)
760765 assert.NoError(t, err)
761766
762− assert.Len(t, cfg.Providers, 1)
763− _, exists := cfg.Providers["openai"]
767+ assert.Equal(t, cfg.Providers.Len(), 1)
768+ _, exists := cfg.Providers.Get("openai")
764769 assert.True(t, exists)
765770 })
766771}
@@ -883,7 +888,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
883888 }
884889
885890 cfg := &Config{
886− Providers: map[string]ProviderConfig{
891+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
887892 "custom": {
888893 APIKey: "test-key",
889894 BaseURL: "https://api.custom.com/v1",
@@ -894,7 +899,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
894899 },
895900 },
896901 },
897− },
902+ }),
898903 }
899904 cfg.setDefaults("/tmp")
900905 env := env.NewFromMap(map[string]string{})
@@ -932,13 +937,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
932937 }
933938
934939 cfg := &Config{
935− Providers: map[string]ProviderConfig{
940+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
936941 "custom": {
937942 APIKey: "test-key",
938943 BaseURL: "https://api.custom.com/v1",
939944 Models: []provider.Model{},
940945 },
941− },
946+ }),
942947 }
943948 cfg.setDefaults("/tmp")
944949 env := env.NewFromMap(map[string]string{})
@@ -969,7 +974,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
969974 }
970975
971976 cfg := &Config{
972− Providers: map[string]ProviderConfig{
977+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
973978 "custom": {
974979 APIKey: "test-key",
975980 BaseURL: "https://api.custom.com/v1",
@@ -980,7 +985,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
980985 },
981986 },
982987 },
983− },
988+ }),
984989 }
985990 cfg.setDefaults("/tmp")
986991 env := env.NewFromMap(map[string]string{})
internal/csync/maps.gonew file+84
@@ -0,0 +1,84 @@
1+package csync
2+
3+import (
4+ "encoding/json"
5+ "iter"
6+ "maps"
7+ "sync"
8+)
9+
10+type Map[K comparable, V any] struct {
11+ inner map[K]V
12+ mu sync.RWMutex
13+}
14+
15+func NewMap[K comparable, V any]() *Map[K, V] {
16+ return &Map[K, V]{
17+ inner: make(map[K]V),
18+ }
19+}
20+
21+func NewMapFrom[K comparable, V any](m map[K]V) *Map[K, V] {
22+ return &Map[K, V]{
23+ inner: m,
24+ }
25+}
26+
27+func (m *Map[K, V]) Set(key K, value V) {
28+ m.mu.Lock()
29+ defer m.mu.Unlock()
30+ m.inner[key] = value
31+}
32+
33+func (m *Map[K, V]) Del(key K) {
34+ m.mu.Lock()
35+ defer m.mu.Unlock()
36+ delete(m.inner, key)
37+}
38+
39+func (m *Map[K, V]) Get(key K) (V, bool) {
40+ m.mu.RLock()
41+ defer m.mu.RUnlock()
42+ v, ok := m.inner[key]
43+ return v, ok
44+}
45+
46+func (m *Map[K, V]) Len() int {
47+ m.mu.RLock()
48+ defer m.mu.RUnlock()
49+ return len(m.inner)
50+}
51+
52+func (m *Map[K, V]) Seq2() iter.Seq2[K, V] {
53+ dst := make(map[K]V)
54+ m.mu.RLock()
55+ maps.Copy(dst, m.inner)
56+ m.mu.RUnlock()
57+ return func(yield func(K, V) bool) {
58+ for k, v := range dst {
59+ if !yield(k, v) {
60+ return
61+ }
62+ }
63+ }
64+}
65+
66+var (
67+ _ json.Unmarshaler = &Map[string, any]{}
68+ _ json.Marshaler = &Map[string, any]{}
69+)
70+
71+// UnmarshalJSON implements json.Unmarshaler.
72+func (m *Map[K, V]) UnmarshalJSON(data []byte) error {
73+ m.mu.Lock()
74+ defer m.mu.Unlock()
75+ m.inner = make(map[K]V)
76+ return json.Unmarshal(data, &m.inner)
77+}
78+
79+// MarshalJSON implements json.Marshaler.
80+func (m *Map[K, V]) MarshalJSON() ([]byte, error) {
81+ m.mu.RLock()
82+ defer m.mu.RUnlock()
83+ return json.Marshal(m.inner)
84+}
internal/csync/maps_test.gonew file+450
@@ -0,0 +1,450 @@
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+ var result map[string]int
172+ err = json.Unmarshal(data, &result)
173+ assert.NoError(t, err)
174+ assert.Equal(t, 2, len(result))
175+ assert.Equal(t, 1, result["key1"])
176+ assert.Equal(t, 2, result["key2"])
177+}
178+
179+func TestMap_MarshalJSON_EmptyMap(t *testing.T) {
180+ t.Parallel()
181+
182+ m := NewMap[string, int]()
183+
184+ data, err := json.Marshal(m)
185+ assert.NoError(t, err)
186+ assert.Equal(t, "{}", string(data))
187+}
188+
189+func TestMap_UnmarshalJSON(t *testing.T) {
190+ t.Parallel()
191+
192+ jsonData := `{"key1": 1, "key2": 2}`
193+
194+ m := NewMap[string, int]()
195+ err := json.Unmarshal([]byte(jsonData), m)
196+ assert.NoError(t, err)
197+
198+ assert.Equal(t, 2, m.Len())
199+ value, ok := m.Get("key1")
200+ assert.True(t, ok)
201+ assert.Equal(t, 1, value)
202+
203+ value, ok = m.Get("key2")
204+ assert.True(t, ok)
205+ assert.Equal(t, 2, value)
206+}
207+
208+func TestMap_UnmarshalJSON_EmptyJSON(t *testing.T) {
209+ t.Parallel()
210+
211+ jsonData := `{}`
212+
213+ m := NewMap[string, int]()
214+ err := json.Unmarshal([]byte(jsonData), m)
215+ assert.NoError(t, err)
216+ assert.Equal(t, 0, m.Len())
217+}
218+
219+func TestMap_UnmarshalJSON_InvalidJSON(t *testing.T) {
220+ t.Parallel()
221+
222+ jsonData := `{"key1": 1, "key2":}`
223+
224+ m := NewMap[string, int]()
225+ err := json.Unmarshal([]byte(jsonData), m)
226+ assert.Error(t, err)
227+}
228+
229+func TestMap_UnmarshalJSON_OverwritesExistingData(t *testing.T) {
230+ t.Parallel()
231+
232+ m := NewMap[string, int]()
233+ m.Set("existing", 999)
234+
235+ jsonData := `{"key1": 1, "key2": 2}`
236+ err := json.Unmarshal([]byte(jsonData), m)
237+ assert.NoError(t, err)
238+
239+ assert.Equal(t, 2, m.Len())
240+ _, ok := m.Get("existing")
241+ assert.False(t, ok)
242+
243+ value, ok := m.Get("key1")
244+ assert.True(t, ok)
245+ assert.Equal(t, 1, value)
246+}
247+
248+func TestMap_JSONRoundTrip(t *testing.T) {
249+ t.Parallel()
250+
251+ original := NewMap[string, int]()
252+ original.Set("key1", 1)
253+ original.Set("key2", 2)
254+ original.Set("key3", 3)
255+
256+ data, err := json.Marshal(original)
257+ assert.NoError(t, err)
258+
259+ restored := NewMap[string, int]()
260+ err = json.Unmarshal(data, restored)
261+ assert.NoError(t, err)
262+
263+ assert.Equal(t, original.Len(), restored.Len())
264+
265+ for k, v := range original.Seq2() {
266+ restoredValue, ok := restored.Get(k)
267+ assert.True(t, ok)
268+ assert.Equal(t, v, restoredValue)
269+ }
270+}
271+
272+func TestMap_ConcurrentAccess(t *testing.T) {
273+ t.Parallel()
274+
275+ m := NewMap[int, int]()
276+ const numGoroutines = 100
277+ const numOperations = 100
278+
279+ var wg sync.WaitGroup
280+ wg.Add(numGoroutines)
281+
282+ for i := range numGoroutines {
283+ go func(id int) {
284+ defer wg.Done()
285+ for j := range numOperations {
286+ key := id*numOperations + j
287+ m.Set(key, key*2)
288+ value, ok := m.Get(key)
289+ assert.True(t, ok)
290+ assert.Equal(t, key*2, value)
291+ }
292+ }(i)
293+ }
294+
295+ wg.Wait()
296+
297+ assert.Equal(t, numGoroutines*numOperations, m.Len())
298+}
299+
300+func TestMap_ConcurrentReadWrite(t *testing.T) {
301+ t.Parallel()
302+
303+ m := NewMap[int, int]()
304+ const numReaders = 50
305+ const numWriters = 50
306+ const numOperations = 100
307+
308+ for i := range 1000 {
309+ m.Set(i, i)
310+ }
311+
312+ var wg sync.WaitGroup
313+ wg.Add(numReaders + numWriters)
314+
315+ for range numReaders {
316+ go func() {
317+ defer wg.Done()
318+ for j := range numOperations {
319+ key := j % 1000
320+ value, ok := m.Get(key)
321+ if ok {
322+ assert.Equal(t, key, value)
323+ }
324+ _ = m.Len()
325+ }
326+ }()
327+ }
328+
329+ for i := range numWriters {
330+ go func(id int) {
331+ defer wg.Done()
332+ for j := range numOperations {
333+ key := 1000 + id*numOperations + j
334+ m.Set(key, key)
335+ if j%10 == 0 {
336+ m.Del(key)
337+ }
338+ }
339+ }(i)
340+ }
341+
342+ wg.Wait()
343+}
344+
345+func TestMap_ConcurrentSeq2(t *testing.T) {
346+ t.Parallel()
347+
348+ m := NewMap[int, int]()
349+ for i := range 100 {
350+ m.Set(i, i*2)
351+ }
352+
353+ var wg sync.WaitGroup
354+ const numIterators = 10
355+
356+ wg.Add(numIterators)
357+ for range numIterators {
358+ go func() {
359+ defer wg.Done()
360+ count := 0
361+ for k, v := range m.Seq2() {
362+ assert.Equal(t, k*2, v)
363+ count++
364+ }
365+ assert.Equal(t, 100, count)
366+ }()
367+ }
368+
369+ wg.Wait()
370+}
371+
372+func TestMap_TypeSafety(t *testing.T) {
373+ t.Parallel()
374+
375+ stringIntMap := NewMap[string, int]()
376+ stringIntMap.Set("key", 42)
377+ value, ok := stringIntMap.Get("key")
378+ assert.True(t, ok)
379+ assert.Equal(t, 42, value)
380+
381+ intStringMap := NewMap[int, string]()
382+ intStringMap.Set(42, "value")
383+ strValue, ok := intStringMap.Get(42)
384+ assert.True(t, ok)
385+ assert.Equal(t, "value", strValue)
386+
387+ structMap := NewMap[string, struct{ Name string }]()
388+ structMap.Set("key", struct{ Name string }{Name: "test"})
389+ structValue, ok := structMap.Get("key")
390+ assert.True(t, ok)
391+ assert.Equal(t, "test", structValue.Name)
392+}
393+
394+func TestMap_InterfaceCompliance(t *testing.T) {
395+ t.Parallel()
396+
397+ var _ json.Marshaler = &Map[string, any]{}
398+ var _ json.Unmarshaler = &Map[string, any]{}
399+}
400+
401+func BenchmarkMap_Set(b *testing.B) {
402+ m := NewMap[int, int]()
403+
404+ for i := 0; b.Loop(); i++ {
405+ m.Set(i, i*2)
406+ }
407+}
408+
409+func BenchmarkMap_Get(b *testing.B) {
410+ m := NewMap[int, int]()
411+ for i := range 1000 {
412+ m.Set(i, i*2)
413+ }
414+
415+ for i := 0; b.Loop(); i++ {
416+ m.Get(i % 1000)
417+ }
418+}
419+
420+func BenchmarkMap_Seq2(b *testing.B) {
421+ m := NewMap[int, int]()
422+ for i := range 1000 {
423+ m.Set(i, i*2)
424+ }
425+
426+ for b.Loop() {
427+ for range m.Seq2() {
428+ }
429+ }
430+}
431+
432+func BenchmarkMap_ConcurrentReadWrite(b *testing.B) {
433+ m := NewMap[int, int]()
434+ for i := range 1000 {
435+ m.Set(i, i*2)
436+ }
437+
438+ b.ResetTimer()
439+ b.RunParallel(func(pb *testing.PB) {
440+ i := 0
441+ for pb.Next() {
442+ if i%2 == 0 {
443+ m.Get(i % 1000)
444+ } else {
445+ m.Set(i+1000, i*2)
446+ }
447+ i++
448+ }
449+ })
450+}
internal/llm/agent/agent.go+22−29
@@ -8,9 +8,9 @@ import (
88 "slices"
99 "strings"
1010 "sync"
11− "sync/atomic"
1211 "time"
1312
13+ "github.com/charmbracelet/crush/csync"
1414 "github.com/charmbracelet/crush/internal/config"
1515 fur "github.com/charmbracelet/crush/internal/fur/provider"
1616 "github.com/charmbracelet/crush/internal/history"
@@ -68,8 +68,7 @@ type agent struct {
6868 sessions session.Service
6969 messages message.Service
7070
71− toolsDone atomic.Bool
72− tools []tools.BaseTool
71+ tools *csync.LazySlice[tools.BaseTool]
7372
7473 provider provider.Provider
7574 providerID string
@@ -168,24 +167,10 @@ func NewAgent(
168167 return nil, err
169168 }
170169
171− agent := &agent{
172− Broker: pubsub.NewBroker[AgentEvent](),
173− agentCfg: agentCfg,
174− provider: agentProvider,
175− providerID: string(providerCfg.ID),
176− messages: messages,
177− sessions: sessions,
178− titleProvider: titleProvider,
179− summarizeProvider: summarizeProvider,
180− summarizeProviderID: string(smallModelProviderCfg.ID),
181− activeRequests: sync.Map{},
182− }
183−
184− go func() {
170+ toolFn := func() []tools.BaseTool {
185171 slog.Info("Initializing agent tools", "agent", agentCfg.ID)
186172 defer func() {
187173 slog.Info("Initialized agent tools", "agent", agentCfg.ID)
188− agent.toolsDone.Store(true)
189174 }()
190175
191176 cwd := cfg.WorkingDir()
@@ -214,8 +199,7 @@ func NewAgent(
214199 }
215200
216201 if agentCfg.AllowedTools == nil {
217− agent.tools = allTools
218− return
202+ return allTools
219203 }
220204
221205 var filteredTools []tools.BaseTool
@@ -224,10 +208,22 @@ func NewAgent(
224208 filteredTools = append(filteredTools, tool)
225209 }
226210 }
227− agent.tools = filteredTools
228− }()
211+ return filteredTools
212+ }
229213
230− return agent, nil
214+ return &agent{
215+ Broker: pubsub.NewBroker[AgentEvent](),
216+ agentCfg: agentCfg,
217+ provider: agentProvider,
218+ providerID: string(providerCfg.ID),
219+ messages: messages,
220+ sessions: sessions,
221+ titleProvider: titleProvider,
222+ summarizeProvider: summarizeProvider,
223+ summarizeProviderID: string(smallModelProviderCfg.ID),
224+ activeRequests: sync.Map{},
225+ tools: csync.NewLazySlice(toolFn),
226+ }, nil
231227}
232228
233229func (a *agent) Model() fur.Model {
@@ -449,10 +445,7 @@ func (a *agent) createUserMessage(ctx context.Context, sessionID, content string
449445
450446func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
451447 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
452− if !a.toolsDone.Load() {
453− return message.Message{}, nil, fmt.Errorf("agent is still initializing, please wait a moment and try again")
454− }
455− eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools)
448+ eventChan := a.provider.StreamResponse(ctx, msgHistory, slices.Collect(a.tools.Iter()))
456449
457450 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
458451 Role: message.Assistant,
@@ -501,7 +494,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
501494 default:
502495 // Continue processing
503496 var tool tools.BaseTool
504− for _, availableTool := range a.tools {
497+ for availableTool := range a.tools.Iter() {
505498 if availableTool.Info().Name == toolCall.Name {
506499 tool = availableTool
507500 break
@@ -911,7 +904,7 @@ func (a *agent) UpdateModel() error {
911904 smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
912905 var smallModelProviderCfg config.ProviderConfig
913906
914− for _, p := range cfg.Providers {
907+ for _, p := range cfg.Providers.Seq2() {
915908 if p.ID == smallModelCfg.Provider {
916909 smallModelProviderCfg = p
917910 break
internal/tui/components/chat/splash/splash.go+1−1
@@ -422,7 +422,7 @@ func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provide
422422
423423func (s *splashCmp) isProviderConfigured(providerID string) bool {
424424 cfg := config.Get()
425− if _, ok := cfg.Providers[providerID]; ok {
425+ if _, ok := cfg.Providers.Get(providerID); ok {
426426 return true
427427 }
428428 return false
internal/tui/components/dialogs/models/list.go+3−3
@@ -103,7 +103,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
103103 if err != nil {
104104 return util.ReportError(err)
105105 }
106− for providerID, providerConfig := range cfg.Providers {
106+ for providerID, providerConfig := range cfg.Providers.Seq2() {
107107 if providerConfig.Disable {
108108 continue
109109 }
@@ -164,7 +164,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
164164 }
165165
166166 // 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 {
168168 continue
169169 }
170170
@@ -174,7 +174,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
174174 }
175175
176176 section := commands.NewItemSection(name)
177− if _, ok := cfg.Providers[string(provider.ID)]; ok {
177+ if _, ok := cfg.Providers.Get(string(provider.ID)); ok {
178178 section.SetInfo(configured)
179179 }
180180 modelItems = append(modelItems, section)
internal/tui/components/dialogs/models/models.go+1−1
@@ -357,7 +357,7 @@ func (m *modelDialogCmp) modelTypeRadio() string {
357357
358358func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
359359 cfg := config.Get()
360− if _, ok := cfg.Providers[providerID]; ok {
360+ if _, ok := cfg.Providers.Get(providerID); ok {
361361 return true
362362 }
363363 return false