refactor(providers): move empty content sanitization to base class
This commit is contained in:
@@ -40,6 +40,46 @@ class LLMProvider(ABC):
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.api_base = api_base
|
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
|
@abstractmethod
|
||||||
async def chat(
|
async def chat(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class CustomProvider(LLMProvider):
|
|||||||
model: str | None = None, max_tokens: int = 4096, temperature: float = 0.7) -> LLMResponse:
|
model: str | None = None, max_tokens: int = 4096, temperature: float = 0.7) -> LLMResponse:
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
"model": model or self.default_model,
|
"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),
|
"max_tokens": max(1, max_tokens),
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
@@ -50,46 +50,3 @@ class CustomProvider(LLMProvider):
|
|||||||
def get_default_model(self) -> str:
|
def get_default_model(self) -> str:
|
||||||
return self.default_model
|
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
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class LiteLLMProvider(LLMProvider):
|
|||||||
|
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": self._sanitize_messages(messages),
|
"messages": self._sanitize_messages(self._sanitize_empty_content(messages)),
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": max_tokens,
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user