diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 5cc7431fb..6fe00b327 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -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( diff --git a/tests/providers/test_reasoning_content.py b/tests/providers/test_reasoning_content.py index a58569143..8bb0b45fd 100644 --- a/tests/providers/test_reasoning_content.py +++ b/tests/providers/test_reasoning_content.py @@ -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 ─────────────────────────────────