BerriAI/litellm · error · OCIError

Prop `image_url` must be a string or an object with a `url`

Error message

Prop `image_url` must be a string or an object with a `url` property

What it means

For image_url content parts, the adapter accepts either a plain string ('image_url': 'https://...') or an OpenAI-style object ('image_url': {'url': '...'}). If after unwrapping the value is not a string, OCIError(400) "Prop `image_url` must be a string or an object with a `url` property" is raised. Notably, an object whose 'url' key is missing or None also fails, because the unwrapped value is not a str.

Source

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

                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],
        content=new_content,
        toolCalls=None,
        toolCallId=None,
    )


def adapt_messages_to_generic_oci_standard_tool_call(role: str, tool_calls: list) -> OCIMessage:
    """Convert an assistant tool-call message to OCI format."""
    tool_calls_formatted: Final = []
    for tool_call in tool_calls:
        if not isinstance(tool_call, dict):

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Use the OpenAI shape: {'type':'image_url','image_url':{'url':'https://...'}} or the shorthand string form.
  2. Ensure the url key exists and is a non-empty string; str() any URL-like objects before building the part.
  3. For base64 images, use a data URL string ('data:image/png;base64,....') as the url value.

Example fix

# before
{'type':'image_url','image_url':{'data': b64_bytes}}

# after
{'type':'image_url','image_url':{'url': f'data:image/png;base64,{b64_str}'}}
Defensive patterns

Strategy: type-guard

Validate before calling

def to_image_part(url_or_obj) -> dict:
    url = url_or_obj.get('url') if isinstance(url_or_obj, dict) else url_or_obj
    assert isinstance(url, str) and url, 'image_url must be a string or {"url": str}'
    return {'type': 'image_url', 'image_url': {'url': url}}

Type guard

def is_valid_image_part(part: object) -> bool:
    if not (isinstance(part, dict) and part.get('type') == 'image_url'):
        return False
    iu = part.get('image_url')
    url = iu.get('url') if isinstance(iu, dict) else iu
    return isinstance(url, str) and bool(url)

Prevention

When it happens

Trigger: Sending {'type':'image_url','image_url':{'data':b64bytes}} (data instead of url), 'image_url': None, or an object where 'url' is missing/None to an oci/ GENERIC model. Also {'image_url': {'url': None}}.

Common situations: Confusing base64 data-field conventions across providers; dicts built from optional values where the url never got populated; passing an httpx.URL or pathlib.Path object instead of a string.

Related errors


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