From e44f14379a289f900556fa3d6f255f446aee634f Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 18 Feb 2026 11:57:58 +0300 Subject: [PATCH 1/2] fix: sanitize messages and ensure 'content' for strict LLM providers - Strip non-standard keys like 'reasoning_content' before sending to LLM - Always include 'content' key in assistant messages (required by StepFun) - Add _sanitize_messages to LiteLLMProvider to prevent 400 BadRequest errors --- nanobot/agent/context.py | 6 +++--- nanobot/providers/litellm_provider.py | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index cfd6318..458016e 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -227,9 +227,9 @@ To recall past events, grep {workspace_path}/memory/HISTORY.md""" """ msg: dict[str, Any] = {"role": "assistant"} - # Omit empty content — some backends reject empty text blocks - if content: - msg["content"] = content + # Always include content — some providers (e.g. StepFun) reject + # assistant messages that omit the key entirely. + msg["content"] = content if tool_calls: msg["tool_calls"] = tool_calls diff --git a/nanobot/providers/litellm_provider.py b/nanobot/providers/litellm_provider.py index 8cc4e35..58acf95 100644 --- a/nanobot/providers/litellm_provider.py +++ b/nanobot/providers/litellm_provider.py @@ -12,6 +12,12 @@ from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest from nanobot.providers.registry import find_by_model, find_gateway +# Keys that are part of the OpenAI chat-completion message schema. +# Anything else (e.g. reasoning_content, timestamp) is stripped before sending +# to avoid "Unrecognized chat message" errors from strict providers like StepFun. +_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name"}) + + class LiteLLMProvider(LLMProvider): """ LLM provider using LiteLLM for multi-provider support. @@ -103,6 +109,24 @@ class LiteLLMProvider(LLMProvider): kwargs.update(overrides) return + @staticmethod + def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Strip non-standard keys from messages for strict providers. + + Some providers (e.g. StepFun via OpenRouter) reject messages that + contain extra keys like ``reasoning_content``. This method keeps + only the keys defined in the OpenAI chat-completion schema and + ensures every assistant message has a ``content`` key. + """ + sanitized = [] + for msg in messages: + clean = {k: v for k, v in msg.items() if k in _ALLOWED_MSG_KEYS} + # Strict providers require "content" even when assistant only has tool_calls + if clean.get("role") == "assistant" and "content" not in clean: + clean["content"] = None + sanitized.append(clean) + return sanitized + async def chat( self, messages: list[dict[str, Any]], @@ -132,7 +156,7 @@ class LiteLLMProvider(LLMProvider): kwargs: dict[str, Any] = { "model": model, - "messages": messages, + "messages": self._sanitize_messages(messages), "max_tokens": max_tokens, "temperature": temperature, } From 5cc019bf1a53df1fd2ff1e50cd40b4e358804a4f Mon Sep 17 00:00:00 2001 From: Re-bin Date: Fri, 20 Feb 2026 11:27:21 +0000 Subject: [PATCH 2/2] style: trim verbose comments in _sanitize_messages --- nanobot/providers/litellm_provider.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/nanobot/providers/litellm_provider.py b/nanobot/providers/litellm_provider.py index 4fe44f7..edeb5c6 100644 --- a/nanobot/providers/litellm_provider.py +++ b/nanobot/providers/litellm_provider.py @@ -12,9 +12,7 @@ from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest from nanobot.providers.registry import find_by_model, find_gateway -# Keys that are part of the OpenAI chat-completion message schema. -# Anything else (e.g. reasoning_content, timestamp) is stripped before sending -# to avoid "Unrecognized chat message" errors from strict providers like StepFun. +# Standard OpenAI chat-completion message keys; extras (e.g. reasoning_content) are stripped for strict providers. _ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name"}) @@ -155,13 +153,7 @@ class LiteLLMProvider(LLMProvider): @staticmethod def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Strip non-standard keys from messages for strict providers. - - Some providers (e.g. StepFun via OpenRouter) reject messages that - contain extra keys like ``reasoning_content``. This method keeps - only the keys defined in the OpenAI chat-completion schema and - ensures every assistant message has a ``content`` key. - """ + """Strip non-standard keys and ensure assistant messages have a content key.""" sanitized = [] for msg in messages: clean = {k: v for k, v in msg.items() if k in _ALLOWED_MSG_KEYS}