mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
fix(bedrock): read invoke capabilities from base_model for an ARN that names no model and map Bedrock's 200 error body to a 404
This commit is contained in:
parent
e11c0d9a9c
commit
e4cebb0fef
10 changed files with 208 additions and 105 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue