mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(bedrock): keep params AWS refuses natively off the chat completions route
Drop the params each family 400s or 503s on runtime Chat Completions from the native config's supported list (GPT-5.6 penalties, stop, and logprobs, Grok penalties, gpt-oss logit_bias) so drop_params drops them as Converse did, gate legacy functions on GPT-5.6 the same way as tools, and send an Anthropic-style thinking block to Converse, the only route that forwards it
This commit is contained in:
parent
b90c2f113d
commit
22b217bd95
3 changed files with 138 additions and 8 deletions
|
|
@ -38,6 +38,27 @@ if TYPE_CHECKING:
|
|||
REASONING_OPEN_TAG: Final = "<reasoning>"
|
||||
REASONING_CLOSE_TAG: Final = "</reasoning>"
|
||||
|
||||
CHAT_COMPLETIONS_REFUSED_PARAMS_BY_FAMILY: Final = MappingProxyType(
|
||||
{
|
||||
"openai.gpt-5": frozenset(("frequency_penalty", "presence_penalty", "stop", "logprobs", "top_logprobs")),
|
||||
"openai.gpt-oss": frozenset(("logit_bias",)),
|
||||
"xai.": frozenset(("frequency_penalty", "presence_penalty")),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def chat_completions_params_refused_for(model: str) -> frozenset[str]:
|
||||
"""The OpenAI params AWS's Chat Completions endpoint rejects for this model whatever else the request says.
|
||||
|
||||
Each family answers them with a 400 (GPT-5.6, gpt-oss) or a 503 (Grok), where Converse dropped the same
|
||||
params under ``drop_params``, so the native config leaves them out of its supported list and the usual
|
||||
drop-or-raise handling applies before the request reaches AWS.
|
||||
"""
|
||||
model_id: Final = split_bedrock_region_path(model)[1]
|
||||
return frozenset().union(
|
||||
*(refused for family, refused in CHAT_COMPLETIONS_REFUSED_PARAMS_BY_FAMILY.items() if family in model_id)
|
||||
)
|
||||
|
||||
|
||||
def _held_close_tag_prefix(text: str) -> int:
|
||||
return next(
|
||||
|
|
@ -362,7 +383,8 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig):
|
|||
return {**validated, "OpenAI-Project": project_id} # mutable-ok: BaseConfig signature returns a dict
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list: # mutable-ok: BaseConfig signature
|
||||
base_params: Final = [param for param in super().get_supported_openai_params(model) if param != "n"]
|
||||
refused: Final = {"n", *chat_completions_params_refused_for(model)}
|
||||
base_params: Final = [param for param in super().get_supported_openai_params(model) if param not in refused]
|
||||
if "reasoning_effort" in base_params or not litellm.supports_reasoning(
|
||||
model=model, custom_llm_provider=self.custom_llm_provider
|
||||
):
|
||||
|
|
|
|||
|
|
@ -849,7 +849,7 @@ def bedrock_runtime_chat_completions_enforces_response_format(model: str) -> boo
|
|||
|
||||
|
||||
BEDROCK_CONVERSE_ONLY_REQUEST_KEYS: Final = frozenset(
|
||||
("guardrailConfig", "performanceConfig", "serviceTier", "requestMetadata", "outputConfig")
|
||||
("guardrailConfig", "performanceConfig", "serviceTier", "requestMetadata", "outputConfig", "thinking")
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -862,12 +862,13 @@ def _response_format_constrains_output(response_format: object) -> bool:
|
|||
def bedrock_request_needs_converse(model: str, request_params: Mapping[str, object]) -> bool:
|
||||
"""Whether a request on a runtime-Chat-Completions model must still be served by Converse.
|
||||
|
||||
Converse-shaped body keys (``BEDROCK_CONVERSE_ONLY_REQUEST_KEYS``) are rejected as malformed input by
|
||||
Converse-shaped body keys (``BEDROCK_CONVERSE_ONLY_REQUEST_KEYS``, the Anthropic-style ``thinking``
|
||||
block included, which only Converse forwards as ``additionalModelRequestFields``) have no field on
|
||||
AWS's native OpenAI surface, operator-owned request metadata is only written onto the Converse body,
|
||||
function tools on a model without ``supports_bedrock_runtime_chat_completions_tools_with_reasoning``
|
||||
are rejected there unless ``reasoning_effort`` is exactly ``"none"``, and a constraining
|
||||
``response_format`` on a model without ``supports_bedrock_runtime_chat_completions_response_format``
|
||||
is only honored by Converse.
|
||||
function tools (``tools`` or legacy ``functions``) on a model without
|
||||
``supports_bedrock_runtime_chat_completions_tools_with_reasoning`` are rejected there unless
|
||||
``reasoning_effort`` is exactly ``"none"``, and a constraining ``response_format`` on a model without
|
||||
``supports_bedrock_runtime_chat_completions_response_format`` is only honored by Converse.
|
||||
"""
|
||||
if any(request_params.get(key) is not None for key in BEDROCK_CONVERSE_ONLY_REQUEST_KEYS):
|
||||
return True
|
||||
|
|
@ -877,7 +878,7 @@ def bedrock_request_needs_converse(model: str, request_params: Mapping[str, obje
|
|||
request_params.get("response_format")
|
||||
) and not bedrock_runtime_chat_completions_enforces_response_format(model):
|
||||
return True
|
||||
if not request_params.get("tools"):
|
||||
if not (request_params.get("tools") or request_params.get("functions")):
|
||||
return False
|
||||
return (
|
||||
not bedrock_runtime_chat_completions_serves_tools_with_reasoning(model)
|
||||
|
|
|
|||
|
|
@ -264,6 +264,26 @@ def test_gpt_oss_tools_with_any_reasoning_effort_stay_on_chat_completions(local_
|
|||
assert BedrockModelInfo.get_bedrock_route("openai.gpt-oss-120b-1:0", params) == "chat_completions"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"request_params, expected_route",
|
||||
[
|
||||
({"functions": [GET_WEATHER_TOOL["function"]]}, "converse"),
|
||||
({"functions": [GET_WEATHER_TOOL["function"]], "reasoning_effort": "low"}, "converse"),
|
||||
({"functions": [GET_WEATHER_TOOL["function"]], "reasoning_effort": "none"}, "chat_completions"),
|
||||
({"functions": [], "reasoning_effort": "low"}, "chat_completions"),
|
||||
],
|
||||
)
|
||||
def test_gpt56_legacy_functions_route_like_tools(local_cost_map, request_params, expected_route):
|
||||
assert BedrockModelInfo.get_bedrock_route("global.openai.gpt-5.6-sol", request_params) == expected_route
|
||||
assert BedrockModelInfo.get_bedrock_route("openai.gpt-oss-120b-1:0", request_params) == "chat_completions"
|
||||
|
||||
|
||||
def test_thinking_block_goes_to_converse(local_cost_map):
|
||||
thinking = {"type": "enabled", "budget_tokens": 1024}
|
||||
assert BedrockModelInfo.get_bedrock_route("us.xai.grok-4.6", {"thinking": thinking}) == "converse"
|
||||
assert BedrockModelInfo.get_bedrock_route("us.xai.grok-4.6", {"thinking": None}) == "chat_completions"
|
||||
|
||||
|
||||
def test_explicit_converse_prefix_wins_for_openai_models(local_cost_map):
|
||||
assert BedrockModelInfo.get_bedrock_route("bedrock/converse/openai.gpt-oss-20b-1:0") == "converse"
|
||||
assert BedrockModelInfo.get_bedrock_route("converse/global.openai.gpt-5.6-sol", {}) == "converse"
|
||||
|
|
@ -301,6 +321,54 @@ def test_supported_params_include_reasoning_effort_for_gpt56(local_cost_map):
|
|||
assert "reasoning_effort" in cfg.get_supported_openai_params("openai.gpt-oss-20b-1:0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, refused, kept",
|
||||
[
|
||||
(
|
||||
"bedrock/global.openai.gpt-5.6-sol",
|
||||
("frequency_penalty", "presence_penalty", "stop", "logprobs", "top_logprobs", "n"),
|
||||
("temperature", "top_p", "logit_bias", "reasoning_effort", "tools", "functions"),
|
||||
),
|
||||
(
|
||||
"us.xai.grok-4.6",
|
||||
("frequency_penalty", "presence_penalty", "n"),
|
||||
("stop", "logprobs", "top_p", "logit_bias", "reasoning_effort"),
|
||||
),
|
||||
(
|
||||
"bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0",
|
||||
("logit_bias", "n"),
|
||||
("frequency_penalty", "presence_penalty", "stop", "logprobs", "reasoning_effort"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_supported_params_leave_out_what_each_family_refuses(local_cost_map, model, refused, kept):
|
||||
supported = set(AmazonBedrockRuntimeChatCompletionsConfig().get_supported_openai_params(model))
|
||||
assert supported.isdisjoint(refused)
|
||||
assert set(kept) <= supported
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, param",
|
||||
[
|
||||
("bedrock/global.openai.gpt-5.6-sol", {"frequency_penalty": 0.5}),
|
||||
("bedrock/global.openai.gpt-5.6-sol", {"logprobs": True, "top_logprobs": 2}),
|
||||
("bedrock/us.xai.grok-4.6", {"presence_penalty": 0.5}),
|
||||
("bedrock/openai.gpt-oss-20b-1:0", {"logit_bias": {"1": 1}}),
|
||||
],
|
||||
ids=lambda value: value if isinstance(value, str) else next(iter(value)),
|
||||
)
|
||||
def test_refused_params_are_dropped_or_refused_before_reaching_aws(local_cost_map, fake_aws_env, model, param):
|
||||
requests, client = _recording_client(json=_chat_completion_json("ok", model.removeprefix("bedrock/")))
|
||||
with pytest.raises(litellm.UnsupportedParamsError, match=next(iter(param))):
|
||||
litellm.completion(model=model, messages=[{"role": "user", "content": "hello"}], client=client, **param)
|
||||
litellm.completion(
|
||||
model=model, messages=[{"role": "user", "content": "hello"}], drop_params=True, client=client, **param
|
||||
)
|
||||
|
||||
assert str(requests[0].url).endswith("/openai/v1/chat/completions")
|
||||
assert param.keys().isdisjoint(json.loads(requests[0].content))
|
||||
|
||||
|
||||
def test_split_reasoning_tag_splits_leading_tag():
|
||||
assert split_reasoning_tag("<reasoning>plan it\n</reasoning>\n\nHello") == ("plan it\n", "Hello")
|
||||
|
||||
|
|
@ -579,6 +647,45 @@ def test_legacy_functions_stay_on_chat_completions(local_cost_map, fake_aws_env)
|
|||
assert json.loads(requests[0].content)["functions"] == [GET_WEATHER_TOOL["function"]]
|
||||
|
||||
|
||||
def test_gpt56_legacy_functions_with_reasoning_fall_back_to_converse(local_cost_map, fake_aws_env):
|
||||
requests, client = _recording_client(json=CONVERSE_JSON)
|
||||
with pytest.raises(litellm.UnsupportedParamsError, match="functions"):
|
||||
litellm.completion(
|
||||
model="bedrock/global.openai.gpt-5.6-sol",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
functions=[GET_WEATHER_TOOL["function"]],
|
||||
reasoning_effort="low",
|
||||
client=client,
|
||||
)
|
||||
litellm.completion(
|
||||
model="bedrock/global.openai.gpt-5.6-sol",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
functions=[GET_WEATHER_TOOL["function"]],
|
||||
reasoning_effort="low",
|
||||
drop_params=True,
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert requests[0].url.raw_path.endswith(b"/model/global.openai.gpt-5.6-sol/converse")
|
||||
body = json.loads(requests[0].content)
|
||||
assert "functions" not in body
|
||||
assert "toolConfig" not in body
|
||||
|
||||
|
||||
def test_grok_thinking_block_is_served_by_converse(local_cost_map, fake_aws_env):
|
||||
requests, client = _recording_client(json=CONVERSE_JSON)
|
||||
thinking = {"type": "enabled", "budget_tokens": 1024}
|
||||
litellm.completion(
|
||||
model="bedrock/us.xai.grok-4.6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
thinking=thinking,
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert requests[0].url.raw_path.endswith(b"/model/us.xai.grok-4.6/converse")
|
||||
assert json.loads(requests[0].content)["additionalModelRequestFields"]["thinking"] == thinking
|
||||
|
||||
|
||||
def test_converse_fallback_validates_against_converse_params(local_cost_map, fake_aws_env):
|
||||
requests, client = _recording_client(json=CONVERSE_JSON)
|
||||
guardrail = {"guardrailIdentifier": "gr-1", "guardrailVersion": "1"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue