fix: handle null tool_call arguments during stream processing

Some OpenAI-compatible providers (e.g. DeepSeek via DeepInfra) stream
tool_calls with function.arguments set to null. split_json_objects then
called len(None) and aborted process_chat mid-stream after partial text
had already rendered.

Coerce null/non-string arguments before splitting, normalize them onto
the tool call, and harden stream delta accumulation when arguments is null.

Closes #27195
This commit is contained in:
Ryan Baron de Ropp 2026-07-19 10:35:14 -04:00
parent 2dbdba1f91
commit 4566540f7b

View file

@ -238,7 +238,15 @@ def _split_tool_calls(
independently. Single-object arguments pass through unchanged.
"""
def split_json_objects(raw: str) -> list[str]:
def split_json_objects(raw: str | None) -> list[str]:
# Some providers stream tool_calls with arguments: null. Treat as empty.
if raw is None:
return ['{}']
if not isinstance(raw, str):
try:
raw = json.dumps(raw)
except Exception:
raw = str(raw)
decoder = json.JSONDecoder()
results = []
position = 0
@ -259,7 +267,17 @@ def _split_tool_calls(
expanded = []
for tool_call in tool_calls:
arguments = tool_call.get('function', {}).get('arguments', '')
arguments = tool_call.get('function', {}).get('arguments')
if arguments is None:
arguments = '{}'
elif not isinstance(arguments, str):
try:
arguments = json.dumps(arguments)
except Exception:
arguments = str(arguments)
# Normalize so downstream never sees None
tool_call.setdefault('function', {})
tool_call['function']['arguments'] = arguments
split_arguments = split_json_objects(arguments)
if len(split_arguments) <= 1:
@ -4331,6 +4349,9 @@ async def streaming_chat_response_handler(response, ctx):
delta_tool_call.setdefault('function', {})
delta_tool_call['function'].setdefault('name', '')
delta_tool_call['function'].setdefault('arguments', '')
# Providers may send arguments: null; setdefault won't replace null.
if delta_tool_call['function'].get('arguments') is None:
delta_tool_call['function']['arguments'] = ''
response_tool_calls.append(delta_tool_call)
else:
# Update the existing tool call
@ -4343,8 +4364,13 @@ async def streaming_chat_response_handler(response, ctx):
current_response_tool_call['function']['name'] = delta_name
if delta_arguments:
current_response_tool_call['function']['arguments'] += (
delta_arguments
existing_args = current_response_tool_call['function'].get(
'arguments'
)
if existing_args is None:
existing_args = ''
current_response_tool_call['function']['arguments'] = (
existing_args + delta_arguments
)
# Emit pending tool calls in real-time