test(bedrock): use controlled price rows and typed params in service_tier tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-10-28 18:03:42 +00:00
parent 92aa53ccbb
commit 8484372323
3 changed files with 118 additions and 140 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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