From a3c953ed4e7eb583cb80a78f280325d0ac447702 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 22 May 2026 12:10:37 -0700 Subject: [PATCH] style: apply black formatting to fix lint CI (LIT-3274) (#28639) (#28641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(bedrock): strip bedrock/ prefix and URL-encode ARNs in get_bedrock_model_id for invoke path The invoke path (used by /v1/messages → Anthropic SDK / Claude Code) called get_bedrock_model_id() which, when falling back to the raw model string, did not strip the 'bedrock/' routing prefix and did not URL-encode ARNs. For a model like: bedrock/arn:aws:bedrock:us-east-1::inference-profile/global.anthropic... the URL built was: /model/bedrock/arn:aws:bedrock:…/invoke-with-response-stream ❌ Bedrock returned a JSON error body. LiteLLM's AWSEventStreamDecoder passed those bytes into botocore's EventStreamBuffer which expects binary event-stream framing. Checksum validation failed on the JSON prelude (0x223a7b22 == ':{"') producing a misleading botocore.eventstream.ChecksumMismatch instead of the actual Bedrock error. Fix: strip 'bedrock/' (and 'invoke/') routing prefix from model string, then URL-encode if the result is an ARN — matching what the converse path already does in converse_handler.py. Fixes: LIT-3274 * fix(bedrock): use strip_bedrock_routing_prefix to handle compound prefixes Address greptile review: the original fix used a loop with break, so bedrock/invoke/arn:... only stripped bedrock/ leaving invoke/arn:... which is not an ARN → fell through to .replace('invoke/','',1) → bare unencoded ARN → same malformed-URL bug. strip_bedrock_routing_prefix() iterates without break, correctly stripping bedrock/ then invoke/ in sequence. Also adds test case for the compound-prefix scenario. * style: apply black formatting to fix lint CI (LIT-3274) --------- Co-authored-by: oss-agent-shin Co-authored-by: LiteLLM Bot --- litellm/llms/bedrock/base_aws_llm.py | 18 ++++ .../llms/bedrock/test_base_aws_llm.py | 99 +++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 9dd2b055a1..8b316a587b 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -450,6 +450,24 @@ class BaseAWSLLM: model_id = BaseAWSLLM.encode_model_id(model_id=model_id) else: model_id = model + # Strip LiteLLM routing prefixes (e.g. "bedrock/", "invoke/", + # "bedrock/invoke/", "bedrock/converse/") that are not part of the + # actual Bedrock model ID. The converse path already does this; the + # invoke path must do the same so that ARN models such as + # bedrock/arn:aws:bedrock:…:inference-profile/global.anthropic.… + # are not forwarded verbatim to the Bedrock API, which would produce + # a malformed URL and cause botocore's EventStreamBuffer to receive + # a JSON error body instead of a binary event-stream — surfaced as a + # misleading ChecksumMismatch (0x223a7b22 == ':{"'). + # Use strip_bedrock_routing_prefix (no break) so compound prefixes + # like "bedrock/invoke/arn:..." are fully stripped in one call. + from litellm.llms.bedrock.common_utils import strip_bedrock_routing_prefix + + model_id = strip_bedrock_routing_prefix(model_id) + # URL-encode ARNs so colons and slashes are safe in the URL path. + if model_id.startswith("arn:"): + model_id = BaseAWSLLM.encode_model_id(model_id=model_id) + return model_id model_id = model_id.replace("invoke/", "", 1) if provider == "llama" and "llama/" in model_id: diff --git a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py index a4969e5dac..10fc358e3a 100644 --- a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py +++ b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py @@ -2112,3 +2112,102 @@ def test_is_already_running_as_role_ssl_verify_passed(): mock_boto3_client.assert_called_once_with( "sts", verify="/path/to/ca-bundle.crt" ) + + +# --------------------------------------------------------------------------- +# LIT-3274: get_bedrock_model_id must strip "bedrock/" prefix and URL-encode +# ARNs for the invoke path (invoke-with-response-stream). Without this fix +# the Bedrock API receives a malformed URL, returns a JSON error body, and +# botocore's EventStreamBuffer raises ChecksumMismatch instead of the real +# error. 0x223a7b22 == ':{\"' — the start of a JSON object. +# --------------------------------------------------------------------------- + + +class TestGetBedrockModelIdArnHandling: + """Unit tests for get_bedrock_model_id with inference-profile ARNs.""" + + ARN = "arn:aws:bedrock:us-east-1:086734376398:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0" + + def _call(self, model: str, optional_params: dict | None = None) -> str: + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + provider = BaseAWSLLM.get_bedrock_invoke_provider(model) + return BaseAWSLLM.get_bedrock_model_id( + model=model, + provider=provider, + optional_params=optional_params or {}, + ) + + def test_arn_with_bedrock_prefix_is_stripped_and_encoded(self): + """bedrock/arn:... must not appear verbatim in the model_id.""" + model_id = self._call(f"bedrock/{self.ARN}") + assert ( + "bedrock/arn" not in model_id + ), f"'bedrock/' prefix not stripped; got: {model_id}" + # Must be URL-encoded (colons → %3A) + assert "%3A" in model_id, f"ARN not URL-encoded; got: {model_id}" + assert "%2F" in model_id, f"ARN slashes not URL-encoded; got: {model_id}" + + def test_arn_with_compound_bedrock_invoke_prefix_is_fully_stripped_and_encoded( + self, + ): + """bedrock/invoke/arn:... — compound prefix — must be fully stripped. + + The old fix used ``break`` after the first matched prefix, so + ``bedrock/invoke/arn:...`` would only strip ``bedrock/``, leaving + ``invoke/arn:...``. The subsequent ``.replace('invoke/', '')`` call + then returned the bare unencoded ARN, reproducing the same + malformed-URL bug the fix aimed to prevent. + + strip_bedrock_routing_prefix() has no break and handles this correctly. + """ + model_id = self._call(f"bedrock/invoke/{self.ARN}") + assert ( + "invoke/" not in model_id + ), f"'invoke/' prefix not stripped; got: {model_id}" + assert ( + "bedrock/" not in model_id + ), f"'bedrock/' prefix not stripped; got: {model_id}" + assert "%3A" in model_id, f"ARN not URL-encoded; got: {model_id}" + assert "%2F" in model_id, f"ARN slashes not URL-encoded; got: {model_id}" + + def test_bare_arn_is_encoded(self): + """Direct ARN without routing prefix must also be URL-encoded.""" + model_id = self._call(self.ARN) + assert "%3A" in model_id, f"ARN not URL-encoded; got: {model_id}" + assert "%2F" in model_id, f"ARN slashes not URL-encoded; got: {model_id}" + + def test_arn_url_matches_expected(self): + """Full URL built from messages config must match expected encoded form.""" + import urllib.parse + from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import ( + AmazonAnthropicClaudeMessagesConfig, + ) + + config = AmazonAnthropicClaudeMessagesConfig() + url = config.get_complete_url( + api_base=None, + api_key=None, + model=f"bedrock/{self.ARN}", + optional_params={"aws_region_name": "us-east-1"}, + litellm_params={}, + stream=True, + ) + encoded_arn = urllib.parse.quote(self.ARN, safe="") + expected = ( + f"https://bedrock-runtime.us-east-1.amazonaws.com" + f"/model/{encoded_arn}/invoke-with-response-stream" + ) + assert ( + url == expected + ), f"URL mismatch:\n got: {url}\n expected: {expected}" + + def test_regular_model_id_unaffected(self): + """Non-ARN model IDs must continue to work as before.""" + model_id = self._call("anthropic.claude-3-sonnet-20240229-v1:0") + assert model_id == "anthropic.claude-3-sonnet-20240229-v1:0" + + def test_invoke_prefixed_model_unaffected(self): + """invoke/ prefix stripping still works after the fix.""" + model_id = self._call("invoke/anthropic.claude-3-sonnet-20240229-v1:0") + assert model_id == "anthropic.claude-3-sonnet-20240229-v1:0"