BerriAI/litellm · error · OCIError

Content item of type `text` must have a string `text` field

Error message

Content item of type `text` must have a string `text` field

What it means

For content parts with type 'text', the OCI GENERIC adapter requires the 'text' field to be a string. A text part whose 'text' is missing, None, a number, or a list raises OCIError(400) "Content item of type `text` must have a string `text` field" before the request is sent.

Source

Thrown at litellm/llms/oci/chat/generic.py:89

        if not isinstance(content_item, dict):
            raise OCIError(status_code=400, message="Each content item must be a dictionary")

        item_type = content_item.get("type")
        if not isinstance(item_type, str):
            raise OCIError(
                status_code=400,
                message="Each content item must have a string `type` field",
            )
        if item_type not in ["text", "image_url"]:
            raise OCIError(
                status_code=400,
                message=f"Content type `{item_type}` is not supported by OCI",
            )

        if item_type == "text":
            text = content_item.get("text")
            if not isinstance(text, str):
                raise OCIError(
                    status_code=400,
                    message="Content item of type `text` must have a string `text` field",
                )
            new_content.append(OCITextContentPart(text=text))

        elif item_type == "image_url":
            image_url = content_item.get("image_url")
            if isinstance(image_url, dict):
                image_url = image_url.get("url")
            if not isinstance(image_url, str):
                raise OCIError(
                    status_code=400,
                    message="Prop `image_url` must be a string or an object with a `url` property",
                )
            new_content.append(OCIImageContentPart(imageUrl=OCIImageUrl(url=image_url)))

    return OCIMessage(
        role=open_ai_to_generic_oci_role_map[role],

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Coerce text values to str and skip empty parts: if v is not None: parts.append({'type':'text','text':str(v)}).
  2. Add a pre-flight assertion that every type=='text' part has isinstance(part['text'], str).
  3. Fix the upstream data source so prompt text is always a plain string.

Example fix

# before
{'type':'text','text': None}

# after
# build only when a value exists
parts = [{'type':'text','text': str(v)} for v in values if v is not None]
Defensive patterns

Strategy: type-guard

Validate before calling

content = [
    {'type': 'text', 'text': str(p['text'])}
    for p in content
    if p.get('type') == 'text' and p.get('text') is not None
]

Type guard

def is_valid_text_part(part: object) -> bool:
    return (
        isinstance(part, dict)
        and part.get('type') == 'text'
        and isinstance(part.get('text'), str)
    )

Prevention

When it happens

Trigger: Sending {'type':'text','text':None} or {'type':'text','text':123} to an oci/ GENERIC model; building parts from template variables where a value can be None; f-string parts accidentally wrapped in a list.

Common situations: Optional prompt fields defaulting to None and still emitted as parts; data from JSON where text is sometimes numeric; a refactor changing text from str to a richer object.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/9d57b85d762ad2da. Report an issue: GitHub.