mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-10 16:22:17 +00:00
* 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:<ACCOUNT>: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 <ext-agent-shin@berri.ai> Co-authored-by: LiteLLM Bot <bot@berri.ai>
This commit is contained in:
co-authored by
oss-agent-shin
LiteLLM Bot
parent
9600fda2cc
commit
a3c953ed4e
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user