preserve DeepSeek reasoning across clean tool rounds

This commit is contained in:
pewdiepie-archdaemon
2026-09-18 16:16:09 +00:00
parent a453b71651
commit 508e017d89
2 changed files with 38 additions and 3 deletions
+16 -1
View File
@@ -3899,6 +3899,7 @@ def provider_wire_messages(messages):
item.get('role') == 'assistant' item.get('role') == 'assistant'
and not item.get('content') and not item.get('content')
and not item.get('tool_calls') and not item.get('tool_calls')
and not item.get('reasoning_content')
): ):
continue continue
cleaned.append(item) cleaned.append(item)
@@ -4483,7 +4484,7 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
'function': {'name': private_browser_name}, 'function': {'name': private_browser_name},
} }
force_private_browser_next_round = False force_private_browser_next_round = False
pending, content = {}, '' pending, content, round_reasoning = {}, '', ''
streamed_round_text = False streamed_round_text = False
request = search_tool_choice_request(request) request = search_tool_choice_request(request)
request = provider_compatible_tool_choice_request(request, model) request = provider_compatible_tool_choice_request(request, model)
@@ -4528,6 +4529,14 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
if not choices: if not choices:
continue continue
delta = choices[0].get('delta') or {} delta = choices[0].get('delta') or {}
reasoning = (
delta.get('reasoning_content')
or delta.get('reasoning')
or delta.get('thinking')
or ''
)
if reasoning:
round_reasoning += str(reasoning)
text = delta.get('content') or '' text = delta.get('content') or ''
if text: if text:
first_token = first_token or time.monotonic() first_token = first_token or time.monotonic()
@@ -4610,6 +4619,12 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
yield event({'type': 'model_tool_proposal', 'round': round_number, yield event({'type': 'model_tool_proposal', 'round': round_number,
'function': proposal.get('function', {})}) 'function': proposal.get('function', {})})
message = {'role': 'assistant', 'content': content or None} message = {'role': 'assistant', 'content': content or None}
if round_reasoning and 'deepseek' in str(model or '').casefold():
# DeepSeek requires each tool-round reasoning payload to
# be echoed verbatim on subsequent requests. Unlike local
# Qwen/Nemotron templates, its API owns this structured
# field and does not reinterpret it as visible output.
message['reasoning_content'] = round_reasoning
if proposed: if proposed:
message['tool_calls'] = protocol_safe_tool_calls(proposed) message['tool_calls'] = protocol_safe_tool_calls(proposed)
history.append(message) history.append(message)
+22 -2
View File
@@ -531,18 +531,30 @@ def test_repeated_off_contract_artifact_calls_trigger_single_file_body_handoff()
) )
def test_provider_wire_keeps_deepseek_reasoning_only_turn_for_continuity():
import src.clean_agent_preview as module
messages = [{
'role': 'assistant',
'content': None,
'reasoning_content': 'private provider reasoning token stream',
}]
assert module.provider_wire_messages(messages) == messages
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_repeated_off_contract_calls_recover_via_required_artifact_body(monkeypatch): async def test_repeated_off_contract_calls_recover_via_required_artifact_body(monkeypatch):
import src.clean_agent_preview as module import src.clean_agent_preview as module
responses = iter([ responses = iter([
{'choices': [{'delta': {'tool_calls': [{ {'choices': [{'delta': {'reasoning_content': 'reasoning-one', 'tool_calls': [{
'index': 0, 'id': 'bad-bash', 'index': 0, 'id': 'bad-bash',
'function': {'name': 'bash', 'arguments': json.dumps({ 'function': {'name': 'bash', 'arguments': json.dumps({
'command': 'echo nope', 'command': 'echo nope',
})}, })},
}]}}]}, }]}}]},
{'choices': [{'delta': {'tool_calls': [{ {'choices': [{'delta': {'reasoning_content': 'reasoning-two', 'tool_calls': [{
'index': 0, 'id': 'bad-python', 'index': 0, 'id': 'bad-python',
'function': {'name': 'python', 'arguments': json.dumps({ 'function': {'name': 'python', 'arguments': json.dumps({
'code': 'print("nope")', 'code': 'print("nope")',
@@ -566,11 +578,14 @@ async def test_repeated_off_contract_calls_recover_via_required_artifact_body(mo
yield 'data: ' + json.dumps(self.payload) yield 'data: ' + json.dumps(self.payload)
yield 'data: [DONE]' yield 'data: [DONE]'
requests = []
class Client: class Client:
def __init__(self, **kwargs): pass def __init__(self, **kwargs): pass
async def __aenter__(self): return self async def __aenter__(self): return self
async def __aexit__(self, *args): pass async def __aexit__(self, *args): pass
def stream(self, *args, **kwargs): def stream(self, *args, **kwargs):
requests.append(kwargs['json'])
return Response(next(responses)) return Response(next(responses))
executed = [] executed = []
@@ -602,6 +617,11 @@ async def test_repeated_off_contract_calls_recover_via_required_artifact_body(mo
)] )]
assert [block.tool_type for block in executed] == ['write_file'] assert [block.tool_type for block in executed] == ['write_file']
prior_tool_turn = next(
message for message in requests[1]['messages']
if message.get('role') == 'assistant' and message.get('tool_calls')
)
assert prior_tool_turn['reasoning_content'] == 'reasoning-one'
assert executed[0].content == ( assert executed[0].content == (
'/workspace/output.html\n<html><body>Recovered</body></html>' '/workspace/output.html\n<html><body>Recovered</body></html>'
) )