mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(hosted_vllm): forward strict only when true and leave the Responses bridge and MCP tools unchanged
This commit is contained in:
parent
3c629ce7c3
commit
4b59c5a240
6 changed files with 48 additions and 43 deletions
|
|
@ -3,9 +3,11 @@ Translate from OpenAI's `/v1/chat/completions` to VLLM's `/v1/chat/completions`
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Coroutine, Mapping
|
||||
from typing import Final, Literal, cast, overload
|
||||
|
||||
from typing_extensions import TypeIs # noqa: TID251 # TypeIs reaches typing only on 3.13
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_image_mime_type_from_url,
|
||||
)
|
||||
|
|
@ -26,6 +28,17 @@ from litellm.types.llms.openai import (
|
|||
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
|
||||
|
||||
def _is_str_keyed_mapping(value: object) -> TypeIs[Mapping[str, object]]: # guard-ok: JSON tool objects have str keys
|
||||
return isinstance(value, Mapping)
|
||||
|
||||
|
||||
def _strict_only_when_true(tool: Mapping[str, object]) -> Mapping[str, object]:
|
||||
function: Final = tool.get("function")
|
||||
if not _is_str_keyed_mapping(function) or function.get("strict", True) is True:
|
||||
return tool
|
||||
return {**tool, "function": {key: value for key, value in function.items() if key != "strict"}}
|
||||
|
||||
|
||||
class HostedVLLMChatConfig(OpenAIGPTConfig):
|
||||
def _convert_custom_tools_to_function_tools(self, tools: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
"""
|
||||
|
|
@ -88,7 +101,9 @@ class HostedVLLMChatConfig(OpenAIGPTConfig):
|
|||
tools: Final = non_default_params.pop("tools", None)
|
||||
if tools is not None:
|
||||
non_default_params["tools"] = (
|
||||
self._convert_custom_tools_to_function_tools(tools) if isinstance(tools, list) else tools
|
||||
[_strict_only_when_true(tool) for tool in self._convert_custom_tools_to_function_tools(tools)]
|
||||
if isinstance(tools, list)
|
||||
else tools
|
||||
)
|
||||
|
||||
thinking: Final = non_default_params.pop("thinking", None)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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[Mapping[str, object]] = (
|
||||
parameters: Final = (
|
||||
MappingProxyType(raw_parameters) if isinstance(raw_parameters, Mapping) else MappingProxyType({})
|
||||
)
|
||||
normalized_parameters: Final = (
|
||||
|
|
@ -2716,16 +2716,11 @@ class LiteLLMCompletionResponsesConfig:
|
|||
)
|
||||
tool_name: Final = str(namespace_tool.get("name") or "")
|
||||
chat_tool_name: Final = f"{namespace}__{tool_name}" if nested else tool_name
|
||||
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
|
||||
)
|
||||
function: Final = ChatCompletionToolParamFunctionChunk(
|
||||
name=chat_tool_name,
|
||||
description=description,
|
||||
parameters=dict(normalized_parameters),
|
||||
strict=bool(namespace_tool.get("strict", False)),
|
||||
)
|
||||
return function_chat_tool(
|
||||
function,
|
||||
|
|
@ -2803,14 +2798,13 @@ 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": caller_strict} if isinstance(caller_strict, bool) else {}),
|
||||
"strict": typed_tool.get("strict", False) or False,
|
||||
},
|
||||
}
|
||||
if tool.get("cache_control"):
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ def test_hosted_vllm_custom_tools_use_top_level_input_schema():
|
|||
assert tools[0]["function"]["parameters"] == input_schema
|
||||
|
||||
|
||||
def _strict_tools(strict: bool = True) -> list[dict[str, object]]:
|
||||
def _strict_tools(strict: object = True) -> list[dict[str, object]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
|
|
@ -428,10 +428,9 @@ def _strict_tools(strict: bool = True) -> 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, strict: bool) -> None:
|
||||
tools: Final = _strict_tools(strict)
|
||||
def test_hosted_vllm_forwards_strict_tool_schema_as_sent(drop_params: bool) -> None:
|
||||
tools: Final = _strict_tools()
|
||||
sent: Final = copy.deepcopy(tools)
|
||||
|
||||
optional_params: Final = get_optional_params(
|
||||
|
|
@ -446,6 +445,26 @@ def test_hosted_vllm_forwards_strict_tool_schema_as_sent(drop_params: bool, stri
|
|||
assert tools == sent
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unmarked_strict", [False, None, "true", 1])
|
||||
def test_hosted_vllm_drops_a_strict_that_is_not_true(unmarked_strict: object) -> None:
|
||||
tools: Final = [*_strict_tools(), *_strict_tools(unmarked_strict)]
|
||||
sent: Final = copy.deepcopy(tools)
|
||||
|
||||
optional_params: Final = get_optional_params(
|
||||
model="my-vllm-model",
|
||||
custom_llm_provider="hosted_vllm",
|
||||
tools=tools,
|
||||
tool_choice="required",
|
||||
)
|
||||
|
||||
strict_tool, unmarked_tool = _strict_tools()[0], _strict_tools(unmarked_strict)[0]
|
||||
assert optional_params["tools"] == [
|
||||
strict_tool,
|
||||
{**unmarked_tool, "function": {k: v for k, v in unmarked_tool["function"].items() if k != "strict"}},
|
||||
]
|
||||
assert tools == sent
|
||||
|
||||
|
||||
def test_hosted_vllm_additional_drop_params_removes_only_the_named_tool_fields() -> None:
|
||||
tools: Final = _strict_tools()
|
||||
sent: Final = copy.deepcopy(tools)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ 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):
|
||||
|
|
|
|||
|
|
@ -2001,29 +2001,6 @@ 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue