fix: assistant messages with only tool calls are sent with an empty string instead of null (#34265)

When an assistant message has tool calls but no text, the OpenAI Chat Completions format uses `content: null`, since content is optional next to tool calls and OpenAI's own API returns null there. Open WebUI sent an empty string in two places: when replaying earlier tool calls from the chat history, and when converting requests from clients that use its Anthropic-compatible endpoint. For Claude models, LiteLLM 1.81.14 through 1.108.x replaces that empty string with "[System: Empty message content sanitised to satisfy protocol]", and the model then repeats that line in its replies (LiteLLM 1.109 and later leave the empty text out instead). These messages now go out with null content, which OpenAI and every provider tested on OpenRouter (Gemini, Claude, DeepSeek, Qwen, Mistral, Llama, Kimi and Grok) accept.

Fixes #34264
This commit is contained in:
Classic298
2026-11-13 18:26:08 +09:00
committed by GitHub
parent bb1305d522
commit 80478abea1
4 changed files with 5 additions and 5 deletions
+1 -1
View File
@@ -364,7 +364,7 @@ def convert_anthropic_payload_to_chat_completions(
if openai_content:
msg_dict['content'] = _finalize_chat_completions_content(openai_content)
else:
msg_dict['content'] = ''
msg_dict['content'] = None
msg_dict['tool_calls'] = tool_calls
messages.append(msg_dict)
elif openai_content or role == 'assistant':
+1 -1
View File
@@ -500,7 +500,7 @@ async def _review_memory(
role = message.get('role', '')
content = message.get('content', '')
if not isinstance(content, str):
content = get_content_from_message(message)
content = get_content_from_message(message) or ''
content = content.strip()
if role not in {'user', 'assistant'} or not content:
continue
+1 -1
View File
@@ -1029,7 +1029,7 @@ async def chat_completion_tools_handler(
recent_messages = messages[-4:] if len(messages) > 4 else messages
chat_history = '\n'.join(
f'{message["role"].upper()}: """{get_content_from_message(message)}"""' for message in recent_messages
f'{message["role"].upper()}: """{get_content_from_message(message) or ""}"""' for message in recent_messages
)
prompt = f'History:\n{chat_history}\nQuery: {user_message}' if chat_history else f'Query: {user_message}'
+2 -2
View File
@@ -223,7 +223,7 @@ def get_message_list(messages_map, message_id):
def get_messages_content(messages: list[dict]) -> str:
return '\n'.join([f'{message["role"].upper()}: {get_content_from_message(message)}' for message in messages])
return '\n'.join([f'{message["role"].upper()}: {get_content_from_message(message) or ""}' for message in messages])
def get_last_user_message_item(messages: list[dict]) -> dict | None:
@@ -477,7 +477,7 @@ def convert_output_to_messages(
message = {
'role': 'assistant',
'content': '\n'.join(pending_content) if pending_content else '',
'content': '\n'.join(pending_content) if pending_content else (None if pending_tool_calls else ''),
**({'tool_calls': pending_tool_calls} if pending_tool_calls else {}),
}