From 778b881c35e62ef43618daa996ba3864e2ded1ce Mon Sep 17 00:00:00 2001 From: kypkk Date: Fri, 12 Jun 2026 13:41:04 +0800 Subject: [PATCH 1/2] test(config): reproduce vertexai provider dropped despite configured api_key Reproduction for charmbracelet/crush#3074: a vertexai provider configured with an api_key (and a custom base_url) is dropped by configureProviders because the vertexai branch only checks VERTEXAI_PROJECT/VERTEXAI_LOCATION env vars and ignores the api_key. The test documents the current buggy behavior (provider count == 0); flip the assertion to 1 once fixed. --- internal/config/load_test.go | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 9d67acaeec..1b8c8920b9 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -552,6 +552,46 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) { require.Equal(t, cfg.Providers.Len(), 0) } +// TestConfig_configureProvidersVertexAIWithAPIKey reproduces issue #3074: +// a Vertex AI provider configured with an api_key (and a custom base_url +// pointing at a proxy) is dropped during config load because the vertexai +// branch only looks at the VERTEXAI_PROJECT / VERTEXAI_LOCATION env vars and +// ignores the configured api_key entirely. Once the provider is dropped, +// Crush thinks it is unconfigured and prompts the user for an API key. +// +// The same api_key style works for openai/anthropic because they go through +// the generic branch that resolves p.APIKey. +// +// This test documents the CURRENT (buggy) behavior: the provider is dropped +// even though a valid api_key is present. After the fix, the provider should +// be kept (flip the assertion to Len() == 1). +func TestConfig_configureProvidersVertexAIWithAPIKey(t *testing.T) { + knownProviders := []catwalk.Provider{ + { + ID: catwalk.InferenceProviderVertexAI, + APIKey: "test-api-key", // user configured an api_key + APIEndpoint: "https://vertex-proxy.example.com", // custom proxy base_url + Models: []catwalk.Model{{ + ID: "gemini-pro", + }}, + }, + } + + cfg := &Config{} + cfg.setDefaults("/tmp", "") + // No VERTEXAI_PROJECT / VERTEXAI_LOCATION on purpose: the user is + // authenticating via api_key + proxy, not native GCP credentials. + env := env.NewFromMap(map[string]string{}) + resolver := NewShellVariableResolver(env) + err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders) + require.NoError(t, err) + + // BUG #3074: the provider is dropped despite a configured api_key. + // This assertion PASSES today, which is exactly the bug. + require.Equal(t, 0, cfg.Providers.Len(), + "BUG #3074") +} + func TestConfig_configureProvidersSetProviderID(t *testing.T) { knownProviders := []catwalk.Provider{ { From 4e9eacbb7e12c454492ed82607be336e0ee2da1b Mon Sep 17 00:00:00 2001 From: kypkk Date: Fri, 12 Jun 2026 13:48:13 +0800 Subject: [PATCH 2/2] fix(config): honor configured api_key for the Vertex AI provider The vertexai branch in configureProviders only checked the VERTEXAI_PROJECT/VERTEXAI_LOCATION env vars and dropped the provider when they were unset, ignoring a configured api_key entirely. Crush then treated the provider as unconfigured and prompted for an API key, even though the same api_key config works for openai/anthropic. Now, when the GCP env vars are absent, the api_key is resolved like any other provider and keeps the provider alive. buildGoogleVertexProvider wires the api_key (via WithGeminiAPIKey) and the configured base_url through to the google provider; the native WithVertex path is unchanged and still takes precedence when project/location are set. Fixes charmbracelet/crush#3074 --- internal/agent/coordinator.go | 15 ++++++++++++--- internal/config/load.go | 10 +++++++--- internal/config/load_test.go | 33 +++++++++++++++++---------------- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 86ca09e3bf..f45fd209b5 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -948,7 +948,7 @@ func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[st return google.New(opts...) } -func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) { +func (c *coordinator) buildGoogleVertexProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) { opts := []google.Option{} if c.cfg.Config().Options.Debug { httpClient := log.NewHTTPClient() @@ -961,7 +961,16 @@ func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, optio project := options["project"] location := options["location"] - opts = append(opts, google.WithVertex(project, location)) + if project != "" && location != "" { + opts = append(opts, google.WithVertex(project, location)) + } else { + // Without native GCP credentials, authenticate with the configured + // API key (e.g. against a Gemini-API-compatible proxy). + opts = append(opts, google.WithGeminiAPIKey(apiKey)) + } + if baseURL != "" { + opts = append(opts, google.WithBaseURL(baseURL)) + } return google.New(opts...) } @@ -1016,7 +1025,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con case google.Name: return c.buildGoogleProvider(baseURL, apiKey, headers) case "google-vertex": - return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams) + return c.buildGoogleVertexProvider(baseURL, apiKey, headers, providerCfg.ExtraParams) case openaicompat.Name, hyper.Name: switch providerCfg.ID { case hyper.Name: diff --git a/internal/config/load.go b/internal/config/load.go index 2f0946e7bc..bf4cc68fa6 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -284,15 +284,19 @@ func (c *Config) configureProviders(store *ConfigStore, env env.Env, resolver Va project = env.Get("VERTEXAI_PROJECT") location = env.Get("VERTEXAI_LOCATION") ) - if project == "" || location == "" { + if project != "" && location != "" { + prepared.ExtraParams["project"] = project + prepared.ExtraParams["location"] = location + } else if v, err := resolver.ResolveValue(p.APIKey); v == "" || err != nil { + // Without native GCP credentials an API key works like any + // other provider (e.g. against a Gemini-API-compatible + // proxy); only drop the provider when neither is available. if configExists { slog.Warn("Skipping Vertex AI provider due to missing credentials") c.Providers.Del(string(p.ID)) } continue } - prepared.ExtraParams["project"] = project - prepared.ExtraParams["location"] = location case catwalk.InferenceProviderAzure: endpoint, err := resolver.ResolveValue(p.APIEndpoint) if err != nil || endpoint == "" { diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 1b8c8920b9..e6d2678c67 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -552,19 +552,16 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) { require.Equal(t, cfg.Providers.Len(), 0) } -// TestConfig_configureProvidersVertexAIWithAPIKey reproduces issue #3074: -// a Vertex AI provider configured with an api_key (and a custom base_url -// pointing at a proxy) is dropped during config load because the vertexai -// branch only looks at the VERTEXAI_PROJECT / VERTEXAI_LOCATION env vars and -// ignores the configured api_key entirely. Once the provider is dropped, -// Crush thinks it is unconfigured and prompts the user for an API key. +// TestConfig_configureProvidersVertexAIWithAPIKey is a regression test for +// issue #3074: a Vertex AI provider configured with an api_key (and a custom +// base_url pointing at a proxy) used to be dropped during config load because +// the vertexai branch only looked at the VERTEXAI_PROJECT / VERTEXAI_LOCATION +// env vars and ignored the configured api_key entirely. Once dropped, Crush +// thought the provider was unconfigured and prompted the user for an API key, +// even though the same api_key style works for openai/anthropic. // -// The same api_key style works for openai/anthropic because they go through -// the generic branch that resolves p.APIKey. -// -// This test documents the CURRENT (buggy) behavior: the provider is dropped -// even though a valid api_key is present. After the fix, the provider should -// be kept (flip the assertion to Len() == 1). +// With the fix, an api_key keeps the provider alive when the GCP env vars are +// absent, matching the generic-provider behavior. func TestConfig_configureProvidersVertexAIWithAPIKey(t *testing.T) { knownProviders := []catwalk.Provider{ { @@ -585,11 +582,15 @@ func TestConfig_configureProvidersVertexAIWithAPIKey(t *testing.T) { resolver := NewShellVariableResolver(env) err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders) require.NoError(t, err) + require.Equal(t, 1, cfg.Providers.Len(), + "vertexai provider with an api_key must be kept (issue #3074)") - // BUG #3074: the provider is dropped despite a configured api_key. - // This assertion PASSES today, which is exactly the bug. - require.Equal(t, 0, cfg.Providers.Len(), - "BUG #3074") + vertexProvider, ok := cfg.Providers.Get("vertexai") + require.True(t, ok, "VertexAI provider should be present") + require.Equal(t, "test-api-key", vertexProvider.APIKey) + require.Equal(t, "https://vertex-proxy.example.com", vertexProvider.BaseURL) + require.Empty(t, vertexProvider.ExtraParams["project"], "no GCP project expected when authenticating via api_key") + require.Empty(t, vertexProvider.ExtraParams["location"]) } func TestConfig_configureProvidersSetProviderID(t *testing.T) {