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

  1. Use plain string content: {'role': 'user', 'content': 'hello'}.
  2. For structured lists, use [{'type': 'text', 'text': 'part1'}, {'text': 'part2'}] - items are joined with newlines.
  3. Never pass content=None; omit the message or use an empty string.
  4. 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

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


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/34d63647af8aa58f. Report an issue: GitHub.