zylon-ai/private-gpt · error · ValueError
Tool use blocks can only be used in assistant messages: {mes
Error message
Tool use blocks can only be used in assistant messages: {message} What it means
ChatBody enforces Anthropic-style tool semantics: ToolUseBlock entries may only appear in assistant messages. A tool_use block in a user (or system/tool) message is rejected with the whole message printed, because tool calls originate from the model (assistant), never from the caller.
Source
Thrown at private_gpt/server/chat/chat_models.py:394
tool_names = [tool.name for tool in self.tools]
if len(tool_names) != len(set(tool_names)):
raise ValueError(
"Duplicate tool names found in the tools list."
f" Provided tools: {self.tools}"
f" Unique tool names: {set(tool_names)}"
)
# Check tool use and result blocks
tool_uses_ids: set[str] = set()
tool_results_ids: set[str] = set()
for message in self.messages:
if isinstance(message.content, list):
for block in message.content:
if block is None:
raise ValueError("Block cannot be None")
elif isinstance(block, ToolUseBlock):
if message.role != "assistant":
raise ValueError(
f"Tool use blocks can only be used in assistant messages: {message}"
)
if block.id in tool_uses_ids:
raise ValueError(f"Duplicate tool use ID found: {block.id}")
tool_uses_ids.add(block.id)
elif isinstance(block, ToolResultBlock):
if block.tool_use_id not in tool_uses_ids:
raise ValueError(
f"Tool result block references an unknown tool use ID: {block.tool_use_id}"
)
tool_results_ids.add(block.tool_use_id)
elif isinstance(block, TLDRBlock):
if message.role != "assistant":
raise ValueError(
f"TLDR blocks can only be used in assistant messages: {message}"
)View on GitHub (pinned to 4a030776a3)
Solutions
- Keep ToolUseBlock only in assistant messages; respond to it with a user message containing ToolResultBlock referencing block.tool_use_id.
- When replaying history, verify each tool_use block's parent message has role='assistant'.
- Use the SDK's message-conversion helpers rather than manual block placement.
Example fix
# before
{"role":"user","content":[{"type":"tool_use","id":"t1","name":"search","input":{}}]}
# after
{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"search","input":{}}]},
{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]} Defensive patterns
Strategy: type-guard
Validate before calling
for m in messages:
if m['role'] != 'assistant':
assert not any(getattr(b, 'type', None) == 'tool_use' for b in m.get('content', []) if isinstance(b, dict)), \
'tool_use blocks belong in assistant messages only' Type guard
def tool_use_roles_valid(msgs: list[dict]) -> bool:
return all(
m['role'] == 'assistant'
for m in msgs
for b in (m.get('content') or [])
if isinstance(b, dict) and b.get('type') == 'tool_use'
) Try / catch
except ValidationError as e:
if 'Tool use blocks can only' in str(e):
move_tool_use_to_assistant(messages); retry()
else:
raise Prevention
- Use SDK helpers to build tool-loop messages instead of manual blocks
- Answer tool_use with ToolResultBlock in the next user turn
- Replay-logged tool conversations through a role validator before resending
When it happens
Trigger: Echoing the assistant's tool_use block back in a user message (instead of a matching ToolResultBlock); clients that copy the full assistant turn including tool_use into the user turn; role mislabeling when replaying tool transcripts.
Common situations: Hand-rolled tool loops that mis-model the protocol; converting OpenAI-style 'tool' role messages to this API's block format; replaying logged conversations where roles were flattened.
Related errors
- TOOL_NAME_CONFLICT
- mistral-common only supports function tools.
- Database query tool requires at least one SQL database artif
- Semantic search tool requires an ingested artifact context.
- Only one ingested context is supported.
AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15).
Data as JSON: /api/errors/02622b8750243101.
Report an issue: GitHub.