From 627d19ebeebde29c6f822150ef68ced173b1747f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:47:57 -0700 Subject: [PATCH] test(bedrock): cover the strict tools 400 retry under drop_params --- tests/unit/test_main.py | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index c3d3575b8b3..10e398cb157 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -9814,3 +9814,93 @@ async def test_converse_fallback_prices_the_current_attempt( else: client.close() configuration.reset_rust_configuration() + + +_STRICT_LOOKUP_TOOL: Final = { + "type": "function", + "function": { + "name": "lookup_record", + "description": "Look up a record", + "strict": True, + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + "additionalProperties": False, + }, + }, +} +# Bedrock's 400 for a request past Claude's strict tool limits, seen live on us.anthropic.claude-sonnet-4-6 on 2026-10-20 +_TOO_MANY_STRICT_TOOLS: Final = ( + "The model returned the following errors: Too many strict tools (21). The maximum number of strict tools " + "supported is 20. Try reducing the number of tools marked as strict." +) + + +async def _converse_with_a_strict_tools_limit_rejection_first( + sent: list[dict[str, object]], asynchronous: bool, stream: bool, drop_params: bool | None +) -> None: + from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler + from tests.unit.llms.bedrock.slow_upstream import CONVERSE_ANSWER + + def respond(request: httpx.Request) -> httpx.Response: + sent.append(json.loads(request.content)) + if len(sent) == 1: + return httpx.Response(400, json={"message": _TOO_MANY_STRICT_TOOLS}) + return httpx.Response(200, content=CONVERSE_ANSWER) + + kwargs: Final = { + "model": "bedrock/us.anthropic.claude-sonnet-4-6", + "messages": [{"role": "user", "content": "Find invoice 42"}], + "tools": [_STRICT_LOOKUP_TOOL], + "aws_access_key_id": "AKIASTRICTTOOLS", + "aws_secret_access_key": "strict-tools-secret", + "aws_region_name": "us-east-1", + "num_retries": 0, + "drop_params": drop_params, + "stream": stream, + "fake_stream": stream, + } + if asynchronous: + response = await litellm.acompletion( + client=AsyncHTTPHandler(transport=httpx.MockTransport(respond)), **kwargs + ) + chunks = [chunk async for chunk in response] if stream else [response] + else: + response = litellm.completion( + client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(respond))), **kwargs + ) + chunks = list(response) if stream else [response] + assert chunks + + +@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize("stream", [False, True], ids=["completion", "stream"]) +async def test_bedrock_strict_tools_limit_rejection_is_sent_again_without_strict_under_drop_params( + asynchronous: bool, stream: bool, local_model_cost_map: None +) -> None: + sent: Final[list[dict[str, object]]] = [] + + await _converse_with_a_strict_tools_limit_rejection_first(sent, asynchronous, stream, drop_params=True) + + first_tool_spec: Final = sent[0]["toolConfig"]["tools"][0]["toolSpec"] + assert first_tool_spec["strict"] is True + assert sent[1] == { + **sent[0], + "toolConfig": { + **sent[0]["toolConfig"], + "tools": [{"toolSpec": {key: value for key, value in first_tool_spec.items() if key != "strict"}}], + }, + } + + +@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"]) +async def test_bedrock_strict_tools_limit_rejection_reaches_the_caller_without_drop_params( + asynchronous: bool, local_model_cost_map: None +) -> None: + sent: Final[list[dict[str, object]]] = [] + + with pytest.raises(litellm.BadRequestError, match="Too many strict tools"): + await _converse_with_a_strict_tools_limit_rejection_first(sent, asynchronous, stream=False, drop_params=None) + + assert len(sent) == 1