mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(deepseek): drop non-function tools before chat completions call
DeepSeek's /chat/completions only accepts tools of type "function". Requests bridged from /v1/responses can carry responses-API-native tool types, for example a Codex CLI tool typed "namespace", which DeepSeek rejects with "unknown variant 'namespace', expected 'function'" so the whole request fails (issue #30722). Filter unsupported tool types in the DeepSeek request transform so the function tools still go through; when nothing callable remains, also drop the now-dangling tool_choice and parallel_tool_calls Fixes #30722
This commit is contained in:
parent
84c1414aef
commit
5bc0830e31
3 changed files with 130 additions and 0 deletions
|
|
@ -146,6 +146,51 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
and (optional_params.get("thinking") or {}).get("type") == "enabled"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _drop_unsupported_tools(optional_params: dict) -> dict:
|
||||
"""
|
||||
DeepSeek's /chat/completions only accepts tools of type "function".
|
||||
|
||||
Requests bridged from /v1/responses can carry responses-API-native tool
|
||||
types (e.g. a Codex CLI tool typed "namespace"); DeepSeek rejects the
|
||||
whole request with `unknown variant '<type>', expected 'function'` (issue
|
||||
#30722). Drop the unsupported entries so the function tools still go
|
||||
through, and drop the now-dangling tool_choice/parallel_tool_calls when
|
||||
nothing callable survives.
|
||||
"""
|
||||
tools = optional_params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return optional_params
|
||||
|
||||
def _is_function_tool(tool: object) -> bool:
|
||||
return isinstance(tool, dict) and tool.get("type") == "function"
|
||||
|
||||
function_tools = [tool for tool in tools if _is_function_tool(tool)]
|
||||
if len(function_tools) == len(tools):
|
||||
return optional_params
|
||||
|
||||
dropped_types = sorted(
|
||||
{
|
||||
str(tool.get("type")) if isinstance(tool, dict) else type(tool).__name__
|
||||
for tool in tools
|
||||
if not _is_function_tool(tool)
|
||||
}
|
||||
)
|
||||
litellm.verbose_logger.warning(
|
||||
"DeepSeek chat completions only supports function tools; dropping "
|
||||
"unsupported tool type(s) %s before sending the request",
|
||||
dropped_types,
|
||||
)
|
||||
|
||||
cleaned = {k: v for k, v in optional_params.items() if k != "tools"}
|
||||
if function_tools:
|
||||
return {**cleaned, "tools": function_tools}
|
||||
return {
|
||||
k: v
|
||||
for k, v in cleaned.items()
|
||||
if k not in ("tool_choice", "parallel_tool_calls")
|
||||
}
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
model: str,
|
||||
|
|
@ -163,6 +208,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
(user explicitly enabled it), preventing spurious injection on models
|
||||
like deepseek-v3.2 that support thinking as opt-in but not always-on.
|
||||
"""
|
||||
optional_params = self._drop_unsupported_tools(optional_params)
|
||||
if self._thinking_mode_active(model=model, optional_params=optional_params):
|
||||
messages = self._fill_reasoning_content(messages)
|
||||
return super().transform_request(
|
||||
|
|
@ -185,6 +231,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
Async equivalent of transform_request — applies the same reasoning_content
|
||||
fix for multi-turn thinking-mode conversations.
|
||||
"""
|
||||
optional_params = self._drop_unsupported_tools(optional_params)
|
||||
if self._thinking_mode_active(model=model, optional_params=optional_params):
|
||||
messages = self._fill_reasoning_content(messages)
|
||||
return await super().async_transform_request(
|
||||
|
|
|
|||
0
tests/test_litellm/llms/deepseek/chat/__init__.py
Normal file
0
tests/test_litellm/llms/deepseek/chat/__init__.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig
|
||||
|
||||
|
||||
def _function_tool(name: str) -> dict:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {"name": name, "parameters": {"type": "object"}},
|
||||
}
|
||||
|
||||
|
||||
def test_drop_unsupported_tools_keeps_function_tools_only():
|
||||
optional_params = {
|
||||
"tools": [
|
||||
_function_tool("shell"),
|
||||
{"type": "namespace", "name": "container.exec"},
|
||||
_function_tool("apply_patch"),
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
}
|
||||
|
||||
result = DeepSeekChatConfig._drop_unsupported_tools(optional_params)
|
||||
|
||||
assert [tool["function"]["name"] for tool in result["tools"]] == [
|
||||
"shell",
|
||||
"apply_patch",
|
||||
]
|
||||
assert all(tool["type"] == "function" for tool in result["tools"])
|
||||
assert result["tool_choice"] == "auto"
|
||||
|
||||
|
||||
def test_drop_unsupported_tools_drops_dangling_tool_choice_when_none_survive():
|
||||
optional_params = {
|
||||
"tools": [{"type": "namespace", "name": "container.exec"}],
|
||||
"tool_choice": "required",
|
||||
"parallel_tool_calls": True,
|
||||
"temperature": 0.2,
|
||||
}
|
||||
|
||||
result = DeepSeekChatConfig._drop_unsupported_tools(optional_params)
|
||||
|
||||
assert "tools" not in result
|
||||
assert "tool_choice" not in result
|
||||
assert "parallel_tool_calls" not in result
|
||||
assert result["temperature"] == 0.2
|
||||
|
||||
|
||||
def test_drop_unsupported_tools_is_noop_for_function_only():
|
||||
optional_params = {
|
||||
"tools": [_function_tool("shell")],
|
||||
"tool_choice": "auto",
|
||||
}
|
||||
|
||||
result = DeepSeekChatConfig._drop_unsupported_tools(optional_params)
|
||||
|
||||
assert result is optional_params
|
||||
|
||||
|
||||
def test_drop_unsupported_tools_is_noop_without_tools():
|
||||
optional_params = {"temperature": 0.7}
|
||||
|
||||
result = DeepSeekChatConfig._drop_unsupported_tools(optional_params)
|
||||
|
||||
assert result is optional_params
|
||||
|
||||
|
||||
def test_transform_request_strips_unsupported_tools_from_body():
|
||||
config = DeepSeekChatConfig()
|
||||
body = config.transform_request(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
optional_params={
|
||||
"tools": [
|
||||
_function_tool("shell"),
|
||||
{"type": "namespace", "name": "container.exec"},
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert [tool["type"] for tool in body["tools"]] == ["function"]
|
||||
assert body["tools"][0]["function"]["name"] == "shell"
|
||||
Loading…
Add table
Reference in a new issue