fix: preserve empty-string reasoning_content instead of coercing to None

Custom providers (e.g. DeepSeek) may return reasoning_content as an
empty string "" to explicitly indicate no reasoning occurred. The
previous truthiness checks (, ) treated "" as falsy
and converted it to None, which caused the field to be dropped from
the message history entirely. Providers that require reasoning_content
on all assistant messages then rejected subsequent requests.

Replace truthiness checks with identity checks () so that
empty-string reasoning_content is preserved as-is. The streaming path
is unchanged since an empty join genuinely means no chunks received.

Fixes #4105
This commit is contained in:
michaelxer
2026-06-08 01:08:27 +08:00
committed by Xubin Ren
parent 4f5f965f09
commit 05de864f5b
2 changed files with 30 additions and 5 deletions
+26 -1
View File
@@ -9,7 +9,6 @@ from unittest.mock import patch
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
# ── _parse: non-streaming ─────────────────────────────────────────────────
@@ -52,6 +51,32 @@ def test_parse_dict_reasoning_content_none_when_absent() -> None:
assert result.reasoning_content is None
def test_parse_dict_reasoning_content_empty_string_preserved() -> None:
"""reasoning_content=\"\" is preserved, not coerced to None.
Some providers (e.g. DeepSeek) require the reasoning_content key to
be present in subsequent requests even when empty. Coercing \"\" to
None drops the key downstream and causes API errors.
"""
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
provider = OpenAICompatProvider()
response = {
"choices": [{
"message": {
"content": "answer",
"reasoning_content": "",
},
"finish_reason": "stop",
}],
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
}
result = provider._parse(response)
assert result.reasoning_content == ""
# ── _parse_chunks: streaming dict branch ─────────────────────────────────