VectifyAI/PageIndex · error · PageIndexAPIError

Each message must be a dict with 'role' and 'content'.

Error message

Each message must be a dict with 'role' and 'content'.

What it means

Each element of the messages list must be a dict containing at least a 'role' key. Strings, tuples, or dicts without 'role' are rejected — this mirror of the OpenAI chat schema is enforced before the model runs.

Source

Thrown at pageindex/local_chat.py:68

                 if isinstance(part, dict) and isinstance(part.get("text"), str)]
        if texts:
            return "\n".join(texts)
    raise PageIndexAPIError(
        "system message content must be a string or a list of text parts."
    )


def _split_chat_messages(messages) -> "tuple[list[str], list[dict]]":
    """Validate the chat_completions surface's messages: system/developer
    content joins the managed instructions; user/assistant history passes
    through. Tool-history round-trips belong to responses()/messages()."""
    if not isinstance(messages, list) or not messages:
        raise PageIndexAPIError("messages must be a non-empty list.")
    system_texts: list[str] = []
    history: list[dict] = []
    for message in messages:
        if not isinstance(message, dict) or "role" not in message:
            raise PageIndexAPIError(
                "Each message must be a dict with 'role' and 'content'.")
        role = message["role"]
        if role in ("system", "developer"):
            system_texts.append(_system_text(message.get("content")))
        elif role in ("user", "assistant"):
            content = message.get("content")
            if not isinstance(content, str):
                raise PageIndexAPIError(
                    "chat_completions content must be a string; for "
                    "structured items use responses() or messages()."
                )
            history.append({"role": role, "content": content})
        else:
            raise PageIndexAPIError(
                f"Unsupported role for chat_completions: {role!r}. Tool "
                "history round-trips belong to responses() or messages()."
            )
    if not history:

View on GitHub (pinned to afb5e11976)

Solutions

  1. Use {"role": ..., "content": ...} dicts for every message
  2. Validate each item: all(isinstance(m, dict) and 'role' in m for m in messages)
  3. If converting from another SDK's format, write an explicit adapter

Example fix

# before
messages=[{"content": "hi"}]

# after
messages=[{"role": "user", "content": "hi"}]
Defensive patterns

Strategy: validation

Validate before calling

for m in messages:
    if not isinstance(m, dict) or 'role' not in m:
        raise ValueError(f'bad message: {m!r}')

Type guard

def is_valid_message(m) -> bool:
    return isinstance(m, dict) and 'role' in m and 'content' in m

Try / catch

null

Prevention

When it happens

Trigger: messages=["hello"], messages=[("user","hi")], or messages=[{"content":"hi"}] (missing role).

Common situations: Hand-building message arrays, JSON payloads with typos ('Role'), converting from Anthropic-style tuples, LLM-generated request bodies with schema drift.

Related errors


AI-assisted analysis of VectifyAI/PageIndex@afb5e11976 (2026-08-27). Data as JSON: /api/errors/4ef83dfddaccdff7. Report an issue: GitHub.