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

  1. Keep ToolUseBlock only in assistant messages; respond to it with a user message containing ToolResultBlock referencing block.tool_use_id.
  2. When replaying history, verify each tool_use block's parent message has role='assistant'.
  3. 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

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


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/02622b8750243101. Report an issue: GitHub.