mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
4eed951e6f
commit
2fe5c8990e
6 changed files with 112 additions and 4 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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="")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue