BerriAI/litellm · error · ValueError

Voyage multimodal embeddings require a non-empty `image_url`

Error message

Voyage multimodal embeddings require a non-empty `image_url`. Got an image content block without a `url`.

What it means

When normalizing multimodal input, VoyageMultimodalEmbeddingConfig._normalize_content_item expects an image content block ({"type": "image_url", "image_url": {..."url": ...}} or {"type": "image_url", "image_url": "https://..."}) to carry a URL. If the url field is absent or None, this ValueError is raised client-side before any HTTP call. data:image/...;base64 URLs are converted to image_base64 blocks automatically.

Source

Thrown at litellm/llms/voyage/embedding/transformation_multimodal.py:108

                or get_secret_str("VOYAGE_AI_API_KEY")
                or get_secret_str("VOYAGE_AI_TOKEN")
            )
        if not api_key:
            raise ValueError(
                "Voyage API key is required for multimodal embeddings. "
                "Set VOYAGE_API_KEY / VOYAGE_AI_API_KEY / VOYAGE_AI_TOKEN "
                "or pass `api_key` explicitly."
            )
        return {"Authorization": f"Bearer {api_key}"}

    def _normalize_content_item(self, item: dict[str, Any]) -> dict[str, Any]:
        item_type: Final = item.get("type")
        if item_type == "image_url":
            image_url = item.get("image_url")
            if isinstance(image_url, dict):
                image_url = image_url.get("url")
            if image_url is None:
                raise ValueError(
                    "Voyage multimodal embeddings require a non-empty `image_url`. "
                    "Got an image content block without a `url`."
                )
            if isinstance(image_url, str) and image_url.startswith("data:image/"):
                _, _, encoded = image_url.partition(",")
                return {"type": "image_base64", "image_base64": encoded}
            return {"type": "image_url", "image_url": image_url}
        return item

    def _normalize_input_item(self, item: Any) -> dict[str, Any]:
        if isinstance(item, str):
            return {"content": [{"type": "text", "text": item}]}
        if isinstance(item, dict) and "content" in item:
            content: Final = item.get("content") or []
            return {
                **item,
                "content": [self._normalize_content_item(content_item) for content_item in content],
            }

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Ensure every image content block has a url: {"type": "image_url", "image_url": {"url": "https://..."}} or {"type": "image_url", "image_url": "https://..."}.
  2. Filter or reject malformed blocks before calling litellm.embedding.
  3. For local files, convert to a data URL: data:image/png;base64,<b64> - it will be base64-encoded into the request.
  4. Add a schema check (pydantic model or manual guard) on the input array before sending.

Example fix

# before
inputs = [{"content": [{"type": "image_url", "image_url": {"url": None}}]}]
resp = litellm.embedding(model="voyage-3-multimodal", input=inputs)
# -> ValueError: ...require a non-empty `image_url`...

# after
inputs = [{"content": [{"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}}]}]
resp = litellm.embedding(model="voyage-3-multimodal", input=inputs)
Defensive patterns

Strategy: validation

Validate before calling

def valid_voyage_multimodal_input(items: list) -> bool:
    for item in items:
        for block in item.get("content", []):
            if block.get("type") == "image_url":
                url = block.get("image_url")
                url = url.get("url") if isinstance(url, dict) else url
                if not url:
                    return False
    return True

if not valid_voyage_multimodal_input(inputs):
    raise ValueError("every image_url block needs a non-empty url")
resp = litellm.embedding(model="voyage-3-multimodal", input=inputs)

Type guard

type ImageBlock = { type: "image_url"; image_url: string | { url: string } };

const hasValidImageUrl = (b: unknown): b is ImageBlock => {
  if (typeof b !== "object" || b === null || (b as any).type !== "image_url") return false;
  const u = (b as any).image_url;
  const url = typeof u === "string" ? u : u?.url;
  return typeof url === "string" && url.length > 0;
};

Try / catch

try:
    resp = litellm.embedding(model="voyage-3-multimodal", input=inputs)
except ValueError as e:
    if "non-empty `image_url`" in str(e):
        # drop or repair malformed blocks, then retry
        inputs = repair_or_drop_image_blocks(inputs)
        resp = litellm.embedding(model="voyage-3-multimodal", input=inputs)
    else:
        raise

Prevention

When it happens

Trigger: Passing {"type": "image_url", "image_url": {}} or {"type": "image_url"} as a content item; building image blocks from an upstream field that was None/missing (e.g. an API response without a thumbnail); confusing the OpenAI shape where image_url is a dict containing "url" with a bare string here.

Common situations: Integrating user uploads where the URL key is sometimes absent; LLM-produced tool-call payloads with incomplete image blocks; schema drift between an internal Media type and OpenAI-style content parts.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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