langchain-ai/langchain · error · ValueError

Expected content to be a string.

Error message

Expected content to be a string.

What it means

`ValueError` in the fake chat model streaming bridge: the produced `AIMessage.content` is non-empty but not a `str` (e.g. a list of content blocks). The tokenizer split `re.split(r"(\s)", content)` requires plain string content.

Source

Thrown at libs/core/langchain_core/language_models/fake_chat_models.py:299

            raise ValueError(msg)  # noqa: TRY004

        message = chat_result.generations[0].message

        if not isinstance(message, AIMessage):
            msg = (
                f"Expected invoke to return an AIMessage, "
                f"but got {type(message)} instead."
            )
            raise ValueError(msg)  # noqa: TRY004

        content = message.content

        if content:
            # Use a regular expression to split on whitespace with a capture group
            # so that we can preserve the whitespace in the output.
            if not isinstance(content, str):
                msg = "Expected content to be a string."
                raise ValueError(msg)

            content_chunks = cast("list[str]", re.split(r"(\s)", content))

            for idx, token in enumerate(content_chunks):
                chunk = ChatGenerationChunk(
                    message=AIMessageChunk(content=token, id=message.id)
                )
                if (
                    idx == len(content_chunks) - 1
                    and isinstance(chunk.message, AIMessageChunk)
                    and not message.additional_kwargs
                ):
                    chunk.message.chunk_position = "last"
                if run_manager:
                    run_manager.on_llm_new_token(token, chunk=chunk)
                yield chunk

        if message.additional_kwargs:

View on GitHub (pinned to e32fa9a52e)

Solutions

  1. Flatten list content to a string in the fake: `content="hi"` instead of block lists.
  2. Extract text from blocks first: `"".join(b["text"] for b in content if b.get("type") == "text")`.
  3. Or bypass the fake streaming bridge by overriding `_stream` directly for multimodal fixtures.

Example fix

# before
return ChatResult(generations=[ChatGeneration(message=AIMessage(content=[{"type":"text","text":"hi"}]))])

# after
return ChatResult(generations=[ChatGeneration(message=AIMessage(content="hi"))])
Defensive patterns

Strategy: type-guard

Validate before calling

content = fake._generate(messages).generations[0].message.content
if content and not isinstance(content, str):
    content = "".join(b.get("text", "") for b in content if isinstance(b, dict))

Type guard

def is_string_content(content: object) -> bool:
    return not content or isinstance(content, str)

Try / catch

try:
    list(fake.stream(messages))
except ValueError as e:
    if "Expected content to be a string" in str(e):
        raise TypeError("flatten multimodal content in the fake before streaming") from e
    raise

Prevention

When it happens

Trigger: A fake `_generate` returning `AIMessage(content=[{"type": "text", "text": "hi"}])` or other multimodal list content, then streaming from the fake.

Common situations: Recording real multimodal provider responses into test fixtures and replaying them through a fake; constructing fakes from Anthropic/OpenAI content-block formats.

Related errors


AI-assisted analysis of langchain-ai/langchain@e32fa9a52e (2026-08-14). Data as JSON: /api/errors/fcd9536172a0a6f6. Report an issue: GitHub.