diff --git a/backend/services/llm/deepseek_provider.py b/backend/services/llm/deepseek_provider.py index aa3d32b..80026f4 100644 --- a/backend/services/llm/deepseek_provider.py +++ b/backend/services/llm/deepseek_provider.py @@ -111,8 +111,11 @@ def messages_to_openai(messages: list[Message], *, vision: bool) -> list[dict]: tool_calls = [b for b in m.content if isinstance(b, ToolCall)] msg: dict[str, Any] = {"role": "assistant"} text = "".join(text_parts) - msg["content"] = text if text else None + # DeepSeek 400s with "content or tool_calls must be set" when + # thinking-mode returns reasoning only (content=null, no tools). + # Tool turns may keep content=null; text-only turns need "". if tool_calls: + msg["content"] = text if text else None msg["tool_calls"] = [ { "id": tc.id, @@ -124,6 +127,8 @@ def messages_to_openai(messages: list[Message], *, vision: bool) -> list[dict]: } for tc in tool_calls ] + else: + msg["content"] = text reasoning = _reasoning_from_blocks(m.content) if reasoning: msg["reasoning_content"] = reasoning diff --git a/tests/test_deepseek_provider.py b/tests/test_deepseek_provider.py index 026d8b7..831721b 100644 --- a/tests/test_deepseek_provider.py +++ b/tests/test_deepseek_provider.py @@ -72,6 +72,33 @@ def test_messages_to_openai_pdf_becomes_text(sample_pdf: Path): assert "Extract the pin table" in blob +def test_thinking_only_assistant_sends_empty_content(): + """Thinking with no visible text and no tools must still set content.""" + out = messages_to_openai( + [Message("assistant", [ + TextBlock("", reasoning_content="Need to inspect pin 3 first."), + ])], + vision=False, + ) + assert out[0]["role"] == "assistant" + assert out[0]["content"] == "" + assert out[0]["reasoning_content"] == "Need to inspect pin 3 first." + assert "tool_calls" not in out[0] + + +def test_tool_call_assistant_may_have_null_content(): + out = messages_to_openai( + [Message("assistant", [ + ToolCall(id="c1", name="get_pintable", input={"ref": "U2"}, + reasoning_content="look up pins"), + ])], + vision=False, + ) + assert out[0]["content"] is None + assert out[0]["tool_calls"][0]["function"]["name"] == "get_pintable" + assert out[0]["reasoning_content"] == "look up pins" + + def test_messages_to_openai_tool_roundtrip(): messages = [ Message("assistant", [