BerriAI/litellm · error · ValueError

Invalid image_url type: {type(image_url).__name__}. Expected

Error message

Invalid image_url type: {type(image_url).__name__}. Expected str or dict with 'url' field.

What it means

An image_url field that is neither str nor dict (int, list, None, bytes, file object) reaches the token counter, which only understands the two OpenAI schema shapes: a URL string or {'url':..., 'detail':...}. The actual Python type name is included in the message.

Source

Thrown at litellm/litellm_core_utils/token_counter.py:607

            raise ValueError(f"Invalid detail value: {detail}. Expected 'low', 'high', or 'auto'.")
        url: Final = image_url.get("url")
        if not url:
            raise ValueError("Missing required key 'url' in image_url dict.")
        return calculate_img_tokens(
            data=url,
            mode=detail,
            use_default_image_token_count=use_default_image_token_count,
        )
    elif isinstance(image_url, str):
        if not image_url.strip():
            raise ValueError("Empty image_url string is not valid.")
        return calculate_img_tokens(
            data=image_url,
            mode="auto",
            use_default_image_token_count=use_default_image_token_count,
        )
    else:
        raise ValueError(f"Invalid image_url type: {type(image_url).__name__}. Expected str or dict with 'url' field.")


def _validate_anthropic_content(content: Mapping[str, Any]) -> type:
    """
    Validate and determine which Anthropic TypedDict applies.

    Returns the corresponding TypedDict class if recognized, otherwise raises.
    """
    content_type: Final = content.get("type")
    if not content_type:
        raise ValueError("Anthropic content missing required field: 'type'")

    mapping: Final = {
        "tool_use": AnthropicMessagesToolUseParam,
        "tool_result": AnthropicMessagesToolResultParam,
    }

    expected_cls: Final = mapping.get(content_type)

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Convert to one of the two supported shapes: a string URL/data-URI or a dict with 'url'.
  2. For local files, base64-encode into a data URI: "data:image/png;base64," + b64.

Example fix

# before
block = {"type": "image_url", "image_url": file.read()}  # bytes

# after
import base64
b64 = base64.b64encode(file.read()).decode()
block = {"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}}
Defensive patterns

Strategy: type-guard

Validate before calling

import base64

def to_image_block(img):
    if isinstance(img, str):
        return {"type": "image_url", "image_url": img}
    if isinstance(img, (bytes, bytearray)):
        b64 = base64.b64encode(img).decode()
        return {"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}}
    if isinstance(img, dict) and img.get("url"):
        return {"type": "image_url", "image_url": img}
    raise ValueError("unsupported image payload " + type(img).__name__)

Type guard

def is_supported_image_url(value) -> bool:
    return isinstance(value, str) or (isinstance(value, dict) and bool(value.get("url")))

Prevention

When it happens

Trigger: image_url=None (block built but URL never set); image_url=[url] (over-wrapped in a list); raw bytes or file-like objects from an upload handler passed directly into message content.

Common situations: Upload pipelines passing file objects or bytes through; inconsistent wrapping layers (double-wrapping in lists/dicts); None defaults leaking from config.

Related errors


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