on main help sign in
chore: add more tests and handle more cases
e157024Kujtim Hoxha
cspell.json+1−1
@@ -1 +1 @@
1−{"language":"en","version":"0.2","flagWords":[],"words":["afero","alecthomas","bubbletea","charmbracelet","charmtone","Charple","crush","diffview","Emph","filepicker","Focusable","fsext","GROQ","Guac","imageorient","Lanczos","lipgloss","lsps","lucasb","nfnt","oksvg","Preproc","rasterx","rivo","Sourcegraph","srwiley","Strikethrough","termenv","textinput","trashhalo","uniseg","Unticked","genai","jsonschema","preconfigured","jsons","qjebbs","LOCALAPPDATA","USERPROFILE","stretchr"]}
1+{"words":["afero","alecthomas","bubbletea","charmbracelet","charmtone","Charple","crush","diffview","Emph","filepicker","Focusable","fsext","GROQ","Guac","imageorient","Lanczos","lipgloss","lsps","lucasb","nfnt","oksvg","Preproc","rasterx","rivo","Sourcegraph","srwiley","Strikethrough","termenv","textinput","trashhalo","uniseg","Unticked","genai","jsonschema","preconfigured","jsons","qjebbs","LOCALAPPDATA","USERPROFILE","stretchr","cursorrules","VERTEXAI","VERTEXAI"],"flagWords":[],"language":"en","version":"0.2"}
pkg/config/config.go+25−1
@@ -29,6 +29,13 @@ var defaultContextPaths = []string{
29 "CRUSH.local.md",
29 "CRUSH.local.md",
30}
30}
31
31
32+type SelectedModelType string
33+
34+const (
35+ SelectedModelTypeLarge SelectedModelType = "large"
36+ SelectedModelTypeSmall SelectedModelType = "small"
37+)
38+
32type SelectedModel struct {
39type SelectedModel struct {
33 // The model id as used by the provider API.
40 // The model id as used by the provider API.
34 // Required.
41 // Required.
@@ -48,6 +55,8 @@ type SelectedModel struct {
48}
55}
49
56
50type ProviderConfig struct {
57type ProviderConfig struct {
58+ // The provider's id.
59+ ID string `json:"id,omitempty"`
51 // The provider's API endpoint.
60 // The provider's API endpoint.
52 BaseURL string `json:"base_url,omitempty"`
61 BaseURL string `json:"base_url,omitempty"`
53 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
62 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
@@ -153,7 +162,7 @@ func (l LSPs) Sorted() []LSP {
153// Config holds the configuration for crush.
162// Config holds the configuration for crush.
154type Config struct {
163type Config struct {
155 // We currently only support large/small as values here.
164 // We currently only support large/small as values here.
156− Models map[string]SelectedModel `json:"models,omitempty"`
165+ Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
157
166
158 // The providers that are configured
167 // The providers that are configured
159 Providers map[string]ProviderConfig `json:"providers,omitempty"`
168 Providers map[string]ProviderConfig `json:"providers,omitempty"`
@@ -171,3 +180,18 @@ type Config struct {
171func (c *Config) WorkingDir() string {
180func (c *Config) WorkingDir() string {
172 return c.workingDir
181 return c.workingDir
173}
182}
183+
184+func (c *Config) EnabledProviders() []ProviderConfig {
185+ enabled := make([]ProviderConfig, 0, len(c.Providers))
186+ for _, p := range c.Providers {
187+ if !p.Disable {
188+ enabled = append(enabled, p)
189+ }
190+ }
191+ return enabled
192+}
193+
194+// IsConfigured return true if at least one provider is configured
195+func (c *Config) IsConfigured() bool {
196+ return len(c.EnabledProviders()) > 0
197+}
pkg/config/load.go+94−12
@@ -14,6 +14,7 @@ import (
14 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/pkg/env"
15 "github.com/charmbracelet/crush/pkg/env"
16 "github.com/charmbracelet/crush/pkg/log"
16 "github.com/charmbracelet/crush/pkg/log"
17+ "golang.org/x/exp/slog"
17)
18)
18
19
19// LoadReader config via io.Reader.
20// LoadReader config via io.Reader.
@@ -55,15 +56,12 @@ func Load(workingDir string, debug bool) (*Config, error) {
55 if err != nil {
56 if err != nil {
56 return nil, fmt.Errorf("failed to load config: %w", err)
57 return nil, fmt.Errorf("failed to load config: %w", err)
57 }
58 }
58− // TODO: maybe add a validation step here right after loading
59− // e.x validate the models
60− // e.x validate provider config
61
59
62 cfg.setDefaults(workingDir)
60 cfg.setDefaults(workingDir)
63
61
64 // Load known providers, this loads the config from fur
62 // Load known providers, this loads the config from fur
65 providers, err := LoadProviders(client.New())
63 providers, err := LoadProviders(client.New())
66− if err != nil {
64+ if err != nil || len(providers) == 0 {
67 return nil, fmt.Errorf("failed to load providers: %w", err)
65 return nil, fmt.Errorf("failed to load providers: %w", err)
68 }
66 }
69
67
@@ -74,15 +72,35 @@ func Load(workingDir string, debug bool) (*Config, error) {
74 return nil, fmt.Errorf("failed to configure providers: %w", err)
72 return nil, fmt.Errorf("failed to configure providers: %w", err)
75 }
73 }
76
74
75+ if !cfg.IsConfigured() {
76+ slog.Warn("No providers configured")
77+ return cfg, nil
78+ }
79+
80+ // largeModel, ok := cfg.Models[SelectedModelTypeLarge]
81+ // if !ok {
82+ // // set default
83+ // }
84+ // smallModel, ok := cfg.Models[SelectedModelTypeSmall]
85+ // if !ok {
86+ // // set default
87+ // }
88+ //
77 return cfg, nil
89 return cfg, nil
78}
90}
79
91
80func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []provider.Provider) error {
92func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []provider.Provider) error {
93+ knownProviderNames := make(map[string]bool)
81 for _, p := range knownProviders {
94 for _, p := range knownProviders {
82−
95+ knownProviderNames[string(p.ID)] = true
83− config, ok := cfg.Providers[string(p.ID)]
96+ config, configExists := cfg.Providers[string(p.ID)]
84 // if the user configured a known provider we need to allow it to override a couple of parameters
97 // if the user configured a known provider we need to allow it to override a couple of parameters
85− if ok {
98+ if configExists {
99+ if config.Disable {
100+ slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
101+ delete(cfg.Providers, string(p.ID))
102+ continue
103+ }
86 if config.BaseURL != "" {
104 if config.BaseURL != "" {
87 p.APIEndpoint = config.BaseURL
105 p.APIEndpoint = config.BaseURL
88 }
106 }
@@ -112,6 +130,7 @@ func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, kn
112 }
130 }
113 }
131 }
114 prepared := ProviderConfig{
132 prepared := ProviderConfig{
133+ ID: string(p.ID),
115 BaseURL: p.APIEndpoint,
134 BaseURL: p.APIEndpoint,
116 APIKey: p.APIKey,
135 APIKey: p.APIKey,
117 Type: p.Type,
136 Type: p.Type,
@@ -125,12 +144,20 @@ func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, kn
125 // Handle specific providers that require additional configuration
144 // Handle specific providers that require additional configuration
126 case provider.InferenceProviderVertexAI:
145 case provider.InferenceProviderVertexAI:
127 if !hasVertexCredentials(env) {
146 if !hasVertexCredentials(env) {
147+ if configExists {
148+ slog.Warn("Skipping Vertex AI provider due to missing credentials")
149+ delete(cfg.Providers, string(p.ID))
150+ }
128 continue
151 continue
129 }
152 }
130 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
153 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
131 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
154 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
132 case provider.InferenceProviderBedrock:
155 case provider.InferenceProviderBedrock:
133 if !hasAWSCredentials(env) {
156 if !hasAWSCredentials(env) {
157+ if configExists {
158+ slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
159+ delete(cfg.Providers, string(p.ID))
160+ }
134 continue
161 continue
135 }
162 }
136 for _, model := range p.Models {
163 for _, model := range p.Models {
@@ -142,15 +169,70 @@ func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, kn
142 // if the provider api or endpoint are missing we skip them
169 // if the provider api or endpoint are missing we skip them
143 v, err := resolver.ResolveValue(p.APIKey)
170 v, err := resolver.ResolveValue(p.APIKey)
144 if v == "" || err != nil {
171 if v == "" || err != nil {
145− continue
172+ if configExists {
146− }
173+ slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
147− v, err = resolver.ResolveValue(p.APIEndpoint)
174+ delete(cfg.Providers, string(p.ID))
148− if v == "" || err != nil {
175+ }
149 continue
176 continue
150 }
177 }
151 }
178 }
152 cfg.Providers[string(p.ID)] = prepared
179 cfg.Providers[string(p.ID)] = prepared
153 }
180 }
181+
182+ // validate the custom providers
183+ for id, providerConfig := range cfg.Providers {
184+ if knownProviderNames[id] {
185+ continue
186+ }
187+
188+ // Make sure the provider ID is set
189+ providerConfig.ID = id
190+ // default to OpenAI if not set
191+ if providerConfig.Type == "" {
192+ providerConfig.Type = provider.TypeOpenAI
193+ }
194+
195+ if providerConfig.Disable {
196+ slog.Debug("Skipping custom provider due to disable flag", "provider", id)
197+ delete(cfg.Providers, id)
198+ continue
199+ }
200+ if providerConfig.APIKey == "" {
201+ slog.Warn("Skipping custom provider due to missing API key", "provider", id)
202+ delete(cfg.Providers, id)
203+ continue
204+ }
205+ if providerConfig.BaseURL == "" {
206+ slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
207+ delete(cfg.Providers, id)
208+ continue
209+ }
210+ if len(providerConfig.Models) == 0 {
211+ slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
212+ delete(cfg.Providers, id)
213+ continue
214+ }
215+ if providerConfig.Type != provider.TypeOpenAI {
216+ slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
217+ delete(cfg.Providers, id)
218+ continue
219+ }
220+
221+ apiKey, err := resolver.ResolveValue(providerConfig.APIKey)
222+ if apiKey == "" || err != nil {
223+ slog.Warn("Skipping custom provider due to missing API key", "provider", id, "error", err)
224+ delete(cfg.Providers, id)
225+ continue
226+ }
227+ baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
228+ if baseURL == "" || err != nil {
229+ slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
230+ delete(cfg.Providers, id)
231+ continue
232+ }
233+
234+ cfg.Providers[id] = providerConfig
235+ }
154 return nil
236 return nil
155}
237}
156
238
@@ -200,7 +282,7 @@ func (cfg *Config) setDefaults(workingDir string) {
200 cfg.Providers = make(map[string]ProviderConfig)
282 cfg.Providers = make(map[string]ProviderConfig)
201 }
283 }
202 if cfg.Models == nil {
284 if cfg.Models == nil {
203− cfg.Models = make(map[string]SelectedModel)
285+ cfg.Models = make(map[SelectedModelType]SelectedModel)
204 }
286 }
205 if cfg.MCP == nil {
287 if cfg.MCP == nil {
206 cfg.MCP = make(map[string]MCPConfig)
288 cfg.MCP = make(map[string]MCPConfig)
pkg/config/load_test.go+445
@@ -2,6 +2,8 @@ package config
2
2
3import (
3import (
4 "io"
4 "io"
5+ "log/slog"
6+ "os"
5 "strings"
7 "strings"
6 "testing"
8 "testing"
7
9
@@ -10,6 +12,13 @@ import (
10 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/assert"
11)
13)
12
14
15+func TestMain(m *testing.M) {
16+ slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
17+
18+ exitVal := m.Run()
19+ os.Exit(exitVal)
20+}
21+
13func TestConfig_LoadFromReaders(t *testing.T) {
22func TestConfig_LoadFromReaders(t *testing.T) {
14 data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
23 data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
15 data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
24 data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
@@ -152,6 +161,8 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
152
161
153 // We want to make sure that we keep the configured API key as a placeholder
162 // We want to make sure that we keep the configured API key as a placeholder
154 assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
163 assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
164+ // Make sure we set the ID correctly
165+ assert.Equal(t, "custom", cfg.Providers["custom"].ID)
155 assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
166 assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
156 assert.Len(t, cfg.Providers["custom"].Models, 1)
167 assert.Len(t, cfg.Providers["custom"].Models, 1)
157
168
@@ -315,3 +326,437 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
315 // Provider should not be configured without project
326 // Provider should not be configured without project
316 assert.Len(t, cfg.Providers, 0)
327 assert.Len(t, cfg.Providers, 0)
317}
328}
329+
330+func TestConfig_configureProvidersSetProviderID(t *testing.T) {
331+ knownProviders := []provider.Provider{
332+ {
333+ ID: "openai",
334+ APIKey: "$OPENAI_API_KEY",
335+ APIEndpoint: "https://api.openai.com/v1",
336+ Models: []provider.Model{{
337+ ID: "test-model",
338+ }},
339+ },
340+ }
341+
342+ cfg := &Config{}
343+ cfg.setDefaults("/tmp")
344+ env := env.NewFromMap(map[string]string{
345+ "OPENAI_API_KEY": "test-key",
346+ })
347+ resolver := NewEnvironmentVariableResolver(env)
348+ err := cfg.configureProviders(env, resolver, knownProviders)
349+ assert.NoError(t, err)
350+ assert.Len(t, cfg.Providers, 1)
351+
352+ // Provider ID should be set
353+ assert.Equal(t, "openai", cfg.Providers["openai"].ID)
354+}
355+
356+func TestConfig_EnabledProviders(t *testing.T) {
357+ t.Run("all providers enabled", func(t *testing.T) {
358+ cfg := &Config{
359+ Providers: map[string]ProviderConfig{
360+ "openai": {
361+ ID: "openai",
362+ APIKey: "key1",
363+ Disable: false,
364+ },
365+ "anthropic": {
366+ ID: "anthropic",
367+ APIKey: "key2",
368+ Disable: false,
369+ },
370+ },
371+ }
372+
373+ enabled := cfg.EnabledProviders()
374+ assert.Len(t, enabled, 2)
375+ })
376+
377+ t.Run("some providers disabled", func(t *testing.T) {
378+ cfg := &Config{
379+ Providers: map[string]ProviderConfig{
380+ "openai": {
381+ ID: "openai",
382+ APIKey: "key1",
383+ Disable: false,
384+ },
385+ "anthropic": {
386+ ID: "anthropic",
387+ APIKey: "key2",
388+ Disable: true,
389+ },
390+ },
391+ }
392+
393+ enabled := cfg.EnabledProviders()
394+ assert.Len(t, enabled, 1)
395+ assert.Equal(t, "openai", enabled[0].ID)
396+ })
397+
398+ t.Run("empty providers map", func(t *testing.T) {
399+ cfg := &Config{
400+ Providers: map[string]ProviderConfig{},
401+ }
402+
403+ enabled := cfg.EnabledProviders()
404+ assert.Len(t, enabled, 0)
405+ })
406+}
407+
408+func TestConfig_IsConfigured(t *testing.T) {
409+ t.Run("returns true when at least one provider is enabled", func(t *testing.T) {
410+ cfg := &Config{
411+ Providers: map[string]ProviderConfig{
412+ "openai": {
413+ ID: "openai",
414+ APIKey: "key1",
415+ Disable: false,
416+ },
417+ },
418+ }
419+
420+ assert.True(t, cfg.IsConfigured())
421+ })
422+
423+ t.Run("returns false when no providers are configured", func(t *testing.T) {
424+ cfg := &Config{
425+ Providers: map[string]ProviderConfig{},
426+ }
427+
428+ assert.False(t, cfg.IsConfigured())
429+ })
430+
431+ t.Run("returns false when all providers are disabled", func(t *testing.T) {
432+ cfg := &Config{
433+ Providers: map[string]ProviderConfig{
434+ "openai": {
435+ ID: "openai",
436+ APIKey: "key1",
437+ Disable: true,
438+ },
439+ "anthropic": {
440+ ID: "anthropic",
441+ APIKey: "key2",
442+ Disable: true,
443+ },
444+ },
445+ }
446+
447+ assert.False(t, cfg.IsConfigured())
448+ })
449+}
450+
451+func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
452+ knownProviders := []provider.Provider{
453+ {
454+ ID: "openai",
455+ APIKey: "$OPENAI_API_KEY",
456+ APIEndpoint: "https://api.openai.com/v1",
457+ Models: []provider.Model{{
458+ ID: "test-model",
459+ }},
460+ },
461+ }
462+
463+ cfg := &Config{
464+ Providers: map[string]ProviderConfig{
465+ "openai": {
466+ Disable: true,
467+ },
468+ },
469+ }
470+ cfg.setDefaults("/tmp")
471+
472+ env := env.NewFromMap(map[string]string{
473+ "OPENAI_API_KEY": "test-key",
474+ })
475+ resolver := NewEnvironmentVariableResolver(env)
476+ err := cfg.configureProviders(env, resolver, knownProviders)
477+ assert.NoError(t, err)
478+
479+ // Provider should be removed from config when disabled
480+ assert.Len(t, cfg.Providers, 0)
481+ _, exists := cfg.Providers["openai"]
482+ assert.False(t, exists)
483+}
484+
485+func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
486+ t.Run("custom provider with missing API key is removed", func(t *testing.T) {
487+ cfg := &Config{
488+ Providers: map[string]ProviderConfig{
489+ "custom": {
490+ BaseURL: "https://api.custom.com/v1",
491+ Models: []provider.Model{{
492+ ID: "test-model",
493+ }},
494+ },
495+ },
496+ }
497+ cfg.setDefaults("/tmp")
498+
499+ env := env.NewFromMap(map[string]string{})
500+ resolver := NewEnvironmentVariableResolver(env)
501+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
502+ assert.NoError(t, err)
503+
504+ assert.Len(t, cfg.Providers, 0)
505+ _, exists := cfg.Providers["custom"]
506+ assert.False(t, exists)
507+ })
508+
509+ t.Run("custom provider with missing BaseURL is removed", func(t *testing.T) {
510+ cfg := &Config{
511+ Providers: map[string]ProviderConfig{
512+ "custom": {
513+ APIKey: "test-key",
514+ Models: []provider.Model{{
515+ ID: "test-model",
516+ }},
517+ },
518+ },
519+ }
520+ cfg.setDefaults("/tmp")
521+
522+ env := env.NewFromMap(map[string]string{})
523+ resolver := NewEnvironmentVariableResolver(env)
524+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
525+ assert.NoError(t, err)
526+
527+ assert.Len(t, cfg.Providers, 0)
528+ _, exists := cfg.Providers["custom"]
529+ assert.False(t, exists)
530+ })
531+
532+ t.Run("custom provider with no models is removed", func(t *testing.T) {
533+ cfg := &Config{
534+ Providers: map[string]ProviderConfig{
535+ "custom": {
536+ APIKey: "test-key",
537+ BaseURL: "https://api.custom.com/v1",
538+ Models: []provider.Model{},
539+ },
540+ },
541+ }
542+ cfg.setDefaults("/tmp")
543+
544+ env := env.NewFromMap(map[string]string{})
545+ resolver := NewEnvironmentVariableResolver(env)
546+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
547+ assert.NoError(t, err)
548+
549+ assert.Len(t, cfg.Providers, 0)
550+ _, exists := cfg.Providers["custom"]
551+ assert.False(t, exists)
552+ })
553+
554+ t.Run("custom provider with unsupported type is removed", func(t *testing.T) {
555+ cfg := &Config{
556+ Providers: map[string]ProviderConfig{
557+ "custom": {
558+ APIKey: "test-key",
559+ BaseURL: "https://api.custom.com/v1",
560+ Type: "unsupported",
561+ Models: []provider.Model{{
562+ ID: "test-model",
563+ }},
564+ },
565+ },
566+ }
567+ cfg.setDefaults("/tmp")
568+
569+ env := env.NewFromMap(map[string]string{})
570+ resolver := NewEnvironmentVariableResolver(env)
571+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
572+ assert.NoError(t, err)
573+
574+ assert.Len(t, cfg.Providers, 0)
575+ _, exists := cfg.Providers["custom"]
576+ assert.False(t, exists)
577+ })
578+
579+ t.Run("valid custom provider is kept and ID is set", func(t *testing.T) {
580+ cfg := &Config{
581+ Providers: map[string]ProviderConfig{
582+ "custom": {
583+ APIKey: "test-key",
584+ BaseURL: "https://api.custom.com/v1",
585+ Type: provider.TypeOpenAI,
586+ Models: []provider.Model{{
587+ ID: "test-model",
588+ }},
589+ },
590+ },
591+ }
592+ cfg.setDefaults("/tmp")
593+
594+ env := env.NewFromMap(map[string]string{})
595+ resolver := NewEnvironmentVariableResolver(env)
596+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
597+ assert.NoError(t, err)
598+
599+ assert.Len(t, cfg.Providers, 1)
600+ customProvider, exists := cfg.Providers["custom"]
601+ assert.True(t, exists)
602+ assert.Equal(t, "custom", customProvider.ID)
603+ assert.Equal(t, "test-key", customProvider.APIKey)
604+ assert.Equal(t, "https://api.custom.com/v1", customProvider.BaseURL)
605+ })
606+
607+ t.Run("disabled custom provider is removed", func(t *testing.T) {
608+ cfg := &Config{
609+ Providers: map[string]ProviderConfig{
610+ "custom": {
611+ APIKey: "test-key",
612+ BaseURL: "https://api.custom.com/v1",
613+ Type: provider.TypeOpenAI,
614+ Disable: true,
615+ Models: []provider.Model{{
616+ ID: "test-model",
617+ }},
618+ },
619+ },
620+ }
621+ cfg.setDefaults("/tmp")
622+
623+ env := env.NewFromMap(map[string]string{})
624+ resolver := NewEnvironmentVariableResolver(env)
625+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
626+ assert.NoError(t, err)
627+
628+ assert.Len(t, cfg.Providers, 0)
629+ _, exists := cfg.Providers["custom"]
630+ assert.False(t, exists)
631+ })
632+}
633+
634+func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
635+ t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
636+ knownProviders := []provider.Provider{
637+ {
638+ ID: provider.InferenceProviderVertexAI,
639+ APIKey: "",
640+ APIEndpoint: "",
641+ Models: []provider.Model{{
642+ ID: "gemini-pro",
643+ }},
644+ },
645+ }
646+
647+ cfg := &Config{
648+ Providers: map[string]ProviderConfig{
649+ "vertexai": {
650+ BaseURL: "custom-url",
651+ },
652+ },
653+ }
654+ cfg.setDefaults("/tmp")
655+
656+ env := env.NewFromMap(map[string]string{
657+ "GOOGLE_GENAI_USE_VERTEXAI": "false",
658+ })
659+ resolver := NewEnvironmentVariableResolver(env)
660+ err := cfg.configureProviders(env, resolver, knownProviders)
661+ assert.NoError(t, err)
662+
663+ assert.Len(t, cfg.Providers, 0)
664+ _, exists := cfg.Providers["vertexai"]
665+ assert.False(t, exists)
666+ })
667+
668+ t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
669+ knownProviders := []provider.Provider{
670+ {
671+ ID: provider.InferenceProviderBedrock,
672+ APIKey: "",
673+ APIEndpoint: "",
674+ Models: []provider.Model{{
675+ ID: "anthropic.claude-sonnet-4-20250514-v1:0",
676+ }},
677+ },
678+ }
679+
680+ cfg := &Config{
681+ Providers: map[string]ProviderConfig{
682+ "bedrock": {
683+ BaseURL: "custom-url",
684+ },
685+ },
686+ }
687+ cfg.setDefaults("/tmp")
688+
689+ env := env.NewFromMap(map[string]string{})
690+ resolver := NewEnvironmentVariableResolver(env)
691+ err := cfg.configureProviders(env, resolver, knownProviders)
692+ assert.NoError(t, err)
693+
694+ assert.Len(t, cfg.Providers, 0)
695+ _, exists := cfg.Providers["bedrock"]
696+ assert.False(t, exists)
697+ })
698+
699+ t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
700+ knownProviders := []provider.Provider{
701+ {
702+ ID: "openai",
703+ APIKey: "$MISSING_API_KEY",
704+ APIEndpoint: "https://api.openai.com/v1",
705+ Models: []provider.Model{{
706+ ID: "test-model",
707+ }},
708+ },
709+ }
710+
711+ cfg := &Config{
712+ Providers: map[string]ProviderConfig{
713+ "openai": {
714+ BaseURL: "custom-url",
715+ },
716+ },
717+ }
718+ cfg.setDefaults("/tmp")
719+
720+ env := env.NewFromMap(map[string]string{})
721+ resolver := NewEnvironmentVariableResolver(env)
722+ err := cfg.configureProviders(env, resolver, knownProviders)
723+ assert.NoError(t, err)
724+
725+ assert.Len(t, cfg.Providers, 0)
726+ _, exists := cfg.Providers["openai"]
727+ assert.False(t, exists)
728+ })
729+
730+ t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
731+ knownProviders := []provider.Provider{
732+ {
733+ ID: "openai",
734+ APIKey: "$OPENAI_API_KEY",
735+ APIEndpoint: "$MISSING_ENDPOINT",
736+ Models: []provider.Model{{
737+ ID: "test-model",
738+ }},
739+ },
740+ }
741+
742+ cfg := &Config{
743+ Providers: map[string]ProviderConfig{
744+ "openai": {
745+ APIKey: "test-key",
746+ },
747+ },
748+ }
749+ cfg.setDefaults("/tmp")
750+
751+ env := env.NewFromMap(map[string]string{
752+ "OPENAI_API_KEY": "test-key",
753+ })
754+ resolver := NewEnvironmentVariableResolver(env)
755+ err := cfg.configureProviders(env, resolver, knownProviders)
756+ assert.NoError(t, err)
757+
758+ assert.Len(t, cfg.Providers, 1)
759+ _, exists := cfg.Providers["openai"]
760+ assert.True(t, exists)
761+ })
762+}