fix(bedrock_mantle): bill Mantle's un-versioned Claude ids from a Mantle cost row

Mantle serves anthropic.claude-haiku-4-5 without the dated -20251001-v1:0
suffix the Bedrock row carries, so the native route billed it at 0. Add a
bedrock_mantle/anthropic.claude-haiku-4-5 row and let a
bedrock_mantle/<region>/<model> name fall back to the region-free
bedrock_mantle/<model> row before the provider-prefixed lookup. Also
satisfy the mutable-collection gate in the native messages transformation.
This commit is contained in:
mateo-berri 2026-09-21 12:00:38 -07:00
parent 4eed951e6f
commit 2fe5c8990e
6 changed files with 112 additions and 4 deletions

View file

@ -33,7 +33,7 @@ _MANTLE_REQUEST: Final = TypeAdapter(dict[str, object])
def build_mantle_native_messages_url(api_base: str | None, litellm_params: Mapping[str, object]) -> str:
region: Final = resolve_mantle_region({**litellm_params, "api_base": api_base})
region: Final = resolve_mantle_region(MappingProxyType({**litellm_params, "api_base": api_base}))
configured: Final = (
api_base or get_secret_str("BEDROCK_MANTLE_API_BASE") or f"https://bedrock-mantle.{region}.api.aws"
).rstrip("/")
@ -96,7 +96,10 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM
)
if any(name.lower() == "anthropic-version" for name in merged_headers):
return merged_headers, resolved_api_base
return {**merged_headers, "anthropic-version": DEFAULT_ANTHROPIC_API_VERSION}, resolved_api_base
return { # mutable-ok: the base class contract returns a dict the handler signs into in place
**merged_headers,
"anthropic-version": DEFAULT_ANTHROPIC_API_VERSION,
}, resolved_api_base
def transform_anthropic_messages_request(
self,
@ -119,4 +122,6 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM
if betas is not None:
header_betas: Final = ",".join(_ANTHROPIC_BETAS.validate_python(betas))
headers["anthropic-beta"] = header_betas # rebind-ok: the handler signs and sends this same dict
return {key: value for key, value in request.items() if key not in _BODY_FIELDS_MANTLE_READS_FROM_HEADERS}
return { # mutable-ok: the base class contract returns the dict the handler serializes as the body
key: value for key, value in request.items() if key not in _BODY_FIELDS_MANTLE_READS_FROM_HEADERS
}

View file

@ -59210,6 +59210,34 @@
"supports_tool_choice": true,
"supports_vision": true
},
"bedrock_mantle/anthropic.claude-haiku-4-5": {
"cache_creation_input_token_cost": 1.25e-06,
"cache_creation_input_token_cost_above_1hr": 2e-06,
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 1e-06,
"litellm_provider": "bedrock_mantle",
"supports_tool_search": true,
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"output_cost_per_token": 5e-06,
"source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock",
"supports_assistant_prefill": true,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_native_structured_output": true,
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 4096,
"input_cost_per_token_batches": 5e-07,
"output_cost_per_token_batches": 2.5e-06
},
"us.xai.grok-4.6": {
"input_cost_per_token": 2.2e-06,
"output_cost_per_token": 6.6e-06,

View file

@ -5665,6 +5665,11 @@ def _get_potential_model_names(model: str, custom_llm_provider: str | None) -> P
region_free_split_model: Final = (
_strip_mantle_region_prefix(split_model) if custom_llm_provider == "bedrock_mantle" else split_model
)
region_free_combined_stripped_model_name: Final = (
f"bedrock_mantle/{_strip_model_name(model=region_free_split_model, custom_llm_provider=custom_llm_provider)}"
if custom_llm_provider == "bedrock_mantle"
else combined_stripped_model_name
)
provider_model_info: Final = (
ProviderConfigManager.get_provider_model_info(
model=region_free_split_model, provider=LlmProviders(custom_llm_provider)
@ -5680,7 +5685,7 @@ def _get_potential_model_names(model: str, custom_llm_provider: str | None) -> P
split_model=region_free_split_model,
combined_model_name=combined_model_name,
stripped_model_name=stripped_model_name,
combined_stripped_model_name=combined_stripped_model_name,
combined_stripped_model_name=region_free_combined_stripped_model_name,
provider_prefixed_model_name=provider_cost_key or provider_prefixed_model_name,
custom_llm_provider=cast(str, custom_llm_provider),
)

View file

@ -59210,6 +59210,34 @@
"supports_tool_choice": true,
"supports_vision": true
},
"bedrock_mantle/anthropic.claude-haiku-4-5": {
"cache_creation_input_token_cost": 1.25e-06,
"cache_creation_input_token_cost_above_1hr": 2e-06,
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 1e-06,
"litellm_provider": "bedrock_mantle",
"supports_tool_search": true,
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"output_cost_per_token": 5e-06,
"source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock",
"supports_assistant_prefill": true,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_native_structured_output": true,
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 4096,
"input_cost_per_token_batches": 5e-07,
"output_cost_per_token_batches": 2.5e-06
},
"us.xai.grok-4.6": {
"input_cost_per_token": 2.2e-06,
"output_cost_per_token": 6.6e-06,

View file

@ -3547,6 +3547,33 @@ def test_completion_cost_mantle_native_messages_prices_claude_from_the_bedrock_r
) == pytest.approx(expected)
def test_completion_cost_mantle_native_messages_prices_haiku_from_the_mantle_row(_local_model_cost_map):
"""Mantle serves Anthropic's un-versioned haiku id, which has no bare Bedrock row (Bedrock's carries
the -20251001-v1:0 suffix), and Claude Code sends every small-fast-model call to it. Both the plain
and the region-prefixed deployment names must price from bedrock_mantle/anthropic.claude-haiku-4-5
instead of billing $0."""
response = litellm.ModelResponse(
id="msg_x",
choices=[{"index": 0, "message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}],
model="claude-haiku-4-5",
usage={"prompt_tokens": 100, "completion_tokens": 10, "total_tokens": 110},
)
row = litellm.model_cost["bedrock_mantle/anthropic.claude-haiku-4-5"]
expected = 100 * row["input_cost_per_token"] + 10 * row["output_cost_per_token"]
assert expected > 0
for model in (
"bedrock_mantle/anthropic.claude-haiku-4-5",
"bedrock_mantle/us-east-2/anthropic.claude-haiku-4-5",
):
assert litellm.completion_cost(
completion_response=response,
model=model,
custom_llm_provider="bedrock_mantle",
) == pytest.approx(expected), model
def test_select_model_name_keeps_base_model_free_of_region(_local_model_cost_map):
"""An explicit base_model keeps pricing on that model's own key even when the request carries a
region with different regional rates, so the private provider model never widens region pricing."""

View file

@ -1163,6 +1163,21 @@ def test_get_model_info_bedrock_regional_inference_profile_pricing(local_model_c
assert control["key"] == "au.anthropic.claude-opus-4-8"
def test_get_model_info_bedrock_mantle_region_prefix_falls_back_to_the_mantle_row(local_model_cost_map):
"""A Mantle deployment name may carry the region as a prefix (bedrock_mantle/us-east-2/<model>).
That name has no cost row of its own, so pricing must fall through to the region-free
bedrock_mantle/<model> row instead of raising, while a region that has its own row keeps it."""
for model, expected_key in (
("bedrock_mantle/us-east-2/anthropic.claude-haiku-4-5", "bedrock_mantle/anthropic.claude-haiku-4-5"),
("bedrock_mantle/us-east-2/openai.gpt-5.6-sol", "bedrock_mantle/openai.gpt-5.6-sol"),
("bedrock_mantle/us-gov-west-1/openai.gpt-5.4", "bedrock_mantle/us-gov-west-1/openai.gpt-5.4"),
):
info = litellm.get_model_info(model=model, custom_llm_provider="bedrock_mantle")
assert info["key"] == expected_key, model
assert info["input_cost_per_token"] == litellm.model_cost[expected_key]["input_cost_per_token"], model
assert info["input_cost_per_token"] > 0, model
def test_openai_models_in_model_info(monkeypatch):
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
litellm.model_cost = litellm.get_model_cost_map(url="")