BerriAI/litellm · error · Exception

Prop ` ` is not a string

Error message

Prop `{value_name}` is not a string

What it means

Each content part's value is fetched by the mapped value_name ('text' for text parts, 'url' for image_url/input_audio/video_url); if missing or falsy this Exception names the expected prop. Crucially, Bytez expects a flattened shape: {"type": "image_url", "url": ...}, not OpenAI's nested {"type": "image_url", "image_url": {"url": ...}} — so standard OpenAI vision/audio parts hit this error because the top-level 'url' key doesn't exist.

Solutions

  1. Flatten multimodal parts for bytez: use {"type": "image_url", "url": "https://..."} instead of the nested image_url object.
  2. Ensure every part carries its value: text parts need non-empty 'text'; image/audio/video parts need non-empty 'url'.
  3. Guard templates so an empty variable skips the part entirely rather than sending a valueless part.

Example fix

# before (OpenAI-style nested part -> raises Prop `url` is not a string)
messages = [{
    "role": "user",
    "content": [
        {"type": "image_url", "image_url": {"url": "https://example.com/cat.png"}},
        {"type": "text", "text": "describe"},
    ],
}]

# after (flattened for Bytez)
messages = [{
    "role": "user",
    "content": [
        {"type": "image_url", "url": "https://example.com/cat.png"},
        {"type": "text", "text": "describe"},
    ],
}]
Defensive patterns

Strategy: validation

Validate before calling

def flatten_parts_for_bytez(content: list) -> list:
    out = []
    for p in content:
        t = p.get("type")
        if t == "text" and p.get("text"):
            out.append({"type": "text", "text": p["text"]})
        elif t == "image_url":
            url = p.get("url") or p.get("image_url", {}).get("url")
            if url:
                out.append({"type": "image_url", "url": url})
    return out

Type guard

def is_valid_bytez_part(part: object) -> bool:
    if not isinstance(part, dict):
        return False
    t = part.get("type")
    if t == "text":
        return isinstance(part.get("text"), str) and bool(part["text"])
    if t in ("image_url", "input_audio", "video_url"):
        return isinstance(part.get("url"), str) and bool(part["url"])
    return False

Try / catch

try:
    litellm.completion(model="bytez/org/model", messages=messages)
except Exception as e:
    if "is not a string" in str(e):
        messages = [{"role": m["role"], "content": flatten_parts_for_bytez(m["content"])} for m in messages]
        litellm.completion(model="bytez/org/model", messages=messages)
    else:
        raise

Prevention

When it happens

Trigger: Sending canonical OpenAI multimodal parts (nested image_url object, or input_audio with data/format) to a bytez model; a text part missing its text (e.g. {"type": "text"}); empty-string url/text values.

Common situations: Reusing vision prompts written for OpenAI/other providers verbatim on bytez; templating that omits the url field when a variable is empty; audio parts built with 'data' instead of 'url'.

Related errors


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

Appendix: source

Thrown at litellm/llms/bytez/chat/transformation.py:407

        for content_item in content:
            type: str | None = content_item.get("type")

            if not type:
                raise Exception("Prop `type` is not a string")

            content_item_map = open_ai_to_bytez_content_item_map[type]

            if not content_item_map:
                raise Exception(f"Prop `{type}` is not supported")

            new_type = content_item_map["type"]

            value_name = content_item_map["value_name"]

            value: str | None = content_item.get(value_name)

            if not value:
                raise Exception(f"Prop `{value_name}` is not a string")

            new_content.append({"type": new_type, value_name: value})

        new_messages.append({"role": role, "content": new_content})

    return new_messages


# "content": "The cat ran so fast"
# becomes
# "content": [{"type": "text", "text": "The cat ran so fast"}]
def _adapt_string_only_content_to_lists(messages: list[dict]):
    new_messages: Final = []

    for message in messages:
        role = message.get("role")
        content = message.get("content")

View on GitHub (pinned to 77b7c6c40c)