on main help sign in
feat: improve providers startup (#287)
df56ffdCarlos Alexandro Becker
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 (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/crush/internal/csync"
12 "github.com/charmbracelet/crush/internal/env"
13 "github.com/charmbracelet/crush/internal/env"
13 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/tidwall/sjson"
15 "github.com/tidwall/sjson"
@@ -236,7 +237,7 @@ type Config struct {
236 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
237 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
237
238
238 // The providers that are configured
239 // The providers that are configured
239− Providers map[string]ProviderConfig `json:"providers,omitempty"`
240+ Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty"`
240
241
241 MCP MCPs `json:"mcp,omitempty"`
242 MCP MCPs `json:"mcp,omitempty"`
242
243
@@ -259,8 +260,8 @@ func (c *Config) WorkingDir() string {
259}
260}
260
261
261func (c *Config) EnabledProviders() []ProviderConfig {
262func (c *Config) EnabledProviders() []ProviderConfig {
262− enabled := make([]ProviderConfig, 0, len(c.Providers))
263+ var enabled []ProviderConfig
263− for _, p := range c.Providers {
264+ for _, p := range c.Providers.Seq2() {
264 if !p.Disable {
265 if !p.Disable {
265 enabled = append(enabled, p)
266 enabled = append(enabled, p)
266 }
267 }
@@ -274,7 +275,7 @@ func (c *Config) IsConfigured() bool {
274}
275}
275
276
276func (c *Config) GetModel(provider, model string) *provider.Model {
277func (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 {
278 for _, m := range providerConfig.Models {
279 for _, m := range providerConfig.Models {
279 if m.ID == model {
280 if m.ID == model {
280 return &m
281 return &m
@@ -289,7 +290,7 @@ func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfi
289 if !ok {
290 if !ok {
290 return nil
291 return nil
291 }
292 }
292− if providerConfig, ok := c.Providers[model.Provider]; ok {
293+ if providerConfig, ok := c.Providers.Get(model.Provider); ok {
293 return &providerConfig
294 return &providerConfig
294 }
295 }
295 return nil
296 return nil
@@ -370,14 +371,10 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
370 return fmt.Errorf("failed to save API key to config file: %w", err)
371 return fmt.Errorf("failed to save API key to config file: %w", err)
371 }
372 }
372
373
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)
378 if exists {
375 if exists {
379 providerConfig.APIKey = apiKey
376 providerConfig.APIKey = apiKey
380− c.Providers[providerID] = providerConfig
377+ c.Providers.Set(providerID, providerConfig)
381 return nil
378 return nil
382 }
379 }
383
380
@@ -406,7 +403,7 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
406 return fmt.Errorf("provider with ID %s not found in known providers", providerID)
403 return fmt.Errorf("provider with ID %s not found in known providers", providerID)
407 }
404 }
408 // Store the updated provider config
405 // Store the updated provider config
409− c.Providers[providerID] = providerConfig
406+ c.Providers.Set(providerID, providerConfig)
410 return nil
407 return nil
411}
408}
412
409
internal/config/load.go+45−40
@@ -11,6 +11,7 @@ import (
11 "strings"
11 "strings"
12 "sync"
12 "sync"
13
13
14+ "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/env"
15 "github.com/charmbracelet/crush/internal/env"
15 "github.com/charmbracelet/crush/internal/fur/client"
16 "github.com/charmbracelet/crush/internal/fur/client"
16 "github.com/charmbracelet/crush/internal/fur/provider"
17 "github.com/charmbracelet/crush/internal/fur/provider"
@@ -80,30 +81,34 @@ func Load(workingDir string, debug bool) (*Config, error) {
80 var testResults sync.Map
81 var testResults sync.Map
81 var wg sync.WaitGroup
82 var wg sync.WaitGroup
82
83
84+ go func() {
85+ slog.Info("Testing provider connections")
86+ defer slog.Info("Provider connection tests completed")
83− for _, p := range cfg.Providers {
87+ for _, p := range cfg.Providers.Seq2() {
84− if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
88+ if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
85− wg.Add(1)
89+ wg.Add(1)
86− go func(provider ProviderConfig) {
90+ go func(provider ProviderConfig) {
87− defer wg.Done()
91+ defer wg.Done()
88− err := provider.TestConnection(cfg.resolver)
92+ err := provider.TestConnection(cfg.resolver)
89− testResults.Store(provider.ID, err == nil)
93+ testResults.Store(provider.ID, err == nil)
90− if err != nil {
94+ if err != nil {
91− slog.Error("Provider connection test failed", "provider", provider.ID, "error", err)
95+ slog.Error("Provider connection test failed", "provider", provider.ID, "error", err)
92− }
96+ }
93− }(p)
97+ }(p)
94− }
95− }
98+ }
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)
104 }
99 }
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+ }
105− return true
109+ return true
110+ })
106− })
111+ }()
107
112
108 if !cfg.IsConfigured() {
113 if !cfg.IsConfigured() {
109 slog.Warn("No providers configured")
114 slog.Warn("No providers configured")
@@ -121,12 +126,12 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
121 knownProviderNames := make(map[string]bool)
126 knownProviderNames := make(map[string]bool)
122 for _, p := range knownProviders {
127 for _, p := range knownProviders {
123 knownProviderNames[string(p.ID)] = true
128 knownProviderNames[string(p.ID)] = true
124− config, configExists := c.Providers[string(p.ID)]
129+ config, configExists := c.Providers.Get(string(p.ID))
125 // if the user configured a known provider we need to allow it to override a couple of parameters
130 // if the user configured a known provider we need to allow it to override a couple of parameters
126 if configExists {
131 if configExists {
127 if config.Disable {
132 if config.Disable {
128 slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
133 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))
130 continue
135 continue
131 }
136 }
132 if config.BaseURL != "" {
137 if config.BaseURL != "" {
@@ -182,7 +187,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
182 if !hasVertexCredentials(env) {
187 if !hasVertexCredentials(env) {
183 if configExists {
188 if configExists {
184 slog.Warn("Skipping Vertex AI provider due to missing credentials")
189 slog.Warn("Skipping Vertex AI provider due to missing credentials")
185− delete(c.Providers, string(p.ID))
190+ c.Providers.Del(string(p.ID))
186 }
191 }
187 continue
192 continue
188 }
193 }
@@ -193,7 +198,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
193 if err != nil || endpoint == "" {
198 if err != nil || endpoint == "" {
194 if configExists {
199 if configExists {
195 slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
200 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))
197 }
202 }
198 continue
203 continue
199 }
204 }
@@ -203,7 +208,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
203 if !hasAWSCredentials(env) {
208 if !hasAWSCredentials(env) {
204 if configExists {
209 if configExists {
205 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
210 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
206− delete(c.Providers, string(p.ID))
211+ c.Providers.Del(string(p.ID))
207 }
212 }
208 continue
213 continue
209 }
214 }
@@ -218,16 +223,16 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
218 if v == "" || err != nil {
223 if v == "" || err != nil {
219 if configExists {
224 if configExists {
220 slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
225 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))
222 }
227 }
223 continue
228 continue
224 }
229 }
225 }
230 }
226− c.Providers[string(p.ID)] = prepared
231+ c.Providers.Set(string(p.ID), prepared)
227 }
232 }
228
233
229 // validate the custom providers
234 // validate the custom providers
230− for id, providerConfig := range c.Providers {
235+ for id, providerConfig := range c.Providers.Seq2() {
231 if knownProviderNames[id] {
236 if knownProviderNames[id] {
232 continue
237 continue
233 }
238 }
@@ -244,7 +249,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
244
249
245 if providerConfig.Disable {
250 if providerConfig.Disable {
246 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
251 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
247− delete(c.Providers, id)
252+ c.Providers.Del(id)
248 continue
253 continue
249 }
254 }
250 if providerConfig.APIKey == "" {
255 if providerConfig.APIKey == "" {
@@ -252,17 +257,17 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
252 }
257 }
253 if providerConfig.BaseURL == "" {
258 if providerConfig.BaseURL == "" {
254 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
259 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
255− delete(c.Providers, id)
260+ c.Providers.Del(id)
256 continue
261 continue
257 }
262 }
258 if len(providerConfig.Models) == 0 {
263 if len(providerConfig.Models) == 0 {
259 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
264 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
260− delete(c.Providers, id)
265+ c.Providers.Del(id)
261 continue
266 continue
262 }
267 }
263 if providerConfig.Type != provider.TypeOpenAI {
268 if providerConfig.Type != provider.TypeOpenAI {
264 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
269 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)
266 continue
271 continue
267 }
272 }
268
273
@@ -273,11 +278,11 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
273 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
278 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
274 if baseURL == "" || err != nil {
279 if baseURL == "" || err != nil {
275 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
280 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
276− delete(c.Providers, id)
281+ c.Providers.Del(id)
277 continue
282 continue
278 }
283 }
279
284
280− c.Providers[id] = providerConfig
285+ c.Providers.Set(id, providerConfig)
281 }
286 }
282 return nil
287 return nil
283}
288}
@@ -297,7 +302,7 @@ func (c *Config) setDefaults(workingDir string) {
297 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
302 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
298 }
303 }
299 if c.Providers == nil {
304 if c.Providers == nil {
300− c.Providers = make(map[string]ProviderConfig)
305+ c.Providers = csync.NewMap[string, ProviderConfig]()
301 }
306 }
302 if c.Models == nil {
307 if c.Models == nil {
303 c.Models = make(map[SelectedModelType]SelectedModel)
308 c.Models = make(map[SelectedModelType]SelectedModel)
@@ -316,7 +321,7 @@ func (c *Config) setDefaults(workingDir string) {
316}
321}
317
322
318func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
323func (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 {
320 err = fmt.Errorf("no providers configured, please configure at least one provider")
325 err = fmt.Errorf("no providers configured, please configure at least one provider")
321 return
326 return
322 }
327 }
@@ -324,7 +329,7 @@ func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (larg
324 // Use the first provider enabled based on the known providers order
329 // Use the first provider enabled based on the known providers order
325 // if no provider found that is known use the first provider configured
330 // if no provider found that is known use the first provider configured
326 for _, p := range knownProviders {
331 for _, p := range knownProviders {
327− providerConfig, ok := c.Providers[string(p.ID)]
332+ providerConfig, ok := c.Providers.Get(string(p.ID))
328 if !ok || providerConfig.Disable {
333 if !ok || providerConfig.Disable {
329 continue
334 continue
330 }
335 }
internal/config/load_test.go+106−101
@@ -8,6 +8,7 @@ import (
8 "strings"
8 "strings"
9 "testing"
9 "testing"
10
10
11+ "github.com/charmbracelet/crush/internal/csync"
11 "github.com/charmbracelet/crush/internal/env"
12 "github.com/charmbracelet/crush/internal/env"
12 "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/assert"
@@ -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: []provider.Model{
103+ Models: []provider.Model{
100− {
104+ {
101− ID: "test-model",
105+ ID: "test-model",
102− Model: "Updated",
106+ Model: "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].Model)
128+ assert.Equal(t, "Updated", pc.Models[0].Model)
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: []provider.Model{{
497 Models: []provider.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, []provider.Provider{})
510 err := cfg.configureProviders(env, resolver, []provider.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: []provider.Model{{
523 Models: []provider.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, []provider.Provider{})
533 err := cfg.configureProviders(env, resolver, []provider.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: []provider.Model{},
547 Models: []provider.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, []provider.Provider{})
555 err := cfg.configureProviders(env, resolver, []provider.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, []provider.Provider{})
580 err := cfg.configureProviders(env, resolver, []provider.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, []provider.Provider{})
605 err := cfg.configureProviders(env, resolver, []provider.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, []provider.Provider{})
634 err := cfg.configureProviders(env, resolver, []provider.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: []provider.Model{},
944 Models: []provider.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/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 (
8 "slices"
8 "slices"
9 "strings"
9 "strings"
10 "sync"
10 "sync"
11− "sync/atomic"
12 "time"
11 "time"
13
12
13+ "github.com/charmbracelet/crush/csync"
14 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
15 fur "github.com/charmbracelet/crush/internal/fur/provider"
15 fur "github.com/charmbracelet/crush/internal/fur/provider"
16 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
@@ -68,8 +68,7 @@ type agent struct {
68 sessions session.Service
68 sessions session.Service
69 messages message.Service
69 messages message.Service
70
70
71− toolsDone atomic.Bool
71+ tools *csync.LazySlice[tools.BaseTool]
72− tools []tools.BaseTool
73
72
74 provider provider.Provider
73 provider provider.Provider
75 providerID string
74 providerID string
@@ -168,24 +167,10 @@ func NewAgent(
168 return nil, err
167 return nil, err
169 }
168 }
170
169
171− agent := &agent{
170+ toolFn := func() []tools.BaseTool {
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() {
185 slog.Info("Initializing agent tools", "agent", agentCfg.ID)
171 slog.Info("Initializing agent tools", "agent", agentCfg.ID)
186 defer func() {
172 defer func() {
187 slog.Info("Initialized agent tools", "agent", agentCfg.ID)
173 slog.Info("Initialized agent tools", "agent", agentCfg.ID)
188− agent.toolsDone.Store(true)
189 }()
174 }()
190
175
191 cwd := cfg.WorkingDir()
176 cwd := cfg.WorkingDir()
@@ -214,8 +199,7 @@ func NewAgent(
214 }
199 }
215
200
216 if agentCfg.AllowedTools == nil {
201 if agentCfg.AllowedTools == nil {
217− agent.tools = allTools
218− return
202+ return allTools
219 }
203 }
220
204
221 var filteredTools []tools.BaseTool
205 var filteredTools []tools.BaseTool
@@ -224,10 +208,22 @@ func NewAgent(
224 filteredTools = append(filteredTools, tool)
208 filteredTools = append(filteredTools, tool)
225 }
209 }
226 }
210 }
227− agent.tools = filteredTools
211+ return filteredTools
228− }()
212+ }
229
213
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
231}
227}
232
228
233func (a *agent) Model() fur.Model {
229func (a *agent) Model() fur.Model {
@@ -449,10 +445,7 @@ func (a *agent) createUserMessage(ctx context.Context, sessionID, content string
449
445
450func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
446func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
451 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
447 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()))
456
449
457 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
450 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
458 Role: message.Assistant,
451 Role: message.Assistant,
@@ -501,7 +494,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
501 default:
494 default:
502 // Continue processing
495 // Continue processing
503 var tool tools.BaseTool
496 var tool tools.BaseTool
504− for _, availableTool := range a.tools {
497+ for availableTool := range a.tools.Iter() {
505 if availableTool.Info().Name == toolCall.Name {
498 if availableTool.Info().Name == toolCall.Name {
506 tool = availableTool
499 tool = availableTool
507 break
500 break
@@ -911,7 +904,7 @@ func (a *agent) UpdateModel() error {
911 smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
904 smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
912 var smallModelProviderCfg config.ProviderConfig
905 var smallModelProviderCfg config.ProviderConfig
913
906
914− for _, p := range cfg.Providers {
907+ for _, p := range cfg.Providers.Seq2() {
915 if p.ID == smallModelCfg.Provider {
908 if p.ID == smallModelCfg.Provider {
916 smallModelProviderCfg = p
909 smallModelProviderCfg = p
917 break
910 break
internal/tui/components/chat/splash/splash.go+1−1
@@ -422,7 +422,7 @@ func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provide
422
422
423func (s *splashCmp) isProviderConfigured(providerID string) bool {
423func (s *splashCmp) isProviderConfigured(providerID string) bool {
424 cfg := config.Get()
424 cfg := config.Get()
425− if _, ok := cfg.Providers[providerID]; ok {
425+ if _, ok := cfg.Providers.Get(providerID); ok {
426 return true
426 return true
427 }
427 }
428 return false
428 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