fix(hosted_vllm): stop the Responses bridge and MCP tools from adding a strict the caller never set

This commit is contained in:
mateo-berri 2026-10-28 17:57:02 -07:00
parent 4ac942adf0
commit 3c629ce7c3
5 changed files with 42 additions and 12 deletions

View file

@ -37,7 +37,6 @@ def transform_mcp_tool_to_openai_tool(mcp_tool: MCPTool) -> ChatCompletionToolPa
name=mcp_tool.name,
description=mcp_tool.description or "",
parameters=normalized_parameters,
strict=False,
),
)

View file

@ -2708,7 +2708,7 @@ class LiteLLMCompletionResponsesConfig:
return convert_custom_tool_to_function_tool({**namespace_tool, "description": description})
raw_parameters: Final = namespace_tool.get("parameters")
parameters: Final = (
parameters: Final[Mapping[str, object]] = (
MappingProxyType(raw_parameters) if isinstance(raw_parameters, Mapping) else MappingProxyType({})
)
normalized_parameters: Final = (
@ -2716,11 +2716,16 @@ class LiteLLMCompletionResponsesConfig:
)
tool_name: Final = str(namespace_tool.get("name") or "")
chat_tool_name: Final = f"{namespace}__{tool_name}" if nested else tool_name
function: Final = ChatCompletionToolParamFunctionChunk(
name=chat_tool_name,
description=description,
parameters=dict(normalized_parameters),
strict=bool(namespace_tool.get("strict", False)),
chat_parameters: Final = dict(normalized_parameters)
caller_strict: Final = namespace_tool.get("strict")
function: Final = (
ChatCompletionToolParamFunctionChunk(
name=chat_tool_name, description=description, parameters=chat_parameters, strict=caller_strict
)
if isinstance(caller_strict, bool)
else ChatCompletionToolParamFunctionChunk(
name=chat_tool_name, description=description, parameters=chat_parameters
)
)
return function_chat_tool(
function,
@ -2798,13 +2803,14 @@ class LiteLLMCompletionResponsesConfig:
typed_tool: Final = cast(FunctionToolParam, tool)
raw_parameters: Final = typed_tool.get("parameters", {}) or {}
parameters: Final = {**raw_parameters} if "type" in raw_parameters else {**raw_parameters, "type": "object"}
caller_strict: Final = typed_tool.get("strict")
chat_completion_tool: Final[dict[str, object]] = {
"type": "function",
"function": {
"name": typed_tool.get("name") or "",
"description": typed_tool.get("description") or "",
"parameters": parameters,
"strict": typed_tool.get("strict", False) or False,
**({"strict": caller_strict} if isinstance(caller_strict, bool) else {}),
},
}
if tool.get("cache_control"):

View file

@ -402,14 +402,14 @@ def test_hosted_vllm_custom_tools_use_top_level_input_schema():
assert tools[0]["function"]["parameters"] == input_schema
def _strict_tools() -> list[dict[str, object]]:
def _strict_tools(strict: bool = True) -> list[dict[str, object]]:
return [
{
"type": "function",
"function": {
"name": "record_code",
"description": "Record a ten letter code",
"strict": True,
"strict": strict,
"parameters": {
"type": "object",
"properties": {
@ -428,9 +428,10 @@ def _strict_tools() -> list[dict[str, object]]:
]
@pytest.mark.parametrize("strict", [True, False])
@pytest.mark.parametrize("drop_params", [True, False])
def test_hosted_vllm_forwards_strict_tool_schema_as_sent(drop_params: bool) -> None:
tools: Final = _strict_tools()
def test_hosted_vllm_forwards_strict_tool_schema_as_sent(drop_params: bool, strict: bool) -> None:
tools: Final = _strict_tools(strict)
sent: Final = copy.deepcopy(tools)
optional_params: Final = get_optional_params(

View file

@ -73,6 +73,7 @@ def test_transform_mcp_tool_to_openai_tool(mock_mcp_tool):
"properties": {"test": {"type": "string"}},
"additionalProperties": False,
}
assert "strict" not in openai_tool["function"]
def testtransform_openai_tool_call_request_to_mcp_tool_call_request(mock_mcp_tool):

View file

@ -2001,6 +2001,29 @@ class TestToolTransformation:
assert [tool["function"]["name"] for tool in not_deferred] == ["finance__get_stock_price", "get_weather"]
assert all("defer_loading" not in tool for tool in not_deferred)
def test_function_tools_carry_strict_only_when_the_caller_set_it(self) -> None:
strict_variants: Final = {"unset": {}, "null": {"strict": None}, "on": {"strict": True}, "off": {"strict": False}}
function_tools: Final = [
{"type": "function", "name": name, "parameters": {"type": "object"}, **strict}
for name, strict in strict_variants.items()
]
chat_tools, _ = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
tools=[*function_tools, {"type": "namespace", "name": "ns", "tools": function_tools}]
)
sent_strict: Final = {tool["function"]["name"]: tool["function"].get("strict", "absent") for tool in chat_tools}
assert sent_strict == {
"unset": "absent",
"null": "absent",
"on": True,
"off": False,
"ns__unset": "absent",
"ns__null": "absent",
"ns__on": True,
"ns__off": False,
}
@pytest.mark.parametrize("global_drop_params", [False, True])
def test_malformed_tool_flags_answer_400_unless_drop_params_drops_them(
self, monkeypatch: pytest.MonkeyPatch, global_drop_params: bool