mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
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:
parent
4f5f965f09
commit
05de864f5b
@ -999,7 +999,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
if not content and msg0.get("reasoning") and self._spec and self._spec.reasoning_as_content:
|
||||
content = self._extract_text_content(msg0.get("reasoning"))
|
||||
reasoning_content = msg0.get("reasoning_content")
|
||||
if not reasoning_content and msg0.get("reasoning"):
|
||||
if reasoning_content is None and msg0.get("reasoning"):
|
||||
reasoning_content = self._extract_text_content(msg0.get("reasoning"))
|
||||
for ch in choices:
|
||||
ch_map = self._maybe_mapping(ch) or {}
|
||||
@ -1011,7 +1011,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
finish_reason = str(ch_map["finish_reason"])
|
||||
if not content:
|
||||
content = self._extract_text_content(m.get("content"))
|
||||
if not reasoning_content:
|
||||
if reasoning_content is None:
|
||||
reasoning_content = m.get("reasoning_content")
|
||||
|
||||
parsed_tool_calls = []
|
||||
@ -1074,8 +1074,8 @@ class OpenAICompatProvider(LLMProvider):
|
||||
function_provider_specific_fields=fn_prov,
|
||||
))
|
||||
|
||||
reasoning_content = getattr(msg, "reasoning_content", None) or None
|
||||
if not reasoning_content and getattr(msg, "reasoning", None):
|
||||
reasoning_content = getattr(msg, "reasoning_content", None)
|
||||
if reasoning_content is None and getattr(msg, "reasoning", None):
|
||||
reasoning_content = msg.reasoning
|
||||
|
||||
return LLMResponse(
|
||||
|
||||
@ -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 ─────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user