fix #15: use all knownProviders in fallback chain for environment variable auth

When resolving credentials, if the default provider (anthropic) has no
credentials configured, Resolve tries to fall back to other providers that
DO have credentials available. This is crucial for environment variable
authentication to work.

The bug: the fallback list was hardcoded to only check a subset of providers:
  - anthropic, openai, openai-codex, kimi, deepseek, google

This meant that providers like amazon-bedrock, github-copilot, google-vertex,
and others were never checked for environment variable credentials.

When ZOT_HOME was set to a new location (with no config.json), the default
provider would be anthropic, and without anthropic credentials set, the
fallback would skip bedrock (and other missing providers), causing the
'you are not logged in' dialog to appear.

The fix: replace the hardcoded list with knownProviders, which contains all
40+ supported providers. Now all providers are checked for available
credentials, regardless of ZOT_HOME setting.

Added two tests:
- TestResolveEnvironmentVariableAuthWithoutConfiguredProvider: verifies
  bedrock credentials via environment variables are found
- TestResolveFallsBackThroughAllKnownProviders: verifies the fallback
  mechanism checks all known providers, not just a hardcoded subset
This commit is contained in:
Raymond Gasper 2026-06-08 09:09:18 -04:00
parent a7ef8c22a1
commit faae96ccc2
2 changed files with 60 additions and 2 deletions

View file

@ -317,8 +317,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
// never shows a "not logged in" banner.
userPickedProvider := args.Provider != ""
if credErr != nil && !userPickedProvider && provName != "ollama" {
for _, other := range []string{"anthropic", "openai", "openai-codex", "kimi", "deepseek", "google"} {
if other == provName {
for _, other := range knownProviders {
if other == provName || other == "ollama" {
continue
}
if c, m, a, err := ResolveCredentialFull(other, args.APIKey); err == nil {

View file

@ -180,3 +180,61 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) {
}
}
}
// TestResolveEnvironmentVariableAuthWithoutConfiguredProvider reproduces
// the bug where setting ZOT_HOME breaks environment variable authentication.
// When no provider is configured and the default provider (anthropic) has no
// credentials, Resolve should fall back to ANY provider that has credentials
// via environment variables, including amazon-bedrock and other providers.
func TestResolveEnvironmentVariableAuthWithoutConfiguredProvider(t *testing.T) {
t.Setenv("ZOT_HOME", t.TempDir())
// No ANTHROPIC credentials set
t.Setenv("ANTHROPIC_API_KEY", "")
t.Setenv("ANTHROPIC_OAUTH_TOKEN", "")
t.Setenv("OPENAI_API_KEY", "")
// Test case 1: amazon-bedrock via environment variables
t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "test-token-123")
t.Setenv("AWS_REGION", "us-east-1")
// No config.json created yet (fresh ZOT_HOME)
// Resolve should NOT error and should detect bedrock credentials
r, err := Resolve(Args{}, false)
if err != nil {
t.Fatalf("Resolve failed with bedrock env vars set: %v", err)
}
if r.Provider != "amazon-bedrock" {
t.Errorf("Resolve with bedrock env var: got provider %q, want amazon-bedrock", r.Provider)
}
if !r.HasCredential() {
t.Errorf("Resolve should have found bedrock credential from environment variable")
}
}
// TestResolveFallsBackThroughAllKnownProviders ensures that when the
// default provider has no credentials, Resolve tries all known providers
// (not just a hardcoded subset) to find one with available credentials.
// We test with github-copilot which was NOT in the old hardcoded list.
func TestResolveFallsBackThroughAllKnownProviders(t *testing.T) {
t.Setenv("ZOT_HOME", t.TempDir())
// Clear anthropic credentials
t.Setenv("ANTHROPIC_API_KEY", "")
t.Setenv("ANTHROPIC_OAUTH_TOKEN", "")
// Clear openai credentials
t.Setenv("OPENAI_API_KEY", "")
// Clear amazon-bedrock (since it comes first in knownProviders)
t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_PROFILE", "")
// Set github-copilot, which was NOT in the old hardcoded list
t.Setenv("COPILOT_GITHUB_TOKEN", "copilot-token-789")
r, err := Resolve(Args{}, false)
if err != nil {
t.Fatalf("Resolve failed: %v", err)
}
if r.Provider != "github-copilot" {
t.Errorf("Resolve should have found github-copilot in fallback chain, got %q", r.Provider)
}
}