feat(provider): alias common provider names and clarify Bedrock 403
Some checks failed
ci / test (macos-latest) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / test (windows-latest) (push) Has been cancelled

Map short/alternate provider names (bedrock -> amazon-bedrock, vertex,
gemini, azure, copilot, codex, ...) to their canonical ids in Resolve so
an alias is never treated as unknown and silently downgraded to
anthropic. Add a region-aware hint to Bedrock 403 responses on the
bearer route.
This commit is contained in:
patriceckhart 2026-06-03 18:13:22 +02:00
parent ea58887bfa
commit ec5eb20ce9
3 changed files with 96 additions and 2 deletions

View file

@ -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.

View file

@ -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)
}
}
}

View file

@ -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.<region>.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)