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 9d67acaeec..e6d2678c67 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -552,6 +552,47 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) { require.Equal(t, cfg.Providers.Len(), 0) } +// 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. +// +// 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{ + { + 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) + require.Equal(t, 1, cfg.Providers.Len(), + "vertexai provider with an api_key must be kept (issue #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) { knownProviders := []catwalk.Provider{ {