Merge pull request #32979 from BerriAI/litellm_anchor_bedrock_routing_regex

fix(proxy-auth): stop unrecognized model namespaces slipping through provider wildcard keys
This commit is contained in:
Mateo Wang 2026-07-11 19:18:27 -07:00 • committed by GitHub
commit 053e64e11b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 53 additions and 6 deletions

View file

@ -45051,8 +45051,8 @@
"rules": [
{
"name": "bedrock-claude-ids",
"pattern": "anthropic\\.claude-",
"description": "Any Bedrock-syntax Claude id: the dotted anthropic.claude- segment appears in bare (anthropic.claude-...), region-prefixed (us./eu./au./jp./apac.) and global.-prefixed ids, for every version. Routes these to bedrock before the bare-id Anthropic rule is consulted.",
"pattern": "^(?:[a-z-]+\\.)?anthropic\\.claude-",
"description": "A Bedrock-syntax Claude id, for every version: anthropic.claude- at the start of the name, optionally behind a single dotted geo segment (us./eu./au./jp./apac./global./us-gov.). Anchored to the start because routing rules see the raw request string and provider inference feeds the proxy's provider/* wildcard access checks: an id under an unrecognized namespace such as bedrockz/anthropic.claude-... must stay unroutable rather than resolve to bedrock and slip through a bedrock/* key. Routes to bedrock before the bare-id Anthropic rule is consulted.",
"model_info": {
"litellm_provider": "bedrock"
}

View file

@ -4383,14 +4383,23 @@ def _model_custom_llm_provider_matches_wildcard_pattern(model: str, allowed_mode
or
- `model=claude-3-5-sonnet-20240620`
- `allowed_model_pattern=anthropic/*`
A model that already carries a namespace get_llm_provider did not consume
(e.g. `bedrockz/anthropic.claude-...`) is never granted here: its provider was
inferred from a fragment of the full string, so rebuilding
`{provider}/{model}` would produce `bedrock/bedrockz/...` and slip an
unrecognized namespace through a `bedrock/*` key.
"""
try:
model, custom_llm_provider, _, _ = get_llm_provider(model=model)
stripped_model, custom_llm_provider, _, _ = get_llm_provider(model=model)
except Exception:
return False
if stripped_model == model and "/" in model:
return False
return is_model_allowed_by_pattern(
model=f"{custom_llm_provider}/{model}",
model=f"{custom_llm_provider}/{stripped_model}",
allowed_model_pattern=allowed_model_pattern,
)

View file

@ -45284,8 +45284,8 @@
"rules": [
{
"name": "bedrock-claude-ids",
"pattern": "anthropic\\.claude-",
"description": "Any Bedrock-syntax Claude id: the dotted anthropic.claude- segment appears in bare (anthropic.claude-...), region-prefixed (us./eu./au./jp./apac.) and global.-prefixed ids, for every version. Routes these to bedrock before the bare-id Anthropic rule is consulted.",
"pattern": "^(?:[a-z-]+\\.)?anthropic\\.claude-",
"description": "A Bedrock-syntax Claude id, for every version: anthropic.claude- at the start of the name, optionally behind a single dotted geo segment (us./eu./au./jp./apac./global./us-gov.). Anchored to the start because routing rules see the raw request string and provider inference feeds the proxy's provider/* wildcard access checks: an id under an unrecognized namespace such as bedrockz/anthropic.claude-... must stay unroutable rather than resolve to bedrock and slip through a bedrock/* key. Routes to bedrock before the bare-id Anthropic rule is consulted.",
"model_info": {
"litellm_provider": "bedrock"
}

View file

@ -236,6 +236,8 @@ async def test_can_team_call_model(model, expect_to_work):
(["bedrock/*"], "bedrock/anthropic.claude-3-5-sonnet-20240620", True),
(["bedrock/*"], "bedrockz/anthropic.claude-3-5-sonnet-20240620", False),
(["bedrock/us.*"], "bedrock/us.amazon.nova-micro-v1:0", True),
(["openai/*"], "ft:gpt-4-0613", True),
(["openai/*"], "bedrockz/ft:gpt-4-0613", False),
],
)
@pytest.mark.asyncio

View file

@ -138,6 +138,42 @@ def test_shipped_backup_carries_the_claude_routing_rules():
set_fallback_generalizations(previous)
def test_shipped_routing_rules_never_match_through_an_unrecognized_namespace():
"""Routing rules decide ``litellm_provider`` for otherwise-unknown ids, and the
proxy's wildcard access check (``can_key_call_model`` with a ``bedrock/*`` key)
trusts that inference: it rebuilds ``{provider}/{model}`` and matches it against
the key's patterns. A routing pattern that matches as a substring lets
``bedrockz/anthropic.claude-...`` resolve to bedrock and slip through a
``bedrock/*`` key, so every shipped routing rule must anchor to the start of
the name and never match an id carrying an unrecognized namespace prefix."""
backup = GetModelCostMap.load_local_model_cost_map()
rules = backup[FALLBACK_GENERALIZATIONS_KEY]["rules"]
routing_rules = [r for r in rules if "litellm_provider" in r["model_info"]]
assert routing_rules
assert all(r["pattern"].startswith("^") for r in routing_rules)
previous = list(get_fallback_generalization_rules())
try:
set_fallback_generalizations(rules)
for bedrock_id in [
"anthropic.claude-3-5-sonnet-20240620-v1:0",
"anthropic.claude-v2:1",
"us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0",
"global.anthropic.claude-fable-5-20260120-v1:0",
]:
assert match_routing_generalization(bedrock_id) == "bedrock", bedrock_id
for namespaced in [
"bedrockz/anthropic.claude-3-5-sonnet-20240620",
"bedrockz/us.anthropic.claude-3-5-sonnet-20240620-v1:0",
"bedrockz/claude-3-5-sonnet-20240620",
]:
assert match_routing_generalization(namespaced) is None, namespaced
finally:
set_fallback_generalizations(previous)
def test_shipped_backup_marks_claude_4_6_plus_adaptive_not_4_0():
"""Adaptive thinking is data, not code. The bundled backup must carry
supports_adaptive_thinking on genuine Claude >= 4.6 entries (every provider