VectifyAI/PageIndex · error · PageIndexAPIError

chat_completions content must be a string; for structured it

Error message

chat_completions content must be a string; for structured items use responses() or messages().

What it means

On the chat_completions surface, user/assistant message content must be a plain string. Structured content (lists of parts, tool calls) is intentionally rejected — those round-trips belong to the responses() or messages() surfaces which understand item dicts.

Source

Thrown at pageindex/local_chat.py:76

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:
        raise PageIndexAPIError("messages must contain a user or assistant "
                                "message.")
    return system_texts, history


def _run_sync(coro):
    from .utils import run_off_loop
    return run_off_loop(asyncio.run, coro)

View on GitHub (pinned to afb5e11976)

Solutions

  1. Flatten content parts to a string: "\n".join(p["text"] for p in content)
  2. Use client.responses(...) or client.messages(...) when history contains structured items or tool calls
  3. Strip tool_call entries from history before chat_completions

Example fix

# before
msgs=[{"role":"user","content":[{"type":"text","text":"hi"}]}]
client.chat_completions(model=m, messages=msgs)

# after
msgs=[{"role":"user","content":"hi"}]
client.chat_completions(model=m, messages=msgs)
Defensive patterns

Strategy: validation

Validate before calling

def flatten(content):
    if isinstance(content, str): return content
    if isinstance(content, list): return "\n".join(p.get('text','') for p in content if isinstance(p, dict))
    return content
messages = [{**m, 'content': flatten(m['content'])} for m in messages]

Type guard

def has_string_content(m) -> bool:
    c = m.get('content')
    return c is None or isinstance(c, str)

Try / catch

null

Prevention

When it happens

Trigger: Passing {"role":"user","content":[{"type":"text","text":"hi"}]} or assistant messages containing tool_calls to chat_completions.

Common situations: Replaying captured OpenAI request bodies that use content-part arrays, mixing surfaces when porting between chat_completions and responses, feeding back assistant output with structured parts.

Related errors


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