From b653183bb0f76b0f3e157506e9906398efe7c311 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 22 Feb 2026 18:26:42 +0000 Subject: [PATCH] refactor(providers): move empty content sanitization to base class --- nanobot/providers/base.py | 40 ++++++++++++++++++++++++ nanobot/providers/custom_provider.py | 45 +-------------------------- nanobot/providers/litellm_provider.py | 2 +- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/nanobot/providers/base.py b/nanobot/providers/base.py index c69c38b..eb1599a 100644 --- a/nanobot/providers/base.py +++ b/nanobot/providers/base.py @@ -39,6 +39,46 @@ class LLMProvider(ABC): def __init__(self, api_key: str | None = None, api_base: str | None = None): self.api_key = api_key self.api_base = api_base + + @staticmethod + def _sanitize_empty_content(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Replace empty text content that causes provider 400 errors. + + Empty content can appear when MCP tools return nothing. Most providers + reject empty-string content or empty text blocks in list content. + """ + result: list[dict[str, Any]] = [] + for msg in messages: + content = msg.get("content") + + if isinstance(content, str) and not content: + clean = dict(msg) + clean["content"] = None if (msg.get("role") == "assistant" and msg.get("tool_calls")) else "(empty)" + result.append(clean) + continue + + if isinstance(content, list): + filtered = [ + item for item in content + if not ( + isinstance(item, dict) + and item.get("type") in ("text", "input_text", "output_text") + and not item.get("text") + ) + ] + if len(filtered) != len(content): + clean = dict(msg) + if filtered: + clean["content"] = filtered + elif msg.get("role") == "assistant" and msg.get("tool_calls"): + clean["content"] = None + else: + clean["content"] = "(empty)" + result.append(clean) + continue + + result.append(msg) + return result @abstractmethod async def chat( diff --git a/nanobot/providers/custom_provider.py b/nanobot/providers/custom_provider.py index 9a0901c..a578d14 100644 --- a/nanobot/providers/custom_provider.py +++ b/nanobot/providers/custom_provider.py @@ -21,7 +21,7 @@ class CustomProvider(LLMProvider): model: str | None = None, max_tokens: int = 4096, temperature: float = 0.7) -> LLMResponse: kwargs: dict[str, Any] = { "model": model or self.default_model, - "messages": self._prevent_empty_text_blocks(messages), + "messages": self._sanitize_empty_content(messages), "max_tokens": max(1, max_tokens), "temperature": temperature, } @@ -50,46 +50,3 @@ class CustomProvider(LLMProvider): def get_default_model(self) -> str: return self.default_model - @staticmethod - def _prevent_empty_text_blocks(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Filter empty text content blocks that cause provider 400 errors. - - When MCP tools return empty content, messages may contain empty-string - text blocks. Most providers (OpenAI-compatible) reject these with 400. - This method filters them out before sending to the API. - """ - patched: list[dict[str, Any]] = [] - for msg in messages: - content = msg.get("content") - - # Empty string content - if isinstance(content, str) and content == "": - clean = dict(msg) - if msg.get("role") == "assistant" and msg.get("tool_calls"): - clean["content"] = None - else: - clean["content"] = "(empty)" - patched.append(clean) - continue - - # List content — filter out empty text items - if isinstance(content, list): - filtered = [ - item for item in content - if not (isinstance(item, dict) - and item.get("type") in {"text", "input_text", "output_text"} - and item.get("text") == "") - ] - if filtered != content: - clean = dict(msg) - if filtered: - clean["content"] = filtered - elif msg.get("role") == "assistant" and msg.get("tool_calls"): - clean["content"] = None - else: - clean["content"] = "(empty)" - patched.append(clean) - continue - - patched.append(msg) - return patched diff --git a/nanobot/providers/litellm_provider.py b/nanobot/providers/litellm_provider.py index 784f02c..7402a2b 100644 --- a/nanobot/providers/litellm_provider.py +++ b/nanobot/providers/litellm_provider.py @@ -196,7 +196,7 @@ class LiteLLMProvider(LLMProvider): kwargs: dict[str, Any] = { "model": model, - "messages": self._sanitize_messages(messages), + "messages": self._sanitize_messages(self._sanitize_empty_content(messages)), "max_tokens": max_tokens, "temperature": temperature, }