on main help sign in
feat: add `disable_default_providers` option (#1675)
163abdcJon Gjengset
internal/config/config.go+1
@@ -248,6 +248,7 @@ type Options struct {
248 DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
248 DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
249 DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of built-in tools to disable and hide from the agent,example=bash,example=sourcegraph"`
249 DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of built-in tools to disable and hide from the agent,example=bash,example=sourcegraph"`
250 DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
250 DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
251+ DisableDefaultProviders bool `json:"disable_default_providers,omitempty" jsonschema:"description=Ignore all default/embedded providers. When enabled, providers must be fully specified in the config file with base_url, models, and api_key - no merging with defaults occurs,default=false"`
251 Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"`
252 Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"`
252 DisableMetrics bool `json:"disable_metrics,omitempty" jsonschema:"description=Disable sending metrics,default=false"`
253 DisableMetrics bool `json:"disable_metrics,omitempty" jsonschema:"description=Disable sending metrics,default=false"`
253 InitializeAs string `json:"initialize_as,omitempty" jsonschema:"description=Name of the context file to create/update during project initialization,default=AGENTS.md,example=AGENTS.md,example=CRUSH.md,example=CLAUDE.md,example=docs/LLMs.md"`
254 InitializeAs string `json:"initialize_as,omitempty" jsonschema:"description=Name of the context file to create/update during project initialization,default=AGENTS.md,example=AGENTS.md,example=CRUSH.md,example=CLAUDE.md,example=docs/LLMs.md"`
internal/config/load.go+12
@@ -122,6 +122,14 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
122 restore := PushPopCrushEnv()
122 restore := PushPopCrushEnv()
123 defer restore()
123 defer restore()
124
124
125+ // When disable_default_providers is enabled, skip all default/embedded
126+ // providers entirely. Users must fully specify any providers they want.
127+ // We skip to the custom provider validation loop which handles all
128+ // user-configured providers uniformly.
129+ if c.Options.DisableDefaultProviders {
130+ knownProviders = nil
131+ }
132+
125 for _, p := range knownProviders {
133 for _, p := range knownProviders {
126 knownProviderNames[string(p.ID)] = true
134 knownProviderNames[string(p.ID)] = true
127 config, configExists := c.Providers.Get(string(p.ID))
135 config, configExists := c.Providers.Get(string(p.ID))
@@ -362,6 +370,10 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
362 c.Options.DisableProviderAutoUpdate, _ = strconv.ParseBool(str)
370 c.Options.DisableProviderAutoUpdate, _ = strconv.ParseBool(str)
363 }
371 }
364
372
373+ if str, ok := os.LookupEnv("CRUSH_DISABLE_DEFAULT_PROVIDERS"); ok {
374+ c.Options.DisableDefaultProviders, _ = strconv.ParseBool(str)
375+ }
376+
365 if c.Options.Attribution == nil {
377 if c.Options.Attribution == nil {
366 c.Options.Attribution = &Attribution{
378 c.Options.Attribution = &Attribution{
367 TrailerStyle: TrailerStyleAssistedBy,
379 TrailerStyle: TrailerStyleAssistedBy,
internal/config/load_test.go+211
@@ -1094,6 +1094,217 @@ func TestConfig_defaultModelSelection(t *testing.T) {
1094 })
1094 })
1095}
1095}
1096
1096
1097+func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
1098+ t.Run("when enabled, ignores all default providers and requires full specification", func(t *testing.T) {
1099+ knownProviders := []catwalk.Provider{
1100+ {
1101+ ID: "openai",
1102+ APIKey: "$OPENAI_API_KEY",
1103+ APIEndpoint: "https://api.openai.com/v1",
1104+ Models: []catwalk.Model{{
1105+ ID: "gpt-4",
1106+ }},
1107+ },
1108+ }
1109+
1110+ // User references openai but doesn't fully specify it (no base_url, no
1111+ // models). This should be rejected because disable_default_providers
1112+ // treats all providers as custom.
1113+ cfg := &Config{
1114+ Options: &Options{
1115+ DisableDefaultProviders: true,
1116+ },
1117+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1118+ "openai": {
1119+ APIKey: "$OPENAI_API_KEY",
1120+ },
1121+ }),
1122+ }
1123+ cfg.setDefaults("/tmp", "")
1124+
1125+ env := env.NewFromMap(map[string]string{
1126+ "OPENAI_API_KEY": "test-key",
1127+ })
1128+ resolver := NewEnvironmentVariableResolver(env)
1129+ err := cfg.configureProviders(env, resolver, knownProviders)
1130+ require.NoError(t, err)
1131+
1132+ // openai should NOT be present because it lacks base_url and models.
1133+ require.Equal(t, 0, cfg.Providers.Len())
1134+ _, exists := cfg.Providers.Get("openai")
1135+ require.False(t, exists, "openai should not be present without full specification")
1136+ })
1137+
1138+ t.Run("when enabled, fully specified providers work", func(t *testing.T) {
1139+ knownProviders := []catwalk.Provider{
1140+ {
1141+ ID: "openai",
1142+ APIKey: "$OPENAI_API_KEY",
1143+ APIEndpoint: "https://api.openai.com/v1",
1144+ Models: []catwalk.Model{{
1145+ ID: "gpt-4",
1146+ }},
1147+ },
1148+ }
1149+
1150+ // User fully specifies their provider.
1151+ cfg := &Config{
1152+ Options: &Options{
1153+ DisableDefaultProviders: true,
1154+ },
1155+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1156+ "my-llm": {
1157+ APIKey: "$MY_API_KEY",
1158+ BaseURL: "https://my-llm.example.com/v1",
1159+ Models: []catwalk.Model{{
1160+ ID: "my-model",
1161+ }},
1162+ },
1163+ }),
1164+ }
1165+ cfg.setDefaults("/tmp", "")
1166+
1167+ env := env.NewFromMap(map[string]string{
1168+ "MY_API_KEY": "test-key",
1169+ "OPENAI_API_KEY": "test-key",
1170+ })
1171+ resolver := NewEnvironmentVariableResolver(env)
1172+ err := cfg.configureProviders(env, resolver, knownProviders)
1173+ require.NoError(t, err)
1174+
1175+ // Only fully specified provider should be present.
1176+ require.Equal(t, 1, cfg.Providers.Len())
1177+ provider, exists := cfg.Providers.Get("my-llm")
1178+ require.True(t, exists, "my-llm should be present")
1179+ require.Equal(t, "https://my-llm.example.com/v1", provider.BaseURL)
1180+ require.Len(t, provider.Models, 1)
1181+
1182+ // Default openai should NOT be present.
1183+ _, exists = cfg.Providers.Get("openai")
1184+ require.False(t, exists, "openai should not be present")
1185+ })
1186+
1187+ t.Run("when disabled, includes all known providers with valid credentials", func(t *testing.T) {
1188+ knownProviders := []catwalk.Provider{
1189+ {
1190+ ID: "openai",
1191+ APIKey: "$OPENAI_API_KEY",
1192+ APIEndpoint: "https://api.openai.com/v1",
1193+ Models: []catwalk.Model{{
1194+ ID: "gpt-4",
1195+ }},
1196+ },
1197+ {
1198+ ID: "anthropic",
1199+ APIKey: "$ANTHROPIC_API_KEY",
1200+ APIEndpoint: "https://api.anthropic.com/v1",
1201+ Models: []catwalk.Model{{
1202+ ID: "claude-3",
1203+ }},
1204+ },
1205+ }
1206+
1207+ // User only configures openai, both API keys are available, but option
1208+ // is disabled.
1209+ cfg := &Config{
1210+ Options: &Options{
1211+ DisableDefaultProviders: false,
1212+ },
1213+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1214+ "openai": {
1215+ APIKey: "$OPENAI_API_KEY",
1216+ },
1217+ }),
1218+ }
1219+ cfg.setDefaults("/tmp", "")
1220+
1221+ env := env.NewFromMap(map[string]string{
1222+ "OPENAI_API_KEY": "test-key",
1223+ "ANTHROPIC_API_KEY": "test-key",
1224+ })
1225+ resolver := NewEnvironmentVariableResolver(env)
1226+ err := cfg.configureProviders(env, resolver, knownProviders)
1227+ require.NoError(t, err)
1228+
1229+ // Both providers should be present.
1230+ require.Equal(t, 2, cfg.Providers.Len())
1231+ _, exists := cfg.Providers.Get("openai")
1232+ require.True(t, exists, "openai should be present")
1233+ _, exists = cfg.Providers.Get("anthropic")
1234+ require.True(t, exists, "anthropic should be present")
1235+ })
1236+
1237+ t.Run("when enabled, provider missing models is rejected", func(t *testing.T) {
1238+ cfg := &Config{
1239+ Options: &Options{
1240+ DisableDefaultProviders: true,
1241+ },
1242+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1243+ "my-llm": {
1244+ APIKey: "test-key",
1245+ BaseURL: "https://my-llm.example.com/v1",
1246+ Models: []catwalk.Model{}, // No models.
1247+ },
1248+ }),
1249+ }
1250+ cfg.setDefaults("/tmp", "")
1251+
1252+ env := env.NewFromMap(map[string]string{})
1253+ resolver := NewEnvironmentVariableResolver(env)
1254+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
1255+ require.NoError(t, err)
1256+
1257+ // Provider should be rejected for missing models.
1258+ require.Equal(t, 0, cfg.Providers.Len())
1259+ })
1260+
1261+ t.Run("when enabled, provider missing base_url is rejected", func(t *testing.T) {
1262+ cfg := &Config{
1263+ Options: &Options{
1264+ DisableDefaultProviders: true,
1265+ },
1266+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1267+ "my-llm": {
1268+ APIKey: "test-key",
1269+ Models: []catwalk.Model{{ID: "model"}},
1270+ // No BaseURL.
1271+ },
1272+ }),
1273+ }
1274+ cfg.setDefaults("/tmp", "")
1275+
1276+ env := env.NewFromMap(map[string]string{})
1277+ resolver := NewEnvironmentVariableResolver(env)
1278+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
1279+ require.NoError(t, err)
1280+
1281+ // Provider should be rejected for missing base_url.
1282+ require.Equal(t, 0, cfg.Providers.Len())
1283+ })
1284+}
1285+
1286+func TestConfig_setDefaultsDisableDefaultProvidersEnvVar(t *testing.T) {
1287+ t.Run("sets option from environment variable", func(t *testing.T) {
1288+ t.Setenv("CRUSH_DISABLE_DEFAULT_PROVIDERS", "true")
1289+
1290+ cfg := &Config{}
1291+ cfg.setDefaults("/tmp", "")
1292+
1293+ require.True(t, cfg.Options.DisableDefaultProviders)
1294+ })
1295+
1296+ t.Run("does not override when env var is not set", func(t *testing.T) {
1297+ cfg := &Config{
1298+ Options: &Options{
1299+ DisableDefaultProviders: true,
1300+ },
1301+ }
1302+ cfg.setDefaults("/tmp", "")
1303+
1304+ require.True(t, cfg.Options.DisableDefaultProviders)
1305+ })
1306+}
1307+
1097func TestConfig_configureSelectedModels(t *testing.T) {
1308func TestConfig_configureSelectedModels(t *testing.T) {
1098 t.Run("should override defaults", func(t *testing.T) {
1309 t.Run("should override defaults", func(t *testing.T) {
1099 knownProviders := []catwalk.Provider{
1310 knownProviders := []catwalk.Provider{