Fix tool-calling HTTP 400 on Gemini and Ollama: send null, not empty, assistant content

When an agent turn uses native (OpenAI-style) function calling and the model
returns only tool calls with no prose, _append_tool_results built the follow-up
assistant message with content "" (empty string).

Google Gemini's OpenAI-compatible endpoint and Ollama both reject an assistant
message that carries tool_calls alongside an empty-string content with HTTP 400.
Because that message feeds the tool results back to the model, every tool-using
turn on these providers dies at the second round: the tool runs, but the agent
never produces a result.

Use None (JSON null) instead, which is the spec-correct form the OpenAI SDK
itself emits and which OpenAI and Anthropic accept too. Adds tests covering the
native tool-call content shaping.
This commit is contained in:
James Arslan
2026-06-02 00:34:51 +00:00
parent 7b9ef95b60
commit cb13d09029
2 changed files with 73 additions and 4 deletions

View File

@@ -1054,7 +1054,14 @@ def _append_tool_results(
"""
if used_native and native_tool_calls:
assistant_msg = {"role": "assistant"}
assistant_msg["content"] = round_response if round_response.strip() else ""
# When the model emitted ONLY tool calls (no prose), content must be
# null, NOT an empty string. Google Gemini's OpenAI-compatible endpoint
# and Ollama both reject an assistant message that carries tool_calls
# alongside empty-string content with HTTP 400 ("contents is not
# specified" / a JSON parse error), which aborts every tool-using turn
# at the follow-up round. null (i.e. omitted text) is the spec-correct
# form the OpenAI SDK itself emits, and OpenAI/Anthropic accept it too.
assistant_msg["content"] = round_response if round_response.strip() else None
if round_reasoning:
assistant_msg["reasoning_content"] = round_reasoning
assistant_msg["tool_calls"] = [