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
- Flatten list content to a string in the fake: `content="hi"` instead of block lists.
- Extract text from blocks first: `"".join(b["text"] for b in content if b.get("type") == "text")`.
- 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
- Keep fake responses as plain strings.
- Flatten recorded multimodal content blocks before using them in fakes.
- Override `_stream` directly when testing multimodal output shapes.
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
- Expected generate to return a ChatResult, but got {type(chat
- Expected invoke to return an AIMessage, but got {type(messag
- messages list cannot be empty.
- AsyncTextProjection received a non-string final value
- Invalid input type {type(model_input)}. Must be a PromptValu
AI-assisted analysis of langchain-ai/langchain@e32fa9a52e (2026-08-14).
Data as JSON: /api/errors/fcd9536172a0a6f6.
Report an issue: GitHub.