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
- Use the canonical OpenAI shape: {"type":"image_url","image_url":{"url":"https://... or data:image/png;base64,...."}}
- For local images, base64-encode into a data URI: f"data:image/png;base64,{b64str}"
- Check the url key actually resolves to a string where you build the part (log the value's type)
- 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
- Encode local images with a helper that always returns {"image_url": {"url": data_uri}}
- Reject None/bytes image sources at your input boundary with a clear message
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
- Content type ` ` is not supported by OCI
- Content item of type `text` must have a string `text` field
- Each content item must be a dictionary
- Each content item must have a string `type` field
- Each tool call must be a dictionary
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)