BerriAI/litellm · warning · ValueError
Content must be a string
Error message
Content must be a string
What it means
SAP chat message content is normalized by a Pydantic field validator that accepts a string, a dict with a 'text' key, or a list of strings/text-dicts (joined with newlines). Any other shape - a dict without 'text', None, a number - raises ValueError('Content must be a string').
Source
Thrown at litellm/llms/sap/chat/models.py:24
def validate_different_content(v: str | dict | list) -> str:
if v in ((), {}, []):
return ""
elif isinstance(v, dict) and "text" in v:
return v["text"]
elif isinstance(v, list):
new_v: Final = []
for item in v:
if isinstance(item, dict) and "text" in item:
if item["text"]:
new_v.append(item["text"])
elif isinstance(item, str):
new_v.append(item)
return "\n".join(new_v)
elif isinstance(v, str):
return v
raise ValueError("Content must be a string")
class TextContent(BaseModel):
type_: Literal["text"] = Field(default="text", alias="type")
text: str
class ImageURLContent(BaseModel):
url: str
detail: str = "auto"
class ImageContent(BaseModel):
type_: Literal["image_url"] = Field(default="image_url", alias="type")
image_url: ImageURLContent
class FunctionObj(BaseModel):View on GitHub (pinned to 77b7c6c40c)
Solutions
- Use plain string content: {'role': 'user', 'content': 'hello'}.
- For structured lists, use [{'type': 'text', 'text': 'part1'}, {'text': 'part2'}] - items are joined with newlines.
- Never pass content=None; omit the message or use an empty string.
- For images/multimodal input, use the SAP-specific content schema or a provider that supports it, not the OpenAI image_url block.
Example fix
# before
messages=[{'role': 'user', 'content': {'type': 'image_url', 'image_url': {'url': '...'}}}]
# after (SAP accepts text or text parts)
messages=[{'role': 'user', 'content': 'describe this: <url>'}] Defensive patterns
Strategy: type-guard
Validate before calling
def normalize_sap_content(content) -> str:
if content is None:
return ''
if isinstance(content, str):
return content
if isinstance(content, dict):
return content.get('text', '') or ''
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict) and item.get('text'):
parts.append(item['text'])
elif isinstance(item, str):
parts.append(item)
return '\n'.join(parts)
return str(content) Type guard
from typing import Any
def is_valid_sap_content(v: Any) -> bool:
if isinstance(v, str):
return True
if isinstance(v, dict):
return 'text' in v
if isinstance(v, list):
return all(isinstance(i, str) or (isinstance(i, dict) and 'text' in i) for i in v)
return False Prevention
- Pass plain strings for SAP chat content unless you need multi-part text.
- Strip or convert OpenAI multimodal content blocks before forwarding to sap/ models.
When it happens
Trigger: Passing messages=[{'role': 'user', 'content': {'type': 'image_url', ...}}] (dict lacking 'text'), content=None, or numeric content to litellm.completion(model='sap/...'). Non-text parts are silently dropped rather than raising only when they carry a 'text' key; everything else fails here.
Common situations: Forwarding OpenAI-style multimodal content blocks to the SAP provider expecting text-only; optional fields where code passes content=None for system messages; data pipelines where content is sometimes a float after JSON mangling.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Cannot specify both maxChunkCount and maxDocumentCount.
- For SAP Masking Module Config you must provide 'providers'.
- For SAP Masking Module Config you must set exactly one of: '
- For using SAP Filtering Module you must provide at least one
- TranslationModuleConfig requires at least one of 'input' or
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/34d63647af8aa58f.
Report an issue: GitHub.