diff --git a/packages/agent/build.go b/packages/agent/build.go index 3e8dc76..343c7ee 100644 --- a/packages/agent/build.go +++ b/packages/agent/build.go @@ -213,6 +213,51 @@ func isKnownProvider(name string) bool { return false } +// providerAliases maps common short / alternate provider names to the +// canonical id in knownProviders. Users (and other agents) reach for +// "bedrock" or "vertex" far more naturally than the fully-qualified +// "amazon-bedrock" / "google-vertex"; without this mapping an alias is +// treated as an unknown provider and Resolve silently falls back to +// anthropic, producing a misleading "no credential for anthropic" error +// after the user explicitly picked, say, bedrock. +var providerAliases = map[string]string{ + "bedrock": "amazon-bedrock", + "aws-bedrock": "amazon-bedrock", + "amazon": "amazon-bedrock", + "vertex": "google-vertex", + "gcp-vertex": "google-vertex", + "gemini": "google", + "googleai": "google", + "google-ai": "google", + "azure": "azure-openai-responses", + "azure-openai": "azure-openai-responses", + "copilot": "github-copilot", + "github": "github-copilot", + "codex": "openai-codex", + "moonshot": "moonshotai", + "kimi-code": "kimi", + "ai-gateway": "vercel-ai-gateway", + "vercel": "vercel-ai-gateway", + "cloudflare": "cloudflare-workers-ai", + "workers-ai": "cloudflare-workers-ai", + "hf": "huggingface", +} + +// canonicalProvider normalises a user-supplied provider name: trims +// surrounding whitespace, lower-cases it, and resolves any known alias +// to its canonical id. Unknown names are returned trimmed/lower-cased +// and unchanged so the existing unknown-provider handling still runs. +func canonicalProvider(name string) string { + n := strings.ToLower(strings.TrimSpace(name)) + if n == "" { + return n + } + if canon, ok := providerAliases[n]; ok { + return canon + } + return n +} + // Resolve merges args, config, and env into a Resolved set. // // Unlike the earlier version, Resolve NEVER returns an error for @@ -223,7 +268,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { cfg, _ := LoadConfig() // User-requested provider (explicit > config > default). - provName := firstNonEmpty(args.Provider, cfg.Provider, "anthropic") + // Normalise common aliases (e.g. "bedrock" -> "amazon-bedrock") + // before validation so an alias is never mistaken for an unknown + // provider and silently downgraded to anthropic. + argProvider := canonicalProvider(args.Provider) + provName := firstNonEmpty(argProvider, canonicalProvider(cfg.Provider), "anthropic") if !isKnownProvider(provName) { // Unknown provider (maybe removed or renamed). Fall back to // the first provider that has credentials, or anthropic. diff --git a/packages/agent/build_test.go b/packages/agent/build_test.go index 92e1711..3e3bd44 100644 --- a/packages/agent/build_test.go +++ b/packages/agent/build_test.go @@ -108,3 +108,37 @@ func TestResolveExplicitFlagStaleDoesNotRepairConfig(t *testing.T) { t.Errorf("config.json was clobbered (was %q; now %q)", good, cfg.Model) } } + +func TestCanonicalProviderResolvesAliases(t *testing.T) { + cases := map[string]string{ + "bedrock": "amazon-bedrock", + "AWS-Bedrock": "amazon-bedrock", + " bedrock ": "amazon-bedrock", + "vertex": "google-vertex", + "gemini": "google", + "azure": "azure-openai-responses", + "copilot": "github-copilot", + "codex": "openai-codex", + "moonshot": "moonshotai", + "vercel": "vercel-ai-gateway", + "hf": "huggingface", + "anthropic": "anthropic", // canonical passes through + "amazon-bedrock": "amazon-bedrock", // already canonical + "totally-unknown": "totally-unknown", // unknown returned unchanged (lowered) + "Totally-UNKNOWN": "totally-unknown", + "": "", + } + for in, want := range cases { + if got := canonicalProvider(in); got != want { + t.Errorf("canonicalProvider(%q) = %q, want %q", in, got, want) + } + } +} + +func TestCanonicalProviderAliasesAreKnown(t *testing.T) { + for alias, canon := range providerAliases { + if !isKnownProvider(canon) { + t.Errorf("alias %q maps to %q which is not a known provider", alias, canon) + } + } +} diff --git a/packages/provider/amazon_bedrock.go b/packages/provider/amazon_bedrock.go index 4acaa88..56fb36e 100644 --- a/packages/provider/amazon_bedrock.go +++ b/packages/provider/amazon_bedrock.go @@ -317,7 +317,18 @@ func (c *bedrockClient) Stream(ctx context.Context, req Request) (<-chan Event, if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) resp.Body.Close() - return nil, fmt.Errorf("bedrock: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b))) + msg := strings.TrimSpace(string(b)) + // A 403 on the bearer route is almost always a region mismatch: + // short-term Bedrock API keys are scoped to the region of the + // console session that minted them, but zot defaults to + // us-east-1. Surface the resolved region and the fix so the user + // is not left guessing why a freshly-copied key is "invalid". + if resp.StatusCode == http.StatusForbidden && c.bearerToken != "" { + return nil, fmt.Errorf( + "bedrock: http 403 (region=%s): %s\nhint: Bedrock API keys are region-scoped. If your key was created in another region, set AWS_REGION (e.g. AWS_REGION=eu-central-1) or pass --base-url https://bedrock-runtime..amazonaws.com", + c.region, msg) + } + return nil, fmt.Errorf("bedrock: http %d: %s", resp.StatusCode, msg) } out := make(chan Event, 16) go c.runStream(ctx, resp, req, out)