[shared] Identify a Bedrock geography by the vendor that follows it

The cross-region inference-profile prefix was matched against a list of
four — us, eu, apac, global — so a profile issued under any other
geography kept its prefix through normalization. That form matches no
catalog key, which cost more than a blank price column:

  - discovery returned those models unpriced, so a real account's listing
    came back almost entirely at $0
  - the cost meter keys its table by the same normalized id, and operators
    are told to register a Bedrock id exactly as AWS issues it, region
    prefix included — so the default entry never resolved and every cache
    bucket, and any model priced only by catalog defaults, metered free

Identify the geography by what follows it instead: a leading segment is a
geography when a known Bedrock vendor namespace comes next. New
geographies then need no change at all, and a vendor missing from the map
fails safe by keeping the prefix — the behaviour of the list this
replaces. Over-stripping is the direction that must not happen, since the
normalized id also decides which route may claim a model.

Covered at all three seams the id passes through: the normalizer, the cost
meter config the proxy bills from, and the discovery listing the dashboard
renders. Each test fails against the old four-geography list.
This commit is contained in:
mlsmaycon
2026-08-23 16:05:22 +00:00
parent 84dda2ba8a
commit 80c68bd6f1
4 changed files with 158 additions and 9 deletions

View File

@@ -494,3 +494,39 @@ func TestRegionFromUpstream(t *testing.T) {
})
}
}
// bedrockGeoListing carries profiles from geographies the original prefix list
// did not name. Every one reduces to a catalog key, so every one must arrive
// priced — an unstripped geography is what made a real account's listing come
// back almost entirely at zero.
const bedrockGeoListing = `{"inferenceProfileSummaries":[
{"inferenceProfileId":"jp.anthropic.claude-sonnet-5-20260514-v1:0",
"inferenceProfileName":"JP Anthropic Claude Sonnet 5","status":"ACTIVE","type":"SYSTEM_DEFINED"},
{"inferenceProfileId":"au.anthropic.claude-haiku-4-5-20251001-v1:0",
"inferenceProfileName":"AU Anthropic Claude Haiku 4.5","status":"ACTIVE","type":"SYSTEM_DEFINED"},
{"inferenceProfileId":"us-gov.anthropic.claude-sonnet-5-20260514-v1:0",
"inferenceProfileName":"GovCloud Anthropic Claude Sonnet 5","status":"ACTIVE","type":"SYSTEM_DEFINED"}
]}`
func TestBedrockProfilesFromAnyGeographyArrivePriced(t *testing.T) {
cl, _ := newStubClient(http.StatusOK, bedrockGeoListing)
models, err := cl.Fetch(context.Background(), Request{
CatalogID: "bedrock_api",
UpstreamURL: "https://bedrock-runtime.eu-central-1.amazonaws.com",
APIKey: "aws-token",
})
require.NoError(t, err)
require.Len(t, models, 3)
for _, m := range models {
assert.True(t, m.PricingKnown, "%s must resolve to a catalog rate", m.ID)
assert.Greater(t, m.InputPer1k, 0.0, "input rate for %s", m.ID)
assert.Greater(t, m.OutputPer1k, 0.0, "output rate for %s", m.ID)
assert.Greater(t, m.CacheReadPer1k, 0.0, "cache-read rate for %s", m.ID)
}
// The wire id is preserved whatever the pricing key reduced to: it is the
// only form that works at invoke time.
assert.Equal(t, "jp.anthropic.claude-sonnet-5-20260514-v1:0", models[0].ID)
}

View File

@@ -103,3 +103,37 @@ func TestBuildCostMeterConfig_OrphanAndGatewayProviders(t *testing.T) {
assert.NotContains(t, cfg.Pricing.Providers, "prov-litellm", "empty-models gateway needs no per-record entry")
assert.NotEmpty(t, cfg.Pricing.Defaults["openai"], "defaults still ship so the gateway's catalog-model traffic is priced")
}
// TestBuildCostMeterConfig_BedrockGeographyOutsideTheOriginalFour is the
// accounting half of the geography bug. The docs tell operators to register a
// Bedrock id exactly as AWS issues it, region prefix included, and the cost
// meter keys its table by the normalized form. While the geography was matched
// against a list of four, a profile issued anywhere else kept its prefix,
// missed the catalog entry it was meant to inherit from, and billed with a
// zero entry underneath the operator's own rates — so every cache bucket
// metered free and a model priced only by catalog defaults metered at nothing
// at all.
func TestBuildCostMeterConfig_BedrockGeographyOutsideTheOriginalFour(t *testing.T) {
for _, geo := range []string{"jp", "au", "ca", "sa", "us-gov"} {
t.Run(geo, func(t *testing.T) {
bedrock := &types.Provider{
ID: "prov-bedrock",
ProviderID: "bedrock_api",
Enabled: true,
Models: []types.ProviderModel{
{ID: geo + ".anthropic.claude-sonnet-5-20260514-v1:0", InputPer1k: 0.003, OutputPer1k: 0.015},
},
}
raw, err := buildCostMeterConfigJSON([]*types.Provider{bedrock}, map[string][]string{"prov-bedrock": {"grp"}})
require.NoError(t, err)
cfg := decodeCostMeterConfig(t, raw)
e, ok := cfg.Pricing.Providers["prov-bedrock"]["anthropic.claude-sonnet-5"]
require.True(t, ok, "a %s profile must key by the same normalized id the parser emits", geo)
assert.InDelta(t, 0.0003, e.CacheReadPer1k, 1e-9,
"cache read must be inherited from the bedrock default entry, not left at zero")
assert.InDelta(t, 0.00375, e.CacheCreationPer1k, 1e-9,
"cache creation must be inherited from the bedrock default entry, not left at zero")
})
}
}

View File

@@ -10,9 +10,59 @@ import (
"strings"
)
// bedrockRegionPrefixes are the cross-region inference-profile prefixes that
// front a Bedrock model id (e.g. "eu.anthropic.claude-...").
var bedrockRegionPrefixes = []string{"us.", "eu.", "apac.", "global."}
// bedrockVendorNamespaces are the vendor segments a Bedrock model id is
// published under. They identify the geography in front of a cross-region
// inference profile without enumerating geographies: in
// "eu.anthropic.claude-...", what makes "eu" a geography is that "anthropic"
// follows it.
//
// Listing geographies instead is what this replaced, and it aged badly — the
// list held us, eu, apac and global, so every profile issued under jp, au, ca,
// sa or us-gov carried its prefix into the pricing key, matched no catalog
// entry, and reported the model unpriced.
//
// A vendor missing from this map fails safe: its id keeps the geography, which
// is exactly the behaviour of the list this replaced. Over-stripping is the
// dangerous direction, because the result also decides which route may claim a
// model.
var bedrockVendorNamespaces = map[string]struct{}{
"ai21": {},
"amazon": {},
"anthropic": {},
"cohere": {},
"deepseek": {},
"luma": {},
"meta": {},
"mistral": {},
"openai": {},
"qwen": {},
"stability": {},
"twelvelabs": {},
"writer": {},
}
// stripBedrockGeography removes the cross-region inference-profile geography
// from a Bedrock model id, leaving the "<vendor>.<model>" form the catalog and
// the pricing table key on.
//
// A leading segment counts as a geography only when a known vendor follows it.
// "amazon.nova-pro" is a vendor and a model, not a geography and a model, and
// cutting its first segment would strip the vendor away.
func stripBedrockGeography(modelID string) string {
dot := strings.IndexByte(modelID, '.')
if dot <= 0 {
return modelID
}
rest := modelID[dot+1:]
vendor, _, found := strings.Cut(rest, ".")
if !found {
return modelID
}
if _, ok := bedrockVendorNamespaces[vendor]; !ok {
return modelID
}
return rest
}
// bedrockVersionSuffix matches the trailing "-vN[:N]" or "-YYYYMMDD-vN[:N]"
// version/throughput suffix of a Bedrock model id.
@@ -37,12 +87,7 @@ func NormalizeBedrockModel(modelID string) string {
m = m[i+1:]
}
}
for _, p := range bedrockRegionPrefixes {
if strings.HasPrefix(m, p) {
m = m[len(p):]
break
}
}
m = stripBedrockGeography(m)
return bedrockVersionSuffix.ReplaceAllString(m, "")
}

View File

@@ -60,3 +60,37 @@ func TestNormalizeAnthropicModel(t *testing.T) {
require.Equal(t, want, NormalizeAnthropicModel(in), "normalize %q", in)
}
}
// TestNormalizeBedrockModel_GeographiesBeyondTheOriginalFour covers the bug
// that made this vendor-anchored: the geography used to be matched against a
// list of four, so a profile issued anywhere else kept its prefix, missed the
// catalog key it was supposed to match, and reported the model unpriced.
func TestNormalizeBedrockModel_GeographiesBeyondTheOriginalFour(t *testing.T) {
for _, geo := range []string{"us", "eu", "apac", "global", "jp", "au", "ca", "sa", "us-gov", "il", "mx"} {
t.Run(geo, func(t *testing.T) {
got := NormalizeBedrockModel(geo + ".anthropic.claude-sonnet-5-20260514-v1:0")
require.Equal(t, "anthropic.claude-sonnet-5", got,
"a cross-region profile must reduce to the catalog key whatever geography issued it")
})
}
}
// TestNormalizeBedrockModel_KeepsAVendorItCannotMistakeForAGeography pins the
// direction that must never break: a plain "<vendor>.<model>" id has no
// geography, and cutting its first segment would strip the vendor away and
// hand the id to whichever route claims the bare model name.
func TestNormalizeBedrockModel_KeepsAVendorItCannotMistakeForAGeography(t *testing.T) {
cases := map[string]string{
"amazon.nova-pro-v1:0": "amazon.nova-pro",
"anthropic.claude-sonnet-5-v1:0": "anthropic.claude-sonnet-5",
"meta.llama3-3-70b-instruct-v1:0": "meta.llama3-3-70b-instruct",
"cohere.command-r-plus-v1:0": "cohere.command-r-plus",
"eu.unknownvendor.some-model-v1:0": "eu.unknownvendor.some-model",
"Qwen/Qwen2.5-0.5B-Instruct": "Qwen/Qwen2.5-0.5B-Instruct",
}
for in, want := range cases {
t.Run(in, func(t *testing.T) {
require.Equal(t, want, NormalizeBedrockModel(in))
})
}
}