fix(sap): default FunctionObj.arguments for streaming tool calls

Streaming tool-call deltas from Claude/SAP often omit `arguments` on
intermediate chunks (name only). FunctionObj required the field, so
pydantic ValidationError crashed the proxy on multi-turn streamed tool use.

Default arguments to "" and add unit tests covering FunctionObj,
MessageToolCall, and SAPAssistantMessage.

Fixes #46405
This commit is contained in:
ningyixi 2026-10-15 00:35:03 +08:00
parent 79abdb66e6
commit 09c4ea4648
2 changed files with 57 additions and 1 deletions

View file

@ -65,7 +65,9 @@ class ImageContent(LiteLLMBaseModel):
class FunctionObj(LiteLLMBaseModel):
name: str
arguments: str
# Default empty string: streaming tool-call deltas often omit arguments
# until the final chunk (OpenAI/Anthropic streaming semantics).
arguments: str = ""
class FunctionTool(LiteLLMBaseModel):

View file

@ -2,6 +2,8 @@ import pytest
from pydantic import ValidationError
from litellm.llms.sap.chat.models import (
FunctionObj,
MessageToolCall,
SAPAssistantMessage,
SAPMessage,
SAPToolChatMessage,
@ -168,6 +170,58 @@ class TestSAPAssistantMessage:
assert msg.content.cache_control.type == "ephemeral"
class TestFunctionObj:
def test_name_and_arguments_accepted(self):
obj = FunctionObj.model_validate({"name": "get_weather", "arguments": '{"city":"Berlin"}'})
assert obj.name == "get_weather"
assert obj.arguments == '{"city":"Berlin"}'
def test_missing_arguments_defaults_to_empty_string(self):
# Streaming intermediate tool-call chunks often omit arguments (name only).
obj = FunctionObj.model_validate({"name": "get_weather"})
assert obj.name == "get_weather"
assert obj.arguments == ""
def test_explicit_empty_arguments_accepted(self):
obj = FunctionObj.model_validate({"name": "get_weather", "arguments": ""})
assert obj.arguments == ""
def test_missing_name_rejected(self):
with pytest.raises(ValidationError):
FunctionObj.model_validate({"arguments": "{}"})
class TestMessageToolCall:
def test_tool_call_with_arguments(self):
tc = MessageToolCall.model_validate({
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{}"},
})
assert tc.function.name == "get_weather"
assert tc.function.arguments == "{}"
def test_tool_call_without_arguments_for_streaming_delta(self):
tc = MessageToolCall.model_validate({
"id": "call_1",
"type": "function",
"function": {"name": "get_weather"},
})
assert tc.function.arguments == ""
def test_assistant_message_streaming_tool_call_without_arguments(self):
msg = SAPAssistantMessage.model_validate({
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather"},
}]
})
assert len(msg.tool_calls) == 1
assert msg.tool_calls[0].function.name == "get_weather"
assert msg.tool_calls[0].function.arguments == ""
class TestSAPToolChatMessage:
def test_role_is_always_tool(self):
msg = SAPToolChatMessage.model_validate({"tool_call_id": "call_1", "content": "ok"})