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 content parts of type "image_url", OCI GENERIC needs a URL string. LiteLLM accepts either the OpenAI object form {"image_url":{"url":...}} (unwrapping the url) or a plain string {"image_url":"https://..."}. If after unwrapping the value is not a string (missing url key, None, nested objects), it raises OCIError 400.

Solutions

  1. Use the canonical OpenAI shape: {"type":"image_url","image_url":{"url":"https://... or data:image/png;base64,...."}}
  2. For local images, base64-encode into a data URI: f"data:image/png;base64,{b64str}"
  3. Check the url key actually resolves to a string where you build the part (log the value's type)
  4. Unwrap extra nesting before sending: image_url = part["image_url"]["url"] if isinstance(...) else part["image_url"]

Example fix

# before
content = [
  {"type": "text", "text": "what is this?"},
  {"type": "image_url", "image_url": {"link": "https://x/img.png"}},
]

# after
content = [
  {"type": "text", "text": "what is this?"},
  {"type": "image_url", "image_url": {"url": "https://x/img.png"}},
]
Defensive patterns

Strategy: type-guard

Validate before calling

def coerce_image_parts(content: list) -> list:
    out = []
    for p in content:
        if p.get("type") == "image_url":
            url = p.get("image_url")
            if isinstance(url, dict):
                url = url.get("url")
            p = {**p, "image_url": {"url": url}}
        out.append(p)
    return out

Type guard

def is_valid_image_part(part) -> bool:
    if part.get("type") != "image_url":
        return True  # not this validator's concern
    u = part.get("image_url")
    return isinstance(u, str) or (isinstance(u, dict) and isinstance(u.get("url"), str))

Prevention

When it happens

Trigger: Sending {"type":"image_url","image_url":{}} (empty object), {"image_url":{"data":...}} (wrong key), {"image_url":None}, or a data-URI structured incorrectly to an oci/generic.* model. Raised client-side in adapt_messages_to_generic_oci_standard_content_message.

Common situations: Typos like {"image_url":{"link":...}} instead of url; constructing image parts where the URL comes from user input that is None; passing a bytes/PIL object instead of a base64 data URL; nested wrappers from other SDKs (e.g. {"image_url":{"url":{"href":...}}}).

Related errors


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

Appendix: 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 77b7c6c40c)