diff --git a/nanobot/providers/base.py b/nanobot/providers/base.py index af23a4c..55bd805 100644 --- a/nanobot/providers/base.py +++ b/nanobot/providers/base.py @@ -78,6 +78,12 @@ class LLMProvider(ABC): result.append(clean) continue + if isinstance(content, dict): + clean = dict(msg) + clean["content"] = [content] + result.append(clean) + continue + result.append(msg) return result diff --git a/nanobot/providers/litellm_provider.py b/nanobot/providers/litellm_provider.py index 79277bc..d8d8ace 100644 --- a/nanobot/providers/litellm_provider.py +++ b/nanobot/providers/litellm_provider.py @@ -12,9 +12,9 @@ from litellm import acompletion from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest from nanobot.providers.registry import find_by_model, find_gateway -# Standard OpenAI chat-completion message keys plus reasoning_content for -# thinking-enabled models (Kimi k2.5, DeepSeek-R1, etc.). -_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content", "thinking_blocks"}) +# Standard chat-completion message keys. +_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"}) +_ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"}) _ALNUM = string.ascii_letters + string.digits def _short_tool_id() -> str: @@ -158,11 +158,20 @@ class LiteLLMProvider(LLMProvider): return @staticmethod - def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + def _extra_msg_keys(original_model: str, resolved_model: str) -> frozenset[str]: + """Return provider-specific extra keys to preserve in request messages.""" + spec = find_by_model(original_model) or find_by_model(resolved_model) + if (spec and spec.name == "anthropic") or "claude" in original_model.lower() or resolved_model.startswith("anthropic/"): + return _ANTHROPIC_EXTRA_KEYS + return frozenset() + + @staticmethod + def _sanitize_messages(messages: list[dict[str, Any]], extra_keys: frozenset[str] = frozenset()) -> list[dict[str, Any]]: """Strip non-standard keys and ensure assistant messages have a content key.""" + allowed = _ALLOWED_MSG_KEYS | extra_keys sanitized = [] for msg in messages: - clean = {k: v for k, v in msg.items() if k in _ALLOWED_MSG_KEYS} + clean = {k: v for k, v in msg.items() if k in allowed} # Strict providers require "content" even when assistant only has tool_calls if clean.get("role") == "assistant" and "content" not in clean: clean["content"] = None @@ -193,6 +202,7 @@ class LiteLLMProvider(LLMProvider): """ original_model = model or self.default_model model = self._resolve_model(original_model) + extra_msg_keys = self._extra_msg_keys(original_model, model) if self._supports_cache_control(original_model): messages, tools = self._apply_cache_control(messages, tools) @@ -203,7 +213,7 @@ class LiteLLMProvider(LLMProvider): kwargs: dict[str, Any] = { "model": model, - "messages": self._sanitize_messages(self._sanitize_empty_content(messages)), + "messages": self._sanitize_messages(self._sanitize_empty_content(messages), extra_keys=extra_msg_keys), "max_tokens": max_tokens, "temperature": temperature, }