From 950c0c41bab47ea6c0d8d6f9575db5d11440e7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Sun, 25 Jan 2026 23:50:41 +0800 Subject: [PATCH] Raise on invalid tool-call deltas --- .../__base/large_language_model.py.md | 2 ++ .../__base/large_language_model.py | 17 +++------------ .../__base/test_increase_tool_call.py | 21 +++++++------------ 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md b/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md index ff39b8a886..f03c41cc25 100644 --- a/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md +++ b/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md @@ -13,6 +13,8 @@ bridge plugin daemon streaming semantics back into API-layer entities (`LLMResul `_normalize_non_stream_plugin_result(...)` / `_build_llm_result_from_first_chunk(...)`. - Tool call deltas are merged incrementally via `_increase_tool_call(...)` to support multiple provider chunking patterns (IDs anchored to first chunk, every chunk, or missing entirely). +- A tool-call delta with an empty `id` requires at least one existing tool call; otherwise we raise `ValueError` to + surface invalid delta sequences explicitly. - Callback invocation is centralized in `_run_callbacks(...)` to ensure consistent error handling/logging. - For compatibility with dify issue `#17799`, `prompt_messages` may be removed by the plugin daemon in chunks and must be re-attached in this layer before callbacks/consumers use them. diff --git a/api/core/model_runtime/model_providers/__base/large_language_model.py b/api/core/model_runtime/model_providers/__base/large_language_model.py index 984e06fa36..1a4310df2f 100644 --- a/api/core/model_runtime/model_providers/__base/large_language_model.py +++ b/api/core/model_runtime/model_providers/__base/large_language_model.py @@ -53,23 +53,12 @@ def _get_or_create_tool_call( If `tool_call_id` is empty, returns the most recently created tool call. """ if not tool_call_id: - if existing_tools_calls: - return existing_tools_calls[-1] - - tool_call = AssistantPromptMessage.ToolCall( - id="", - type="function", - function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="", arguments=""), - ) - existing_tools_calls.append(tool_call) - return tool_call + if not existing_tools_calls: + raise ValueError("tool_call_id is empty but no existing tool call is available to apply the delta") + return existing_tools_calls[-1] tool_call = next((tool_call for tool_call in existing_tools_calls if tool_call.id == tool_call_id), None) if tool_call is None: - if existing_tools_calls and not existing_tools_calls[-1].id: - existing_tools_calls[-1].id = tool_call_id - return existing_tools_calls[-1] - tool_call = AssistantPromptMessage.ToolCall( id=tool_call_id, type="function", diff --git a/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py b/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py index cb4eee3d3b..5fbdabceed 100644 --- a/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py +++ b/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +import pytest + from core.model_runtime.entities.message_entities import AssistantPromptMessage from core.model_runtime.model_providers.__base.large_language_model import _increase_tool_call @@ -99,21 +101,12 @@ def test__increase_tool_call(): _run_case(INPUTS_CASE_4, EXPECTED_CASE_4) -def test__increase_tool_call__no_id_no_name_first_delta(): +def test__increase_tool_call__no_id_no_name_first_delta_should_raise(): inputs = [ ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')), ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments='"value"}')), ] - - expected = [ - ToolCall( - id="RANDOM_ID_1", - type="function", - function=ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}'), - ), - ] - - mock_id_generator = MagicMock() - mock_id_generator.side_effect = ["RANDOM_ID_1"] - with patch("core.model_runtime.model_providers.__base.large_language_model._gen_tool_call_id", mock_id_generator): - _run_case(inputs, expected) + actual: list[ToolCall] = [] + with patch("core.model_runtime.model_providers.__base.large_language_model._gen_tool_call_id", MagicMock()): + with pytest.raises(ValueError): + _increase_tool_call(inputs, actual)