From 5fb477521f7f6a7f46baf51207f44f60f6ad2703 Mon Sep 17 00:00:00 2001 From: Riddhi04 Date: Wed, 22 Jul 2026 15:58:37 +0400 Subject: [PATCH 1/2] fix(proxy): enforce model access checks on Bedrock passthrough routes get_model_from_request could not resolve a model for /bedrock/... routes since it only checked the JSON body's model field and a small set of URL regexes, none matching Bedrock's passthrough path. This let common_checks skip the key/project model allowlist entirely for any Bedrock passthrough action (invoke, converse, and their streaming variants), while the same model was correctly blocked on /v1/chat/completions Extract the model from the Bedrock endpoint path using the same helper the passthrough handler itself relies on, so the existing allowlist check applies uniformly across auth methods and call paths --- litellm/proxy/auth/auth_utils.py | 11 ++++ .../proxy/auth/test_auth_utils.py | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index ecb37e67c14..52c59831780 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1638,6 +1638,17 @@ def get_model_from_request( if vertex_match: model = vertex_match.group(1) + if model is None and route.lower().startswith("/bedrock"): + from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( + _extract_model_from_bedrock_endpoint, + ) + + bedrock_endpoint = re.sub(r"^/bedrock/", "", route, flags=re.IGNORECASE) + try: + model = _extract_model_from_bedrock_endpoint(bedrock_endpoint) + except ValueError: + model = None + return model diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index 9f24c662581..e8681881f43 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -428,6 +428,56 @@ def test_get_model_from_request_openai_deployment_route_still_works(): ) +def test_get_model_from_request_bedrock_converse_passthrough(): + assert ( + get_model_from_request( + request_data={}, + route="/bedrock/model/us.anthropic.claude-sonnet-4-6/converse", + ) + == "us.anthropic.claude-sonnet-4-6" + ) + + +def test_get_model_from_request_bedrock_invoke_passthrough(): + assert ( + get_model_from_request( + request_data={}, + route="/bedrock/model/us.anthropic.claude-sonnet-4-6/invoke", + ) + == "us.anthropic.claude-sonnet-4-6" + ) + + +def test_get_model_from_request_bedrock_v2_converse_stream_passthrough(): + assert ( + get_model_from_request( + request_data={}, + route="/bedrock/v2/model/us.anthropic.claude-sonnet-4-6/converse-stream", + ) + == "us.anthropic.claude-sonnet-4-6" + ) + + +def test_get_model_from_request_bedrock_model_id_with_slashes(): + assert ( + get_model_from_request( + request_data={}, + route="/bedrock/model/aws/anthropic/model-name/invoke", + ) + == "aws/anthropic/model-name" + ) + + +def test_get_model_from_request_bedrock_unparseable_endpoint_returns_none(): + assert ( + get_model_from_request( + request_data={}, + route="/bedrock/agents/some-agent-route", + ) + is None + ) + + def test_get_model_from_request_includes_file_endpoint_header_model(): assert ( get_model_from_request( From af25b699a54aa7b1c42cdace0e3d717a905c004c Mon Sep 17 00:00:00 2001 From: Riddhi04 Date: Fri, 24 Jul 2026 16:38:34 +0400 Subject: [PATCH 2/2] fix(proxy): make URL model authoritative for Bedrock path-routed passthrough actions The allowlist check read model from the request body first, while bedrock_llm_proxy_route dispatches purely on the path model for invoke, converse, and their streaming variants. A caller could put an allowed model in the JSON body while targeting a disallowed model in the URL and slip past the check. count_tokens keeps reading from the body since its route has no model segment in the path. --- litellm/proxy/auth/auth_utils.py | 22 +++++----- .../proxy/auth/test_auth_utils.py | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 52c59831780..bfc039851ac 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1638,16 +1638,20 @@ def get_model_from_request( if vertex_match: model = vertex_match.group(1) - if model is None and route.lower().startswith("/bedrock"): - from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( - _extract_model_from_bedrock_endpoint, - ) - + if route.lower().startswith("/bedrock"): bedrock_endpoint = re.sub(r"^/bedrock/", "", route, flags=re.IGNORECASE) - try: - model = _extract_model_from_bedrock_endpoint(bedrock_endpoint) - except ValueError: - model = None + is_bedrock_count_tokens_route = ( + "count_tokens" in bedrock_endpoint.lower() or "count-tokens" in bedrock_endpoint.lower() + ) + if not is_bedrock_count_tokens_route: + from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( + _extract_model_from_bedrock_endpoint, + ) + + try: + model = _extract_model_from_bedrock_endpoint(bedrock_endpoint) + except ValueError: + pass return model diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index e8681881f43..f2dcc372672 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -478,6 +478,46 @@ def test_get_model_from_request_bedrock_unparseable_endpoint_returns_none(): ) +def test_get_model_from_request_bedrock_url_model_overrides_body_model(): + assert ( + get_model_from_request( + request_data={"model": "us.anthropic.claude-sonnet-4-6"}, + route="/bedrock/model/us.anthropic.claude-opus-4-6-v1/converse", + ) + == "us.anthropic.claude-opus-4-6-v1" + ) + + +def test_get_model_from_request_bedrock_invoke_url_model_overrides_body_model(): + assert ( + get_model_from_request( + request_data={"model": "us.anthropic.claude-sonnet-4-6"}, + route="/bedrock/model/us.anthropic.claude-opus-4-6-v1/invoke", + ) + == "us.anthropic.claude-opus-4-6-v1" + ) + + +def test_get_model_from_request_bedrock_count_tokens_uses_body_model(): + assert ( + get_model_from_request( + request_data={"model": "us.anthropic.claude-sonnet-4-6"}, + route="/bedrock/v1/messages/count_tokens", + ) + == "us.anthropic.claude-sonnet-4-6" + ) + + +def test_get_model_from_request_bedrock_unparseable_endpoint_keeps_body_model(): + assert ( + get_model_from_request( + request_data={"model": "us.anthropic.claude-sonnet-4-6"}, + route="/bedrock/agents/some-agent-route", + ) + == "us.anthropic.claude-sonnet-4-6" + ) + + def test_get_model_from_request_includes_file_endpoint_header_model(): assert ( get_model_from_request(