diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 37bed632205..b6dfc235a74 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -622,6 +622,10 @@ class BaseAWSLLM(SignsRequestsWithAWS): if model.startswith("invoke/"): model = model.replace("invoke/", "", 1) + spec_provider: Final = BaseAWSLLM._get_provider_from_model_path(model) + if spec_provider is not None: + return spec_provider + # Special case: Check for "nova" in model name first (before "amazon") # This handles amazon.nova-* models which would otherwise match "amazon" (Titan) if "nova" in model.lower(): @@ -632,11 +636,6 @@ class BaseAWSLLM(SignsRequestsWithAWS): if _split_model in get_args(BEDROCK_INVOKE_PROVIDERS_LITERAL): return cast(BEDROCK_INVOKE_PROVIDERS_LITERAL, _split_model) - # If not a known provider, check for pattern with two slashes - provider = BaseAWSLLM._get_provider_from_model_path(model) - if provider is not None: - return provider - invoke_providers: Final[tuple[BEDROCK_INVOKE_PROVIDERS_LITERAL, ...]] = get_args( BEDROCK_INVOKE_PROVIDERS_LITERAL ) @@ -669,7 +668,7 @@ class BaseAWSLLM(SignsRequestsWithAWS): # like "bedrock/invoke/arn:..." are fully stripped in one call. from litellm.llms.bedrock.common_utils import strip_bedrock_routing_prefix - model_id = BaseAWSLLM._without_invoke_provider_spec(strip_bedrock_routing_prefix(model_id)) + model_id = BaseAWSLLM.without_provider_spec(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) @@ -697,7 +696,7 @@ class BaseAWSLLM(SignsRequestsWithAWS): return model_id @staticmethod - def _without_invoke_provider_spec(model_id: str) -> str: + def without_provider_spec(model_id: str) -> str: _, _, arn = model_id.partition("/") return arn if arn.startswith("arn:") else model_id diff --git a/litellm/llms/bedrock/chat/converse_handler.py b/litellm/llms/bedrock/chat/converse_handler.py index a6f9192339f..1b94f2e288f 100644 --- a/litellm/llms/bedrock/chat/converse_handler.py +++ b/litellm/llms/bedrock/chat/converse_handler.py @@ -22,7 +22,13 @@ from litellm.types.utils import ModelResponse from litellm.utils import CustomStreamWrapper from ..base_aws_llm import BaseAWSLLM, Credentials, bedrock_bearer_token, pop_aws_auth_params, run_aws_signing -from ..common_utils import BedrockError, error_response_text, get_all_bedrock_regions +from ..common_utils import ( + BedrockError, + bedrock_answered_json, + bedrock_error_in_success_body, + error_response_text, + get_all_bedrock_regions, +) from .invoke_handler import AWSEventStreamDecoder, make_call @@ -68,6 +74,12 @@ def make_sync_call( response=response, ) + if bedrock_answered_json(response): + response.read() + success_body_error: Final = bedrock_error_in_success_body(response) + if success_body_error is not None: + raise success_body_error + if fake_stream: model_response: Final[ModelResponse] = litellm.AmazonConverseConfig()._transform_response( model=model, @@ -326,7 +338,7 @@ class BedrockConverseLLM(BaseAWSLLM): if _stripped.startswith(_nova_prefix): _model_for_id = _model_for_id.replace(_nova_prefix, "", 1) break - modelId = self.encode_model_id(model_id=_model_for_id) + modelId = self.encode_model_id(model_id=self.without_provider_spec(_model_for_id)) # Inject region extracted from model path so _get_aws_region_name picks it up if _region_from_model is not None and "aws_region_name" not in optional_params: optional_params["aws_region_name"] = _region_from_model diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 443e722168d..f6373ebead7 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -128,6 +128,8 @@ from ..common_utils import ( bedrock_model_accepts_cache_points, bedrock_reasoning_effort_disabled, bedrock_rejects_stop_sequences, + bedrock_service_error, + bedrock_service_error_type, get_anthropic_beta_from_headers, get_bedrock_tool_name, is_bedrock_application_inference_profile_arn, @@ -2747,13 +2749,17 @@ class AmazonConverseConfig(BaseConfig): ) ## RESPONSE OBJECT try: - completion_response: Final = ConverseResponseBlock(**response.json()) + response_json: Final = response.json() + completion_response: Final = ConverseResponseBlock(**response_json) except Exception as e: raise BedrockError( message=f"Error converting to valid response block={e}. File an issue if litellm error - https://github.com/BerriAI/litellm/issues", status_code=422, headers=response.headers, ) + service_error_type: Final = bedrock_service_error_type(response_json) + if service_error_type is not None: + raise bedrock_service_error(service_error_type, response.headers) """ Bedrock Response Object has optional message block diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py index a928ab56677..38bc656068a 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py @@ -26,6 +26,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation from litellm.llms.bedrock.common_utils import ( BEDROCK_INVOKE_TRACE_FIELD, apply_bedrock_invoke_structured_output, + bedrock_capability_model, bedrock_chat_uses_native_structured_output, bedrock_cleaned_output_schema, bedrock_error_in_success_body, @@ -193,8 +194,11 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): litellm_params: dict, headers: dict, ) -> dict: + capability_model: Final = bedrock_capability_model( + model, _LITELLM_PARAMS.validate_python(litellm_params).get("base_model") + ) _anthropic_request: Final = self._build_bedrock_anthropic_request_base( - model=model, + model=capability_model, messages=messages, optional_params=optional_params, litellm_params=litellm_params, @@ -203,7 +207,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): self._convert_document_url_sources_to_base64(_anthropic_request) beta_list: Final = self._compute_bedrock_invoke_beta_headers( - model=model, + model=capability_model, messages=messages, optional_params=optional_params, headers=headers, @@ -217,7 +221,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): _anthropic_request, # pyright: ignore[reportUnknownArgumentType] # built by the base signature's bare dict transform {}, drop_params=litellm_params.get("drop_params"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # litellm_params is the base signature's bare dict - model=model, + model=capability_model, provider="bedrock_invoke", ) ) diff --git a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py index 17f929a1f0f..f83bae2146f 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py @@ -2,7 +2,7 @@ import copy import json import time from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Final, cast, get_args +from typing import TYPE_CHECKING, Any, Final import httpx from pydantic import ConfigDict, TypeAdapter, ValidationError, with_config @@ -74,10 +74,6 @@ _GUARDRAIL_CONFIG_EXPECTED_FORMAT: Final = ( "{'guardrailIdentifier': str, 'guardrailVersion': str, 'trace': 'enabled'|'disabled'|'enabled_full'}" ) -_INVOKE_PROVIDERS: Final = TypeAdapter(tuple[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL, ...]).validate_python( - get_args(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL) -) - def _response_header(headers: Mapping[str, str], name: str) -> str | None: return headers.get(name, None) @@ -562,62 +558,6 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM): """ return False - @staticmethod - def get_bedrock_invoke_provider( - model: str, - ) -> litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL | None: - """ - Helper function to get the bedrock provider from the model - - handles 4 scenarios: - 1. model=invoke/anthropic.claude-3-5-sonnet-20240620-v1:0 -> Returns `anthropic` - 2. model=anthropic.claude-3-5-sonnet-20240620-v1:0 -> Returns `anthropic` - 3. model=llama/arn:aws:bedrock:us-east-1:086734376398:imported-model/r4c4kewx2s0n -> Returns `llama` - 4. model=us.amazon.nova-pro-v1:0 -> Returns `nova` - """ - if model.startswith("invoke/"): - model = model.replace("invoke/", "", 1) - - # Special case: Check for "nova" in model name first (before "amazon") - # This handles amazon.nova-* models which would otherwise match "amazon" (Titan) - if "nova" in model.lower(): - if "nova" in get_args(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL): - return cast(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL, "nova") - - _split_model: Final = model.split(".")[0] - if _split_model in get_args(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL): - return cast(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL, _split_model) - - # If not a known provider, check for pattern with two slashes - provider = AmazonInvokeConfig._get_provider_from_model_path(model) - if provider is not None: - return provider - - for provider in _INVOKE_PROVIDERS: - if provider in model: - return provider - return None - - @staticmethod - def _get_provider_from_model_path( - model_path: str, - ) -> litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL | None: - """ - Helper function to get the provider from a model path with format: provider/model-name - - Args: - model_path (str): The model path (e.g., 'llama/arn:aws:bedrock:us-east-1:086734376398:imported-model/r4c4kewx2s0n' or 'anthropic/model-name') - - Returns: - Optional[str]: The provider name, or None if no valid provider found - """ - parts: Final = model_path.split("/") - if len(parts) >= 1: - provider: Final = parts[0] - if provider in get_args(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL): - return cast(litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL, provider) - return None - def convert_messages_to_prompt(self, model: str, messages, provider, custom_prompt_dict) -> tuple[str, list | None]: # handle anthropic prompts and amazon titan prompts prompt = "" diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 29cbbcfa6d9..bbe4841bbf1 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -130,6 +130,17 @@ class _BedrockServiceErrorBody(BaseModel): Output: _BedrockServiceErrorOutput +def bedrock_service_error(error_type: str, headers: httpx.Headers) -> BedrockError: + return BedrockError( + status_code=404, + message=( + f"Bedrock answered HTTP 200 with the error {error_type}, which usually means the model id in the " + "model string does not name a Bedrock model or profile this route can call." + ), + headers=headers, + ) + + def bedrock_error_in_success_body(response: httpx.Response) -> BedrockError | None: if b'"__type"' not in response.content: return None @@ -137,14 +148,16 @@ def bedrock_error_in_success_body(response: httpx.Response) -> BedrockError | No error_type: Final = _BedrockServiceErrorBody.model_validate_json(response.content).Output.error_type except ValidationError: return None - return BedrockError( - status_code=400, - message=( - f"Bedrock answered HTTP 200 with the error {error_type}, which usually means the model id in the " - "model string does not name a Bedrock model or profile this route can call." - ), - headers=response.headers, - ) + return bedrock_service_error(error_type, response.headers) + + +def bedrock_service_error_type(body: object) -> str | None: + if not isinstance(body, dict) or "Output" not in body: + return None + try: + return _BedrockServiceErrorBody.model_validate(body).Output.error_type + except ValidationError: + return None def bedrock_answered_json(response: httpx.Response) -> bool: @@ -1098,10 +1111,23 @@ def is_bedrock_application_inference_profile_arn(model: str) -> bool: return ":application-inference-profile/" in model +_BEDROCK_ARN_RESOURCES_NAMING_NO_MODEL: Final = ( + ":application-inference-profile/", + ":provisioned-model/", + ":imported-model/", + ":custom-model-deployment/", +) + + +def bedrock_arn_names_no_model(model: str) -> bool: + return any(resource in model for resource in _BEDROCK_ARN_RESOURCES_NAMING_NO_MODEL) + + def bedrock_capability_model(model: str, base_model: object) -> str: """The model id to look capabilities up by: the deployment's ``base_model`` for an - application inference profile ARN, since the ARN names no model, else ``model``.""" - if isinstance(base_model, str) and base_model and is_bedrock_application_inference_profile_arn(model): + ARN whose resource id names no model (an application inference profile, a provisioned + or imported model, a custom model deployment), else ``model``.""" + if isinstance(base_model, str) and base_model and bedrock_arn_names_no_model(model): return strip_bedrock_routing_prefix(base_model) return model @@ -1114,10 +1140,13 @@ def invoke_model_with_base_model_provider(model: str, base_model: object) -> str not isinstance(base_model, str) or not model_id.startswith("arn:") or BedrockModelInfo.get_bedrock_route(model) != "invoke" - or BaseAWSLLM.get_bedrock_invoke_provider(model) is not None + or (not bedrock_arn_names_no_model(model_id) and BaseAWSLLM.get_bedrock_invoke_provider(model) is not None) ): return model - provider: Final = BaseAWSLLM.get_bedrock_invoke_provider(strip_bedrock_routing_prefix(base_model)) + base_model_id: Final = strip_bedrock_routing_prefix(base_model) + provider: Final = BaseAWSLLM.get_bedrock_invoke_provider(base_model_id) or ( + "anthropic" if "claude" in base_model_id.lower() else None + ) return model if provider is None else f"invoke/{provider}/{model_id}" diff --git a/litellm/utils.py b/litellm/utils.py index 99368426171..2b76682e9c9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5600,13 +5600,14 @@ def get_optional_params( drop_params=bool(drop_params), ) elif custom_llm_provider == "bedrock": + from litellm.llms.bedrock.common_utils import bedrock_capability_model + bedrock_model_info: Final[type[BedrockModelInfo]] = litellm_utils.BedrockModelInfo bedrock_base_model: Final = bedrock_model_info.get_base_model(model) + capability_model: Final = bedrock_capability_model(model, base_model) if bedrock_route == "converse" or bedrock_route == "converse_like": - from litellm.llms.bedrock.common_utils import bedrock_capability_model - optional_params = litellm.AmazonConverseConfig().map_openai_params( - model=bedrock_capability_model(model, base_model), + model=capability_model, non_default_params=non_default_params, optional_params=optional_params, drop_params=bool(drop_params), @@ -5631,14 +5632,14 @@ def get_optional_params( optional_params = litellm.AmazonAnthropicClaudeConfig(model_info=model_info).map_openai_params( non_default_params=non_default_params, optional_params=optional_params, - model=model, + model=capability_model, drop_params=bool(drop_params), ) elif provider_config is not None: optional_params = provider_config.map_openai_params( non_default_params=non_default_params, optional_params=optional_params, - model=model, + model=capability_model, drop_params=bool(drop_params), ) if bedrock_route == "claude_platform": diff --git a/tests/unit/llms/bedrock/chat/test_invoke_handler.py b/tests/unit/llms/bedrock/chat/test_invoke_handler.py index 5978101dcd9..7073e02e48d 100644 --- a/tests/unit/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/unit/llms/bedrock/chat/test_invoke_handler.py @@ -1987,6 +1987,7 @@ def test_invoke_stream_decoder_carries_the_guardrail_trace_of_the_message_stop_c _ARN_ACCOUNT: Final = "arn:aws:bedrock:us-east-1:123456789012" _APPLICATION_PROFILE_ARN: Final = f"{_ARN_ACCOUNT}:application-inference-profile/a1b2c3d4e5f6" +_NOVA_LOOKING_PROFILE_ARN: Final = f"{_ARN_ACCOUNT}:application-inference-profile/nova1meta2ab" _CLAUDE_INVOKE_ANSWER: Final = json.dumps( { "id": "msg_1", @@ -2079,6 +2080,12 @@ def _answer_text(response: object) -> str: {"model_info": {"base_model": "us.anthropic.claude-haiku-5-5"}}, id="model-info-base-model", ), + pytest.param( + f"bedrock/invoke/{_APPLICATION_PROFILE_ARN}", + _APPLICATION_PROFILE_ARN, + {"base_model": "claude-haiku-5-5"}, + id="claude-base-model-without-a-bedrock-prefix", + ), pytest.param( f"bedrock/invoke/{_ARN_ACCOUNT}:custom-model/anthropic.claude-haiku-5-5/a1b2c3d4e5f6", f"{_ARN_ACCOUNT}:custom-model/anthropic.claude-haiku-5-5/a1b2c3d4e5f6", @@ -2091,6 +2098,18 @@ def _answer_text(response: object) -> str: {}, id="custom-model-under-unlisted-spec", ), + pytest.param( + f"bedrock/invoke/anthropic/{_NOVA_LOOKING_PROFILE_ARN}", + _NOVA_LOOKING_PROFILE_ARN, + {}, + id="spec-over-provider-name-in-profile-id", + ), + pytest.param( + f"bedrock/invoke/{_NOVA_LOOKING_PROFILE_ARN}", + _NOVA_LOOKING_PROFILE_ARN, + {"base_model": "bedrock/us.anthropic.claude-haiku-5-5"}, + id="base-model-over-provider-name-in-profile-id", + ), ], ) def test_an_arn_in_the_invoke_model_string_reaches_bedrock_as_the_encoded_model_id( @@ -2147,8 +2166,8 @@ def _unknown_operation_upstream() -> httpx.MockTransport: ) -def _assert_unknown_operation_is_a_bad_request(error: litellm.BadRequestError) -> None: - assert error.status_code == 400 +def _assert_unknown_operation_is_not_found(error: litellm.NotFoundError) -> None: + assert error.status_code == 404 assert "com.amazon.coral.service#UnknownOperationException" in error.message @@ -2159,10 +2178,14 @@ def _assert_unknown_operation_is_a_bad_request(error: litellm.BadRequestError) - "bedrock/invoke/anthropic.claude-haiku-5-5", "bedrock/invoke/mistral.mistral-large-2407-v1:0", "bedrock/invoke/cohere.command-r-plus-v1:0", + "bedrock/invoke/us.amazon.nova-pro-v1:0", + "bedrock/converse/us.anthropic.claude-haiku-5-5", ], ) -def test_a_200_unknown_operation_body_is_a_bad_request(_aws_test_credentials: None, model: str, stream: bool) -> None: - with pytest.raises(litellm.BadRequestError) as exc_info: +def test_a_200_unknown_operation_body_is_a_not_found_error( + _aws_test_credentials: None, model: str, stream: bool +) -> None: + with pytest.raises(litellm.NotFoundError) as exc_info: _answer_text( litellm.completion( model=model, @@ -2173,7 +2196,7 @@ def test_a_200_unknown_operation_body_is_a_bad_request(_aws_test_credentials: No ) ) - _assert_unknown_operation_is_a_bad_request(exc_info.value) + _assert_unknown_operation_is_not_found(exc_info.value) async def _drain_async_unknown_operation_stream() -> None: @@ -2188,11 +2211,13 @@ async def _drain_async_unknown_operation_stream() -> None: @pytest.mark.asyncio -async def test_a_200_unknown_operation_body_is_a_bad_request_on_an_async_stream(_aws_test_credentials: None) -> None: - with pytest.raises(litellm.BadRequestError) as exc_info: +async def test_a_200_unknown_operation_body_is_a_not_found_error_on_an_async_stream( + _aws_test_credentials: None, +) -> None: + with pytest.raises(litellm.NotFoundError) as exc_info: await _drain_async_unknown_operation_stream() - _assert_unknown_operation_is_a_bad_request(exc_info.value) + _assert_unknown_operation_is_not_found(exc_info.value) def test_a_tool_call_whose_arguments_carry_a_type_key_is_not_read_as_a_bedrock_error( @@ -2228,3 +2253,74 @@ def test_a_tool_call_whose_arguments_carry_a_type_key_is_not_read_as_a_bedrock_e tool_calls: Final = response.choices[0].message.tool_calls assert tool_calls is not None assert json.loads(tool_calls[0].function.arguments) == arguments + + +def _sent_invoke_body(model: str, deployment: Mapping[str, object]) -> object: + sent: Final[list[object]] = [] # mutable-ok: the scripted upstream records each body it receives + + def answer(request: httpx.Request) -> httpx.Response: + sent.append(json.loads(request.content)) + return httpx.Response( + 200, content=_CLAUDE_INVOKE_ANSWER, headers={"content-type": "application/json"}, request=request + ) + + _ = litellm.completion( + model=model, + messages=[{"role": "user", "content": "hi"}], + max_tokens=3000, + reasoning_effort="low", + client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(answer))), + **deployment, + ) + (body,) = sent + return body + + +@pytest.mark.usefixtures("local_model_cost_map") +@pytest.mark.parametrize( + "model", + [ + f"bedrock/invoke/{_APPLICATION_PROFILE_ARN}", + f"bedrock/invoke/anthropic/{_APPLICATION_PROFILE_ARN}", + f"bedrock/invoke/{_ARN_ACCOUNT}:provisioned-model/nova1meta2ab", + ], + ids=["arn", "arn-under-spec", "provisioned-arn"], +) +def test_an_arn_with_base_model_sends_the_body_the_base_model_would_send( + _aws_test_credentials: None, model: str +) -> None: + base_model: Final = "us.anthropic.claude-haiku-5-5" + + arn_body: Final = _sent_invoke_body(model, {"base_model": f"bedrock/{base_model}"}) + + assert arn_body == _sent_invoke_body(f"bedrock/invoke/{base_model}", {}) + + +_CONVERSE_ANSWER: Final = json.dumps( + { + "output": {"message": {"role": "assistant", "content": [{"text": "hi"}]}}, + "stopReason": "end_turn", + "usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 2}, + "metrics": {"latencyMs": 1}, + } +).encode() + + +def test_a_provider_spec_before_an_arn_on_converse_is_left_out_of_the_model_id(_aws_test_credentials: None) -> None: + expected_path: Final = f"/model/{urllib.parse.quote(_APPLICATION_PROFILE_ARN, safe='')}/converse" + + def answer(request: httpx.Request) -> httpx.Response: + if request.url.raw_path.decode() != expected_path: + return httpx.Response(404, json={"message": f"unexpected path {request.url.raw_path}"}, request=request) + return httpx.Response( + 200, content=_CONVERSE_ANSWER, headers={"content-type": "application/json"}, request=request + ) + + response: Final = litellm.completion( + model=f"bedrock/anthropic/{_APPLICATION_PROFILE_ARN}", + messages=[{"role": "user", "content": "hi"}], + max_tokens=10, + client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(answer))), + ) + + assert _answer_text(response) == "hi" diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index 4ff48a9856a..5a0d0e31ac7 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py @@ -4565,12 +4565,12 @@ _UNKNOWN_OPERATION_BODY: Final = { @pytest.mark.usefixtures("local_model_cost_map") @pytest.mark.parametrize( ("stream", "raised"), - [(False, litellm.BadRequestError), (True, BedrockError)], + [(False, litellm.NotFoundError), (True, BedrockError)], ids=["non-stream", "stream"], ) -async def test_bedrock_messages_raise_a_400_when_bedrock_answers_200_with_an_unknown_operation_body( +async def test_bedrock_messages_raise_a_404_when_bedrock_answers_200_with_an_unknown_operation_body( stream: bool, - raised: type[litellm.BadRequestError] | type[BedrockError], + raised: type[litellm.NotFoundError] | type[BedrockError], ) -> None: bedrock: Final = ScriptedBedrockInvoke(replies=((200, _UNKNOWN_OPERATION_BODY),)) @@ -4591,5 +4591,5 @@ async def test_bedrock_messages_raise_a_400_when_bedrock_answers_200_with_an_unk with pytest.raises(raised) as error: await _call_and_drain() - assert error.value.status_code == 400 + assert error.value.status_code == 404 assert "UnknownOperationException" in str(error.value) diff --git a/tests/unit/llms/bedrock/test_bedrock_common_utils.py b/tests/unit/llms/bedrock/test_bedrock_common_utils.py index dd89d9c46b6..3aad0fd5a9f 100644 --- a/tests/unit/llms/bedrock/test_bedrock_common_utils.py +++ b/tests/unit/llms/bedrock/test_bedrock_common_utils.py @@ -2063,10 +2063,26 @@ def test_event_decoder_reports_only_missing_aws_dependency(missing): "arn:aws:bedrock:us-east-1:000000000000:application-inference-profile/profile-id", ), ("us.anthropic.claude-opus-4-8", "anthropic.claude-sonnet-4-5", "us.anthropic.claude-opus-4-8"), + ( + "invoke/anthropic/arn:aws:bedrock:us-east-1:000000000000:provisioned-model/abc123", + "bedrock/us.anthropic.claude-opus-4-8", + "us.anthropic.claude-opus-4-8", + ), + ( + "arn:aws:bedrock:us-east-1:000000000000:inference-profile/us.anthropic.claude-opus-4-8", + "anthropic.claude-sonnet-4-5", + "arn:aws:bedrock:us-east-1:000000000000:inference-profile/us.anthropic.claude-opus-4-8", + ), + ], + ids=[ + "profile-arn-with-base-model", + "profile-arn-without-base-model", + "named-model-keeps-its-name", + "provisioned-arn-with-base-model", + "system-profile-arn-keeps-the-model-it-names", ], - ids=["profile-arn-with-base-model", "profile-arn-without-base-model", "named-model-keeps-its-name"], ) -def test_bedrock_capability_model_reads_base_model_only_for_application_profile_arns(model, base_model, expected): +def test_bedrock_capability_model_reads_base_model_only_for_arns_that_name_no_model(model, base_model, expected): from litellm.llms.bedrock.common_utils import bedrock_capability_model assert bedrock_capability_model(model, base_model) == expected