diff --git a/tests/unit/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py b/tests/unit/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py index cf65facf6ee..2b84a606492 100644 --- a/tests/unit/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py +++ b/tests/unit/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py @@ -2,6 +2,7 @@ import json import re +from typing import Final import httpx import pytest @@ -656,7 +657,7 @@ def _assert_remote_images_inlined(content): def test_transform_request_inlines_remote_image_urls(local_cost_map, monkeypatch): - import litellm.litellm_core_utils.prompt_templates.image_handling as image_handling + from litellm.litellm_core_utils.prompt_templates import image_handling monkeypatch.setattr(image_handling, "convert_url_to_base64", lambda url: f"data:image/png;base64,{url}") body = AmazonBedrockRuntimeChatCompletionsConfig().transform_request( @@ -671,7 +672,7 @@ def test_transform_request_inlines_remote_image_urls(local_cost_map, monkeypatch async def test_async_transform_request_inlines_remote_image_urls(local_cost_map, monkeypatch): - import litellm.litellm_core_utils.prompt_templates.image_handling as image_handling + from litellm.litellm_core_utils.prompt_templates import image_handling async def fake_convert(url, timeout=None): return f"data:image/png;base64,{url}" @@ -1706,13 +1707,15 @@ def test_positional_tool_call_ids_are_minted_for_gpt_too(local_cost_map, fake_aw assert all(re.fullmatch(r"call_[0-9a-f]{32}", tool_call_id) for tool_call_id in ids) -def test_native_chat_completions_drops_service_tier_the_model_does_not_sell(local_cost_map, monkeypatch): - """GPT 5.6 Terra on Bedrock answers priority/flex with `'priority' is not supported for - 'service_tier' on this model`, so under drop_params the tier is dropped before the call.""" +# GPT 5.6 Terra on Bedrock answers priority and flex with `'priority' is not supported for 'service_tier' +# on this model`, checked live on 2026-10-27 (LIT-9793). Its price-map row carries null tier prices to match. +def test_native_chat_completions_drops_service_tier_the_model_does_not_sell( + local_cost_map: object, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonBedrockRuntimeChatCompletionsConfig() + config: Final = AmazonBedrockRuntimeChatCompletionsConfig() - result = config.map_openai_params( + result: Final = config.map_openai_params( non_default_params={"service_tier": "priority", "max_tokens": 5}, optional_params={}, model="us.openai.gpt-5.6-terra", @@ -1723,9 +1726,11 @@ def test_native_chat_completions_drops_service_tier_the_model_does_not_sell(loca assert result["max_completion_tokens"] == 5 -def test_native_chat_completions_raises_on_service_tier_the_model_does_not_sell(local_cost_map, monkeypatch): +def test_native_chat_completions_raises_on_service_tier_the_model_does_not_sell( + local_cost_map: object, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonBedrockRuntimeChatCompletionsConfig() + config: Final = AmazonBedrockRuntimeChatCompletionsConfig() with pytest.raises(litellm.utils.UnsupportedParamsError, match="service_tier='priority'"): config.map_openai_params( @@ -1736,11 +1741,13 @@ def test_native_chat_completions_raises_on_service_tier_the_model_does_not_sell( ) -def test_native_chat_completions_keeps_default_service_tier(local_cost_map, monkeypatch): +def test_native_chat_completions_keeps_default_service_tier( + local_cost_map: object, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonBedrockRuntimeChatCompletionsConfig() + config: Final = AmazonBedrockRuntimeChatCompletionsConfig() - result = config.map_openai_params( + result: Final = config.map_openai_params( non_default_params={"service_tier": "default"}, optional_params={}, model="us.openai.gpt-5.6-terra", diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index c8cf0a0b304..ff8ef15f054 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -23,9 +23,9 @@ from litellm.llms.anthropic.chat.transformation import ( NATIVE_OUTPUT_MAX_OPTIONAL_PARAMETERS, NATIVE_OUTPUT_MAX_UNION_PARAMETERS, ) -from litellm.llms.bedrock.common_utils import BEDROCK_NATIVE_OUTPUT_MAX_PROPERTIES from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig +from litellm.llms.bedrock.common_utils import BEDROCK_NATIVE_OUTPUT_MAX_PROPERTIES from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.types.llms.bedrock import ContentBlock, ConverseTokenUsageBlock, RequestObject from litellm.types.llms.openai import AllMessageValues @@ -5348,7 +5348,6 @@ def test_recursive_output_config_format_follows_the_profile_native_key( assert ("outputConfig" in request) is forwarded - def test_sonnet_4_6_json_schema_still_uses_native_output_config(monkeypatch): monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) @@ -10665,7 +10664,10 @@ def test_uses_opt_in_native_output_reads_the_base_model_of_an_application_infere _CALLER_OUTPUT_CONFIG_WITH_A_SCHEMA_OBJECT: Final = { - "textFormat": {"type": "json_schema", "structure": {"jsonSchema": {"name": "capital", "schema": {"type": "object"}}}} + "textFormat": { + "type": "json_schema", + "structure": {"jsonSchema": {"name": "capital", "schema": {"type": "object"}}}, + } } _BEDROCK_SCHEMA_TYPE_ERROR: Final = "outputConfig.textFormat.structure.jsonSchema.schema must be a string" @@ -10791,12 +10793,13 @@ def test_a_deployment_with_a_batch_control_plane_endpoint_keeps_it_out_of_chat_r assert "aws_bedrock_control_plane_endpoint" not in route.calls[0].request.content.decode() -def test_converse_service_tier_dropped_on_models_without_that_tier_under_drop_params(): - """Bedrock sells priority/flex per model. Claude has neither, so with drop_params the tier must - be dropped instead of forwarded to AWS's `The provided service tier is not supported` 400.""" - config = AmazonConverseConfig() +# Bedrock tier support is per model. Checked live on 2026-10-27 (LIT-9793): Claude Sonnet 4.5 and +# Haiku 4.5 answer priority and flex with `The provided service tier is not supported for this model.`, +# Nova 2 Lite serves both. The price map mirrors that, Claude rows have null tier prices, Nova 2 has real ones. +def test_converse_service_tier_dropped_on_models_without_that_tier_under_drop_params() -> None: + config: Final = AmazonConverseConfig() - result = config.map_openai_params( + result: Final = config.map_openai_params( non_default_params={"service_tier": "priority"}, optional_params={}, model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", @@ -10806,9 +10809,11 @@ def test_converse_service_tier_dropped_on_models_without_that_tier_under_drop_pa assert "serviceTier" not in result -def test_converse_service_tier_raises_on_models_without_that_tier_without_drop_params(monkeypatch): +def test_converse_service_tier_raises_on_models_without_that_tier_without_drop_params( + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonConverseConfig() + config: Final = AmazonConverseConfig() with pytest.raises(litellm.utils.UnsupportedParamsError, match="service_tier='flex'"): config.map_openai_params( @@ -10820,11 +10825,13 @@ def test_converse_service_tier_raises_on_models_without_that_tier_without_drop_p @pytest.mark.parametrize("service_tier", ["priority", "flex"]) -def test_converse_service_tier_forwarded_on_models_that_sell_it(monkeypatch, service_tier): +def test_converse_service_tier_forwarded_on_models_that_sell_it( + monkeypatch: pytest.MonkeyPatch, service_tier: str +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonConverseConfig() + config: Final = AmazonConverseConfig() - result = config.map_openai_params( + result: Final = config.map_openai_params( non_default_params={"service_tier": service_tier}, optional_params={}, model="us.amazon.nova-2-lite-v1:0", @@ -10835,11 +10842,13 @@ def test_converse_service_tier_forwarded_on_models_that_sell_it(monkeypatch, ser @pytest.mark.parametrize("service_tier", ["default", "auto"]) -def test_converse_default_and_auto_service_tier_always_forwarded(monkeypatch, service_tier): +def test_converse_default_and_auto_service_tier_always_forwarded( + monkeypatch: pytest.MonkeyPatch, service_tier: str +) -> None: monkeypatch.setattr(litellm, "drop_params", False) - config = AmazonConverseConfig() + config: Final = AmazonConverseConfig() - result = config.map_openai_params( + result: Final = config.map_openai_params( non_default_params={"service_tier": service_tier}, optional_params={}, model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", diff --git a/tests/unit/llms/bedrock/test_bedrock_common_utils.py b/tests/unit/llms/bedrock/test_bedrock_common_utils.py index e02ff1c8d26..bd1ef9cba00 100644 --- a/tests/unit/llms/bedrock/test_bedrock_common_utils.py +++ b/tests/unit/llms/bedrock/test_bedrock_common_utils.py @@ -1,4 +1,3 @@ - import asyncio import importlib from collections.abc import Iterator @@ -55,9 +54,7 @@ def test_bedrock_response_stream_shape_lazy_loads_once(): import litellm.llms.bedrock.common_utils as mod sentinel = MagicMock() - with patch.object( - mod, "_load_bedrock_response_stream_shape", return_value=sentinel - ) as mock_load: + with patch.object(mod, "_load_bedrock_response_stream_shape", return_value=sentinel) as mock_load: assert mod.get_bedrock_response_stream_shape() is sentinel assert mod.get_bedrock_response_stream_shape() is sentinel mock_load.assert_called_once() @@ -104,9 +101,7 @@ def test_bedrock_response_stream_shape_is_structure_shape(): from litellm.llms.bedrock.common_utils import get_bedrock_response_stream_shape loaded_shape = get_bedrock_response_stream_shape() - assert ( - loaded_shape is not None - ), "get_bedrock_response_stream_shape() is None — botocore may not be installed" + assert loaded_shape is not None, "get_bedrock_response_stream_shape() is None — botocore may not be installed" shape: StructureShape = loaded_shape assert isinstance(shape, StructureShape) assert shape.name == "ResponseStream" @@ -171,9 +166,7 @@ def test_deepseek_cris(): Test that DeepSeek models with cross-region inference prefix use converse route """ bedrock_model_info = BedrockModelInfo - bedrock_route = bedrock_model_info.get_bedrock_route( - model="bedrock/us.deepseek.r1-v1:0" - ) + bedrock_route = bedrock_model_info.get_bedrock_route(model="bedrock/us.deepseek.r1-v1:0") assert bedrock_route == "converse" @@ -246,27 +239,19 @@ def test_govcloud_cross_region_inference_prefix(): bedrock_model_info = BedrockModelInfo # Test us-gov prefix is stripped correctly for Claude models - base_model = bedrock_model_info.get_base_model( - model="bedrock/us-gov.anthropic.claude-haiku-4-5-20251001-v1:0" - ) + base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-haiku-4-5-20251001-v1:0") assert base_model == "anthropic.claude-haiku-4-5-20251001-v1:0" # Test us-gov prefix is stripped correctly for different Claude versions - base_model = bedrock_model_info.get_base_model( - model="bedrock/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0" - ) + base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0") assert base_model == "anthropic.claude-sonnet-4-5-20250929-v1:0" # Test us-gov prefix is stripped correctly for Haiku models - base_model = bedrock_model_info.get_base_model( - model="bedrock/us-gov.anthropic.claude-3-haiku-20240307-v1:0" - ) + base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-3-haiku-20240307-v1:0") assert base_model == "anthropic.claude-3-haiku-20240307-v1:0" # Test us-gov prefix is stripped correctly for Meta models - base_model = bedrock_model_info.get_base_model( - model="bedrock/us-gov.meta.llama3-8b-instruct-v1:0" - ) + base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.meta.llama3-8b-instruct-v1:0") assert base_model == "meta.llama3-8b-instruct-v1:0" @@ -280,23 +265,14 @@ def test_context_window_suffix_stripped_for_cost_lookup(): """ from litellm.llms.bedrock.common_utils import get_bedrock_base_model - assert ( - get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1[1m]") - == "anthropic.claude-opus-4-6-v1" - ) - assert ( - get_bedrock_base_model("us.anthropic.claude-sonnet-4-6[1m]") - == "anthropic.claude-sonnet-4-6" - ) + assert get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1[1m]") == "anthropic.claude-opus-4-6-v1" + assert get_bedrock_base_model("us.anthropic.claude-sonnet-4-6[1m]") == "anthropic.claude-sonnet-4-6" assert ( get_bedrock_base_model("global.anthropic.claude-opus-4-5-20251101-v1:0[1m]") == "anthropic.claude-opus-4-5-20251101-v1:0" ) # Ensure models without suffix are unaffected - assert ( - get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1") - == "anthropic.claude-opus-4-6-v1" - ) + assert get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1") == "anthropic.claude-opus-4-6-v1" # Ensure :51k throughput suffix still works assert ( get_bedrock_base_model("anthropic.claude-3-5-sonnet-20241022-v2:0:51k") @@ -346,9 +322,7 @@ def test_output_config_effort_normalization_uses_model_info_ceiling(monkeypatch) ("us.anthropic.claude-opus-4-7", "xhigh"), ], ) -def test_bundled_bedrock_opus_model_info_declares_output_config_effort_ceiling( - model, expected_ceiling -): +def test_bundled_bedrock_opus_model_info_declares_output_config_effort_ceiling(model, expected_ceiling): from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap model_info = GetModelCostMap.load_local_model_cost_map()[model] @@ -367,54 +341,24 @@ def test_route_prefix_matched_as_path_segment_not_substring(): or a ``/`` boundary. """ # The bedrock_mantle/ provider prefix must NOT be read as the mantle/ route. - assert ( - BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.5") != "mantle" - ) - assert ( - BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.4") == "converse" - ) - assert ( - BedrockModelInfo._explicit_mantle_route("bedrock_mantle/openai.gpt-5.5") - is False - ) + assert BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.5") != "mantle" + assert BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.4") == "converse" + assert BedrockModelInfo._explicit_mantle_route("bedrock_mantle/openai.gpt-5.5") is False # A genuine mantle route still resolves, via the startswith branch... - assert ( - BedrockModelInfo.get_bedrock_route("mantle/anthropic.claude-mythos-preview") - == "mantle" - ) + assert BedrockModelInfo.get_bedrock_route("mantle/anthropic.claude-mythos-preview") == "mantle" # ...and via the mid-path "/mantle/" branch (after the bedrock/ provider prefix). - assert ( - BedrockModelInfo.get_bedrock_route( - "bedrock/mantle/anthropic.claude-mythos-preview" - ) - == "mantle" - ) + assert BedrockModelInfo.get_bedrock_route("bedrock/mantle/anthropic.claude-mythos-preview") == "mantle" def test_model_has_route_prefix_exercises_both_branches(): """``_model_has_route_prefix`` matches on ``startswith`` or a ``/`` boundary only.""" # startswith branch - assert ( - BedrockModelInfo._model_has_route_prefix( - "mantle/anthropic.claude-mythos-preview", "mantle/" - ) - is True - ) + assert BedrockModelInfo._model_has_route_prefix("mantle/anthropic.claude-mythos-preview", "mantle/") is True # f"/{prefix}" boundary branch - assert ( - BedrockModelInfo._model_has_route_prefix( - "bedrock/mantle/anthropic.claude-mythos-preview", "mantle/" - ) - is True - ) + assert BedrockModelInfo._model_has_route_prefix("bedrock/mantle/anthropic.claude-mythos-preview", "mantle/") is True # neither branch: the token only appears glued to another segment - assert ( - BedrockModelInfo._model_has_route_prefix( - "bedrock_mantle/openai.gpt-5.5", "mantle/" - ) - is False - ) + assert BedrockModelInfo._model_has_route_prefix("bedrock_mantle/openai.gpt-5.5", "mantle/") is False @pytest.mark.parametrize( @@ -464,16 +408,10 @@ def test_explicit_invoke_route_does_not_match_async_invoke(): """ async_invoke_model = "async_invoke/twelvelabs.marengo-embed-2-7-v1:0" assert BedrockModelInfo._explicit_invoke_route(async_invoke_model) is False - assert ( - BedrockModelInfo._explicit_invoke_route(f"bedrock/{async_invoke_model}") - is False - ) + assert BedrockModelInfo._explicit_invoke_route(f"bedrock/{async_invoke_model}") is False # ...while async_invoke/ is still detected as its own route. assert BedrockModelInfo._explicit_async_invoke_route(async_invoke_model) is True - assert ( - BedrockModelInfo._explicit_async_invoke_route(f"bedrock/{async_invoke_model}") - is True - ) + assert BedrockModelInfo._explicit_async_invoke_route(f"bedrock/{async_invoke_model}") is True def test_capability_lookups_fall_back_to_base_model_when_regional_entry_lacks_field(monkeypatch): @@ -1219,6 +1157,7 @@ def _vcr_outcome_gate(request, vcr): yield record_vcr_outcome(request, vcr) + @pytest.fixture(scope="session") def event_loop(): try: @@ -1228,6 +1167,7 @@ def event_loop(): yield loop loop.close() + @pytest.fixture(scope="function") def setup_and_teardown(event_loop): import litellm @@ -1261,6 +1201,7 @@ def setup_and_teardown(event_loop): if pending: event_loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + _SCALAR_DEFAULTS = { "num_retries": getattr(litellm, "num_retries", None), "set_verbose": getattr(litellm, "set_verbose", False), @@ -1275,6 +1216,7 @@ _SCALAR_DEFAULTS = { "cohere_key": getattr(litellm, "cohere_key", None), } + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestStripBedrockRoutingPrefix: """Tests for strip_bedrock_routing_prefix function.""" @@ -1305,6 +1247,7 @@ class TestStripBedrockRoutingPrefix: == "anthropic.claude-3-sonnet-20240229-v1:0" ) + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestStripBedrockThroughputSuffix: """Tests for strip_bedrock_throughput_suffix function.""" @@ -1332,6 +1275,7 @@ class TestStripBedrockThroughputSuffix: def test_strip_throughput_suffix(self, input_model, expected): assert strip_bedrock_throughput_suffix(input_model) == expected + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestExtractModelNameFromBedrockArn: """Tests for extract_model_name_from_bedrock_arn function.""" @@ -1352,6 +1296,7 @@ class TestExtractModelNameFromBedrockArn: arn = "ARN:aws:bedrock:us-east-1:123456789012:model/my-model" assert extract_model_name_from_bedrock_arn(arn) == "my-model" + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestGetBedrockCrossRegionInferenceRegions: """Tests for get_bedrock_cross_region_inference_regions function.""" @@ -1367,6 +1312,7 @@ class TestGetBedrockCrossRegionInferenceRegions: regions = get_bedrock_cross_region_inference_regions() assert isinstance(regions, list) + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestGetBedrockBaseModel: """Tests for get_bedrock_base_model function.""" @@ -1430,6 +1376,7 @@ class TestGetBedrockBaseModel: """Test that throughput tier suffixes like :51k are stripped. Issue #19113.""" assert get_bedrock_base_model(input_model) == expected + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestBedrockModelInfoWrappers: """Tests that BedrockModelInfo methods correctly wrap standalone functions.""" @@ -1451,6 +1398,7 @@ class TestBedrockModelInfoWrappers: model = "bedrock/converse/claude-3" assert BedrockModelInfo.get_non_litellm_routing_model_name(model) == strip_bedrock_routing_prefix(model) + @pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown") class TestBedrockTokenCounter: """Tests for BedrockTokenCounter class.""" @@ -1487,12 +1435,14 @@ class TestBedrockTokenCounter: ) assert result is None + @pytest.fixture def _pr4_bedrock_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("AWS_ACCESS_KEY_ID", "pr4-test-aws-access-key") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "pr4-test-aws-secret-key") monkeypatch.setenv("AWS_REGION_NAME", "us-east-1") + @pytest.fixture(scope="module") def _use_local_model_cost_map() -> Iterator[None]: with pytest.MonkeyPatch.context() as monkeypatch: @@ -1501,6 +1451,7 @@ def _use_local_model_cost_map() -> Iterator[None]: importlib.reload(litellm) yield + @pytest.mark.usefixtures( "_pr4_bedrock_env", "_use_local_model_cost_map", @@ -2078,8 +2029,12 @@ def test_bedrock_capability_model_reads_base_model_only_for_application_profile_ assert bedrock_capability_model(model, base_model) == expected -_FORMAT_REJECTION: Final = "The model returned the following errors: output_config.format: Extra inputs are not permitted" -_COMPILE_REJECTION: Final = "The model returned the following errors: compilation exceeded limits: Schema is too complex." +_FORMAT_REJECTION: Final = ( + "The model returned the following errors: output_config.format: Extra inputs are not permitted" +) +_COMPILE_REJECTION: Final = ( + "The model returned the following errors: compilation exceeded limits: Schema is too complex." +) _SCHEMA_PATTERN_REJECTION: Final = ( "The model returned the following errors: output_config.format.schema: Unsupported regex feature in pattern " "field: pattern is too complex for structured output: reduce the {n,m} upper bound, narrow the character class " @@ -2180,7 +2135,10 @@ def test_bedrock_strict_tools_fallback_drops_only_strict_and_keeps_everything_el assert fallback == NativeOutputFallback( optional_params={ "tools": [ - {**strict_tool, "function": {"name": "lookup", "description": "Look up", "parameters": {"type": "object"}}}, + { + **strict_tool, + "function": {"name": "lookup", "description": "Look up", "parameters": {"type": "object"}}, + }, plain_tool, anthropic_tool, {"toolSpec": {"name": "spec", "inputSchema": {"json": {"type": "object"}}}}, @@ -2237,36 +2195,40 @@ def test_bedrock_strict_tools_fallback_retries_only_when_the_request_sent_strict assert (fallback is not None) == sent_strict +_TIER_PRICED_ROW: Final = { + "litellm_provider": "bedrock_converse", + "input_cost_per_token": 1e-6, + "input_cost_per_token_priority": 2e-6, + "input_cost_per_token_flex": 5e-7, +} +_STANDARD_ONLY_ROW: Final = { + "litellm_provider": "bedrock_converse", + "input_cost_per_token": 1e-6, + "input_cost_per_token_priority": None, + "input_cost_per_token_flex": None, +} + + @pytest.mark.parametrize( ("model", "service_tier", "expected"), [ - ("us.anthropic.claude-sonnet-4-5-20250929-v1:0", "priority", False), - ("us.anthropic.claude-sonnet-4-5-20250929-v1:0", "default", True), - ("us.anthropic.claude-sonnet-4-5-20250929-v1:0", "auto", True), - ("us.amazon.nova-2-lite-v1:0", "flex", True), - ("bedrock/us-west-2/moonshotai.kimi-k2.5", "flex", True), + ("tiered.model-v1:0", "priority", True), + ("tiered.model-v1:0", "flex", True), + ("bedrock/us-west-2/tiered.model-v1:0", "flex", True), + ("standard.model-v1:0", "priority", False), + ("standard.model-v1:0", "flex", False), + ("standard.model-v1:0", "default", True), + ("standard.model-v1:0", "auto", True), ("a-model-the-price-map-has-never-heard-of", "priority", True), ], ) -def test_bedrock_serves_service_tier_follows_price_map_tier_rows(model, service_tier, expected): - """A Bedrock model serves a tier exactly when its price-map row carries that tier's pricing. - Unknown models have no signal and keep being forwarded.""" +def test_bedrock_serves_service_tier_follows_price_map_tier_rows( + monkeypatch: pytest.MonkeyPatch, model: str, service_tier: str, expected: bool +) -> None: + """A Bedrock model serves a tier exactly when its price-map row carries a non-null price for that tier. + The proxy expands every row to the full ModelInfo shape, so a null price (standard-only row) is no signal, + `default` and `auto` are always served, and an unknown model keeps being forwarded.""" + monkeypatch.setitem(litellm.model_cost, "tiered.model-v1:0", dict(_TIER_PRICED_ROW)) + monkeypatch.setitem(litellm.model_cost, "standard.model-v1:0", dict(_STANDARD_ONLY_ROW)) + assert bedrock_serves_service_tier(model, service_tier) is expected - - -def test_bedrock_serves_service_tier_ignores_null_tier_prices_from_normalized_rows(monkeypatch): - """The proxy expands every price-map row to the full ModelInfo shape, so a Claude row carries - `input_cost_per_token_priority: None`. A null price is no signal that AWS sells the tier.""" - monkeypatch.setitem( - litellm.model_cost, - "normalized.claude-row", - {"litellm_provider": "bedrock_converse", "input_cost_per_token": 1e-6, "input_cost_per_token_priority": None}, - ) - monkeypatch.setitem( - litellm.model_cost, - "normalized.nova-row", - {"litellm_provider": "bedrock_converse", "input_cost_per_token": 1e-6, "input_cost_per_token_priority": 2e-6}, - ) - - assert bedrock_serves_service_tier("normalized.claude-row", "priority") is False - assert bedrock_serves_service_tier("normalized.nova-row", "priority") is True