mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
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:
parent
92aa53ccbb
commit
8484372323
3 changed files with 118 additions and 140 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue