From 2edcd3f61d9e1d963c823bb3655ad795cb740104 Mon Sep 17 00:00:00 2001 From: wesley Date: Mon, 27 Apr 2026 23:37:28 -0700 Subject: [PATCH] fix(agent): use full tool schema for DeepSeek V4 --- .../agent/runners/tool_loop_agent_runner.py | 26 ++++++++++++-- tests/test_tool_loop_agent_runner.py | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index d96a4c92cb..f1fc9a40c2 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -289,10 +289,12 @@ async def reset( # Light tool schema does not include tool parameters. # This can reduce token usage when tools have large descriptions. # See #4681 - self.tool_schema_mode = tool_schema_mode + self.tool_schema_mode = self._normalize_tool_schema_mode( + tool_schema_mode, provider, request + ) self._tool_schema_param_set = None self._skill_like_raw_tool_set = None - if tool_schema_mode == "skills_like": + if self.tool_schema_mode == "skills_like": tool_set = self.req.func_tool if not tool_set: return @@ -322,6 +324,26 @@ async def reset( self.stats = AgentStats() self.stats.start_time = time.time() + @staticmethod + def _normalize_tool_schema_mode( + tool_schema_mode: str | None, + provider: Provider, + request: ProviderRequest, + ) -> str | None: + if tool_schema_mode != "skills_like": + return tool_schema_mode + + model = (request.model or provider.get_model() or "").lower().strip() + model_name = model.rsplit("/", 1)[-1] + if model_name not in {"deepseek-v4-flash", "deepseek-v4-pro"}: + return tool_schema_mode + + logger.info( + "DeepSeek V4 does not support skills-like light tool schemas; " + "using full tool schemas for function calling." + ) + return "full" + def _read_tool_hint(self) -> str: if self.read_tool is not None: return f"`{self.read_tool.name}`" diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index 74d0691085..ef958fc078 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -1273,6 +1273,40 @@ async def text_chat(self, **kwargs) -> LLMResponse: assert parts[0].text == "一张猫的照片" +@pytest.mark.asyncio +async def test_deepseek_v4_uses_full_tool_schema_instead_of_skills_like(): + provider = MockProvider() + tool = FunctionTool( + name="test_tool", + description="测试", + parameters={"type": "object", "properties": {"query": {"type": "string"}}}, + handler=AsyncMock(), + ) + tool_set = ToolSet(tools=[tool]) + req = ProviderRequest( + prompt="test", + func_tool=tool_set, + contexts=[], + model="deepseek-v4-flash", + ) + runner = ToolLoopAgentRunner() + + await runner.reset( + provider=provider, + request=req, + run_context=ContextWrapper(context=None), + tool_executor=cast(Any, MockToolExecutor()), + agent_hooks=MockHooks(), + tool_schema_mode="skills_like", + ) + + assert runner.tool_schema_mode == "full" + assert runner.req.func_tool is tool_set + assert runner.req.func_tool.tools[0].parameters == tool.parameters + assert runner.req.func_tool.tools[0].handler is tool.handler + assert runner._tool_schema_param_set is None + + @pytest.mark.asyncio async def test_follow_up_accepted_when_active_and_not_stopping( runner, mock_provider, provider_request, mock_tool_executor, mock_hooks