Retry DeepSeek thinking-only turns so review does not 400 on U19.

Empty assistant content is still treated as unset; replay a placeholder and retry once with thinking disabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 08:15:07 +02:00
co-authored by Cursor
parent e5e8c42966
commit a9238875dd
2 changed files with 137 additions and 10 deletions
+73 -3
View File
@@ -72,8 +72,10 @@ 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."""
def test_thinking_only_assistant_sends_placeholder_content():
"""Empty string is treated as unset; replay a non-empty placeholder."""
from backend.services.llm.deepseek_provider import _EMPTY_ASSISTANT_CONTENT
out = messages_to_openai(
[Message("assistant", [
TextBlock("", reasoning_content="Need to inspect pin 3 first."),
@@ -81,11 +83,19 @@ def test_thinking_only_assistant_sends_empty_content():
vision=False,
)
assert out[0]["role"] == "assistant"
assert out[0]["content"] == ""
assert out[0]["content"] == _EMPTY_ASSISTANT_CONTENT
assert out[0]["content"]
assert out[0]["reasoning_content"] == "Need to inspect pin 3 first."
assert "tool_calls" not in out[0]
def test_empty_assistant_blocks_still_set_content():
from backend.services.llm.deepseek_provider import _EMPTY_ASSISTANT_CONTENT
out = messages_to_openai([Message("assistant", [])], vision=False)
assert out[0]["content"] == _EMPTY_ASSISTANT_CONTENT
def test_tool_call_assistant_may_have_null_content():
out = messages_to_openai(
[Message("assistant", [
@@ -165,6 +175,66 @@ def test_forced_tool_choice_disables_thinking():
assert captured["tool_choice"]["function"]["name"] == "save_resolved_specs"
def test_thinking_only_retries_with_thinking_disabled():
calls: list[dict] = []
class FakeCompletions:
async def create(self, **kwargs):
calls.append({
"thinking": kwargs["extra_body"]["thinking"]["type"],
})
usage = SimpleNamespace(
prompt_tokens=10, completion_tokens=5,
prompt_cache_hit_tokens=0, prompt_tokens_details=None,
)
if kwargs["extra_body"]["thinking"]["type"] == "enabled":
msg = SimpleNamespace(
content=None,
reasoning_content="pondering the schematic",
tool_calls=None,
)
return SimpleNamespace(
choices=[SimpleNamespace(message=msg, finish_reason="stop")],
usage=usage,
)
fn = SimpleNamespace(name="get_pintable", arguments='{"ref":"U19"}')
tc = SimpleNamespace(id="c1", function=fn)
msg = SimpleNamespace(content=None, reasoning_content=None, tool_calls=[tc])
return SimpleNamespace(
choices=[SimpleNamespace(message=msg, finish_reason="tool_calls")],
usage=usage,
)
class FakeClient:
def __init__(self):
self.chat = SimpleNamespace(completions=FakeCompletions())
from backend.services.llm.deepseek_provider import DeepSeekSession
from backend.services.llm.types import Message, TextBlock, ToolSchema
import asyncio
session = DeepSeekSession(
client=FakeClient(),
model="deepseek-v4-pro",
system="sys",
max_tokens=256,
thinking=True,
reasoning_effort="medium",
)
completion = asyncio.run(session.complete(
messages=[Message("user", [TextBlock("review U19")])],
tools=[ToolSchema(
name="get_pintable", description="x",
input_schema={"type": "object"},
)],
tool_choice="auto",
))
assert len(calls) == 2
assert calls[0]["thinking"] == "enabled"
assert calls[1]["thinking"] == "disabled"
assert completion.tool_calls[0].name == "get_pintable"
def test_tool_schema_and_choice():
schema = ToolSchema(
name="save_pintable",