BerriAI/litellm · error · ValueError

Invalid image URL

Error message

Invalid image URL: {content_image_url}

What it means

While converting multimodal chat content to Responses API input, an image_url content block yielded no usable URL: image_url was neither a plain string nor a dict containing a 'url' key (or the url value was None). The Responses API requires input_image.image_url to be an actual URL string, so the bridge rejects the block up front with the offending value echoed in the message.

Solutions

  1. Ensure every image block is {"type":"image_url","image_url":{"url":"https://..." or "data:image/png;base64,..."}}
  2. Validate image blocks in a preflight loop and drop or reject blocks without a non-empty url string
  3. When converting from other formats, map the source's URL/base64 field into the url key explicitly
  4. Add optional detail only as image_url.detail — never in place of url

Example fix

// before
messages=[{"role":"user","content":[
  {"type": "image_url", "image_url": {}},  # ValueError: Invalid image URL: {}
  {"type": "text", "text": "what is this?"}]}]

// after
messages=[{"role":"user","content":[
  {"type": "image_url", "image_url": {"url": "https://example.com/cat.png", "detail": "auto"}},
  {"type": "text", "text": "what is this?"}]}]
Defensive patterns

Strategy: validation

Validate before calling

def image_blocks_are_valid(messages) -> bool:
    for msg in messages or []:
        content = msg.get("content")
        if not isinstance(content, list):
            continue
        for block in content:
            if isinstance(block, dict) and block.get("type") == "image_url":
                iu = block.get("image_url")
                url = iu if isinstance(iu, str) else (iu or {}).get("url") if isinstance(iu, dict) else None
                if not isinstance(url, str) or not url:
                    return False
    return True

Type guard

def is_valid_image_block(block: dict) -> bool:
    iu = block.get("image_url")
    return isinstance(iu, str) or (isinstance(iu, dict) and isinstance(iu.get("url"), str) and bool(iu["url"]))

Try / catch

try:
    resp = litellm.completion(model=m, messages=messages)
except ValueError as e:
    if "Invalid image URL" in str(e):
        cleaned = [sanitize_image_blocks(msg) for msg in messages]  # drop/repair bad blocks
        resp = litellm.completion(model=m, messages=cleaned)
    else:
        raise

Prevention

When it happens

Trigger: messages with {"type":"image_url","image_url":{}} (empty object); image_url key missing or None; url misspelled or nested one level wrong (e.g. passing {"image_url": {"image_url": {...}}}); image blocks produced by another SDK's schema and forwarded unconverted; content where the URL failed to serialize.

Common situations: Multimodal agent loops replaying stored conversations where the URL field was dropped; converting Anthropic-style image blocks (source/base64) to OpenAI format incompletely; frontend forms submitting empty image fields; base64 data URLs built with a typo in the key.

Related errors


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

Appendix: source

Thrown at litellm/completion_extras/litellm_responses_transformation/transformation.py:834

        self, content: "ChatCompletionImageObject", role: str
    ) -> "ResponseInputImageParam":
        from openai.types.responses import ResponseInputImageParam

        content_image_url: Final = content.get("image_url")
        actual_image_url: str | None = None
        detail: Literal["low", "high", "auto"] | None = None

        if isinstance(content_image_url, str):
            actual_image_url = content_image_url
        elif isinstance(content_image_url, dict):
            actual_image_url = content_image_url.get("url")
            detail = cast(
                Literal["low", "high", "auto"] | None,
                content_image_url.get("detail"),
            )

        if actual_image_url is None:
            raise ValueError(f"Invalid image URL: {content_image_url}")

        image_param: Final = ResponseInputImageParam(image_url=actual_image_url, detail="auto", type="input_image")

        if detail:
            image_param["detail"] = detail

        return image_param

    def _convert_content_to_responses_format(
        self,
        content: str
        | list[object]
        | Iterable[
            Union["OpenAIMessageContentListBlock", "ChatCompletionThinkingBlock", "ChatCompletionRedactedThinkingBlock"]
        ]
        | None,
        role: str,
    ) -> list[dict[str, object]]:

View on GitHub (pinned to 77b7c6c40c)