fix(bedrock): fill default maxTokens for anthropic converse models from the cost map

bedrock applies a 4096 output token default when inferenceConfig.maxTokens is omitted, which silently truncates anthropic models whose documented maximum is far higher

fill maxTokens from the litellm cost map when the request has no max_tokens, the thinking branch did not set one, and the model is an anthropic id, unknown models stay unchanged

fixes #41529
This commit is contained in:
Abdel 2026-09-17 01:39:44 +00:00 • committed by Abdel
parent 7a659973e3
commit 8f9ed91db6
2 changed files with 103 additions and 10 deletions

View file

@ -1186,6 +1186,16 @@ class AmazonConverseConfig(BaseConfig):
non_default_params=non_default_params, optional_params=optional_params
)
# Bedrock applies a 4096 output-token default when maxTokens is omitted, silently truncating anthropic models
if (
not self.is_max_tokens_in_request(non_default_params) # pyright: ignore[reportUnknownMemberType] # is_max_tokens_in_request accepts an untyped params dict
and "maxTokens" not in optional_params
and "anthropic" in model
):
default_max_tokens: Final = self._get_default_max_tokens_for_model(model)
if default_max_tokens is not None:
optional_params["maxTokens"] = default_max_tokens # rebind-ok: out-param store like siblings
final_is_thinking_enabled: Final = self.is_thinking_enabled(optional_params)
if final_is_thinking_enabled and "tool_choice" in optional_params:
tool_choice_block: Final = optional_params["tool_choice"]
@ -1328,6 +1338,33 @@ class AmazonConverseConfig(BaseConfig):
if thinking_token_budget is not None:
optional_params["maxTokens"] = thinking_token_budget + DEFAULT_MAX_TOKENS
@staticmethod
def _get_default_max_tokens_for_model(model: str) -> int | None:
"""
Resolve the model's documented max output tokens from the litellm cost map.
Returns None when the model is not mapped, so the request stays unchanged.
Bedrock model ids carry an optional cross-region prefix that cost map keys do
not include, so strip it and retry.
"""
cost_map: Final = cast("Mapping[str, Mapping[str, object]]", litellm.model_cost) # cast-ok: untyped cost map
def lookup(candidate: str) -> int | None:
entry: Final = cost_map.get(candidate)
if entry is None:
return None
max_output_tokens: Final = entry.get("max_output_tokens")
if isinstance(max_output_tokens, int):
return max_output_tokens
max_tokens: Final = entry.get("max_tokens")
return max_tokens if isinstance(max_tokens, int) else None
first_segment, separator, rest = model.partition(".")
region_prefixed: Final = separator == "." and first_segment in ("us", "eu", "apac", "global", "ca", "sa")
candidates: Final = (model, rest) if region_prefixed else (model,)
return next((value for value in map(lookup, candidates) if value is not None), None)
@overload
def get_cache_point_block(
self,

View file

@ -8178,16 +8178,6 @@ def test_supports_sampling_params_prefixed_and_anthropic_fallback(monkeypatch: p
assert AmazonConverseConfig._supports_sampling_params("anthropic.claude-custom-unregistered") is True
@pytest.mark.usefixtures("fake_provider_credentials")
def test_bedrock_tools_pt_valid_names():
"""
@ -9482,3 +9472,69 @@ def test_bedrock_nova_web_search_options_ignored_for_non_nova():
system_tool2 = result2.get("systemTool")
assert system_tool2 is not None
assert system_tool2["name"] == "nova_grounding"
def test_anthropic_converse_default_max_tokens_from_cost_map(local_model_cost_map):
"""Bedrock caps output at 4096 when maxTokens is omitted; fill the model max from the cost map instead."""
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
model="us.anthropic.claude-sonnet-5",
non_default_params={},
optional_params={},
drop_params=False,
)
assert optional_params["maxTokens"] == 128000
data = config._transform_request_helper(
model="us.anthropic.claude-sonnet-5",
system_content_blocks=[],
optional_params=optional_params,
messages=None,
)
assert data["inferenceConfig"]["maxTokens"] == 128000
def test_anthropic_converse_explicit_max_tokens_still_wins(local_model_cost_map):
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
model="us.anthropic.claude-sonnet-5",
non_default_params={"max_tokens": 111},
optional_params={},
drop_params=False,
)
assert optional_params["maxTokens"] == 111
def test_anthropic_converse_default_max_tokens_strips_region_prefix(local_model_cost_map):
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
model="eu.anthropic.claude-3-7-sonnet-20240620-v1:0",
non_default_params={},
optional_params={},
drop_params=False,
)
assert optional_params["maxTokens"] == 8192
def test_non_anthropic_converse_gets_no_default_max_tokens(local_model_cost_map):
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
model="us.meta.llama4-maverick-17b-instruct-v1:0",
non_default_params={},
optional_params={},
drop_params=False,
)
assert "maxTokens" not in optional_params
def test_anthropic_converse_unknown_model_gets_no_default_max_tokens(local_model_cost_map):
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
model="us.anthropic.not-a-real-model-v1:0",
non_default_params={},
optional_params={},
drop_params=False,
)
assert "maxTokens" not in optional_params