fix(proxy): pass through cross-region prefixed Bedrock model IDs directly (#2330)
## Description When a model ID with a cross-region prefix (`au.`, `us.`, `eu.`, `apac.`, `global.`) is sent to the Bedrock backend, `map_model_id` was normalising it (e.g. `au.anthropic.claude-opus-4-8` → `claude-opus-4-8`) then re-looking it up in the discovery map. If an APPLICATION inference profile wrapping the same foundation model existed in the account, it would be returned — routing the request to a profile the caller is not authorised to invoke, resulting in a 403 from Bedrock even though the system-defined profile is reachable directly. Cross-region prefixed IDs are already fully-qualified system-defined profile IDs; they must pass through unchanged. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/backends/litellm.py`: added early-exit in `map_model_id` — model IDs starting with `au.`, `us.`, `eu.`, `apac.`, or `global.` are returned as `bedrock/<model_id>` without any discovery lookup - `tests/test_bedrock_region.py`: two new regression tests covering the exact failure mode and all five prefix families ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text tests/test_bedrock_region.py ........................................... [ 95%] .. [100%] 45 passed in 4.45s ``` ## Real Behavior Proof - Environment: headroom 0.32.0-dev, `--backend bedrock`, `--region ap-southeast-2`, `--bedrock-profile <BEDROCK_PROFILE>`, proxy on port 8788 - Exact command / steps: Output ``` # Before fix — old map_model_id logic with a contaminated discovery map: # Input: au.anthropic.claude-opus-4-8 # Normalized: claude-opus-4-8 # Resolved: bedrock/<application-inference-profile-arn> <-- 403 # After fix — cross-region prefix detected, passed through directly: curl -s -X POST http://localhost:8788/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: sk-ant-dummy" \ -H "anthropic-version: 2023-06-01" \ -d '{"model":"au.anthropic.claude-opus-4-8","max_tokens":64,"messages":[{"role":"user","content":"Reply with just: fix works"}]}' ``` - Observed result: `{"type":"message","role":"assistant","content":[{"type":"text","text":"fix works"}],"model":"au.anthropic.claude-opus-4-8","stop_reason":"end_turn",...}` — HTTP 200, routed to `bedrock/au.anthropic.claude-opus-4-8` (system-defined profile) rather than the APPLICATION profile ARN - Not tested: `apac.` and `global.` prefixes against a live AWS account (covered by unit tests only) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes ## Additional Notes The existing `_fetch_bedrock_inference_profiles` already filters to `typeEquals="SYSTEM_DEFINED"` so APPLICATION profiles are not added to the discovery map during normal startup. This fix closes the remaining gap where a caller passes a cross-region prefixed ID directly — previously that ID was normalised before lookup, which could accidentally match a stale or externally-injected map entry. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
This commit is contained in:
@@ -697,6 +697,21 @@ class LiteLLMBackend(Backend):
|
||||
if anthropic_model.startswith("arn:aws:"):
|
||||
return f"bedrock/converse/{anthropic_model}"
|
||||
|
||||
# Cross-region prefixed IDs are already fully qualified system-defined
|
||||
# profile IDs — pass through directly. Normalizing and re-looking them
|
||||
# up in the discovery map can route the request to a wrong or
|
||||
# unauthorized profile (e.g. an APPLICATION profile in the same account
|
||||
# that also wraps the same foundation model). This applies whether the
|
||||
# prefix arrives bare ("us.anthropic...") or already LiteLLM-qualified
|
||||
# ("bedrock/us.anthropic...").
|
||||
_CROSS_REGION_PREFIXES = ("au.", "us.", "eu.", "apac.", "global.")
|
||||
if anthropic_model.startswith(_CROSS_REGION_PREFIXES):
|
||||
return f"bedrock/{anthropic_model}"
|
||||
if anthropic_model.startswith("bedrock/") and anthropic_model[
|
||||
len("bedrock/") :
|
||||
].startswith(_CROSS_REGION_PREFIXES):
|
||||
return anthropic_model
|
||||
|
||||
normalized = _normalize_bedrock_profile_id(anthropic_model)
|
||||
if normalized and normalized in self._model_map:
|
||||
return self._model_map[normalized]
|
||||
|
||||
@@ -2145,7 +2145,9 @@
|
||||
|
||||
truncateModel(model) {
|
||||
if (!model) return '-';
|
||||
return model.replace(/^(anthropic\.|openai\.|bedrock\/)/, '')
|
||||
return model.replace(/^(bedrock\/)/, '')
|
||||
.replace(/^(au\.|us\.|eu\.|apac\.|global\.)/, '')
|
||||
.replace(/^(anthropic\.|openai\.)/, '')
|
||||
.replace(/-\d{8}$/, '')
|
||||
.substring(0, 20);
|
||||
},
|
||||
|
||||
@@ -374,6 +374,69 @@ class TestBedrockModelMapping:
|
||||
"bedrock/global.anthropic.claude-opus-4-8"
|
||||
)
|
||||
|
||||
def test_cross_region_prefixed_id_passes_through_directly(self):
|
||||
"""au./us./eu./apac./global. prefixed IDs must be passed straight to
|
||||
Bedrock without discovery remapping. Remapping can route to an
|
||||
APPLICATION inference profile owned by another team, causing a 403.
|
||||
Regression test for the bug reported in BUG_FIX.md."""
|
||||
# Simulate a discovery map that contains an APPLICATION-type profile
|
||||
# for the same underlying model (the bad case: wrong profile selected).
|
||||
bad_app_profile = "bedrock/arn:aws:bedrock:ap-southeast-2:002037730852:application-inference-profile/6lgt8epqa0wf"
|
||||
with patch(
|
||||
"headroom.backends.litellm._fetch_bedrock_inference_profiles",
|
||||
return_value={"claude-opus-4-8": bad_app_profile},
|
||||
):
|
||||
backend = LiteLLMBackend(provider="bedrock", region="ap-southeast-2")
|
||||
# The au. prefix must bypass discovery and pass through directly.
|
||||
assert backend.map_model_id("au.anthropic.claude-opus-4-8") == (
|
||||
"bedrock/au.anthropic.claude-opus-4-8"
|
||||
)
|
||||
|
||||
def test_cross_region_prefixes_all_pass_through(self):
|
||||
"""All five cross-region prefix families pass through without remapping."""
|
||||
with patch(
|
||||
"headroom.backends.litellm._fetch_bedrock_inference_profiles",
|
||||
return_value={},
|
||||
):
|
||||
backend = LiteLLMBackend(provider="bedrock", region="us-east-1")
|
||||
cases = [
|
||||
("au.anthropic.claude-opus-4-8", "bedrock/au.anthropic.claude-opus-4-8"),
|
||||
(
|
||||
"us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
"bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
),
|
||||
(
|
||||
"eu.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
"bedrock/eu.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
),
|
||||
(
|
||||
"apac.anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"bedrock/apac.anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
),
|
||||
("global.anthropic.claude-opus-4-8", "bedrock/global.anthropic.claude-opus-4-8"),
|
||||
]
|
||||
for model_in, expected in cases:
|
||||
assert backend.map_model_id(model_in) == expected, (
|
||||
f"Expected {expected!r} for input {model_in!r}"
|
||||
)
|
||||
|
||||
def test_litellm_qualified_cross_region_id_passes_through_with_contaminated_map(self):
|
||||
"""'bedrock/<cross-region-prefix>...' — the already LiteLLM-qualified form
|
||||
documented in map_model_id's docstring — must also bypass discovery
|
||||
remapping. Regression for the gap where only the bare 'us.anthropic...'
|
||||
form was checked, so 'bedrock/us.anthropic...' still fell through to
|
||||
normalization and could be remapped to a contaminating APPLICATION
|
||||
profile in the discovery map."""
|
||||
bad_app_profile = "bedrock/arn:aws:bedrock:ap-southeast-2:002037730852:application-inference-profile/6lgt8epqa0wf"
|
||||
with patch(
|
||||
"headroom.backends.litellm._fetch_bedrock_inference_profiles",
|
||||
return_value={"claude-opus-4-8": bad_app_profile},
|
||||
):
|
||||
backend = LiteLLMBackend(provider="bedrock", region="ap-southeast-2")
|
||||
assert backend.map_model_id("bedrock/au.anthropic.claude-opus-4-8") == (
|
||||
"bedrock/au.anthropic.claude-opus-4-8"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Normalize Bedrock Profile ID (edge cases)
|
||||
|
||||
Reference in New Issue
Block a user