zylon-ai/private-gpt · error · ValueError

Duplicate tool use ID found: {block.id}

Error message

Duplicate tool use ID found: {block.id}

What it means

Raised by the ChatMessages validator while scanning message content blocks: a ToolUseBlock was found whose id has already been registered by an earlier ToolUseBlock in the same conversation. Tool use IDs must be globally unique across the whole message list because tool result blocks reference them by id; a duplicate makes the tool-call/result pairing ambiguous, so validation fails fast in the model validator (it returns self, i.e. it runs inside Pydantic validation of ChatMessages).

Source

Thrown at private_gpt/server/chat/chat_models.py:398

                    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}"
                            )

        if tool_results_ids != tool_uses_ids:
            raise ValueError(
                "Tool result blocks must match the tool use IDs in the same message."

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Generate a fresh unique id (e.g. uuid4) for every ToolUseBlock you send, or reuse the exact id the provider returned without duplicating the block
  2. Inspect your messages payload and de-duplicate assistant turns containing tool use blocks before sending
  3. If you build history from provider events, keep the 1:1 mapping between tool_use id and the original assistant call instead of regenerating ids

Example fix

# before
{"role": "assistant", "content": [{"type": "tool_use", "id": "tool_1", "name": "search", "input": {}}]}
# ...later another assistant message reuses "tool_1"

# after
import uuid
block_id = str(uuid.uuid4())  # unique per tool_use block
Defensive patterns

Strategy: validation

Validate before calling

ids = [b.id for m in messages for b in (m.content or []) if getattr(b, "type", None) == "tool_use"]
assert len(ids) == len(set(ids)), f"duplicate tool_use ids: {[i for i in ids if ids.count(i) > 1]}"

Type guard

def has_unique_tool_use_ids(messages: list) -> bool:
    seen: set[str] = set()
    for m in messages:
        for b in (m.get("content") or [] if isinstance(m, dict) else m.content or []):
            bid = b.get("id") if isinstance(b, dict) else getattr(b, "id", None)
            if (b.get("type") if isinstance(b, dict) else getattr(b, "type", None)) == "tool_use":
                if bid in seen:
                    return False
                seen.add(bid)
    return True

Try / catch

try:
    chat = ChatMessages(messages=msgs)
except ValueError as e:
    if "Duplicate tool use ID" in str(e):
        dedupe_tool_use_blocks(msgs)  # regenerate ids
    else:
        raise

Prevention

When it happens

Trigger: POSTing a chat request whose messages array contains two assistant ToolUseBlocks with the same block.id (e.g. copy-pasted history, or a client that fabricates fixed IDs like 'tool_1' for every call). Also happens when replaying a persisted conversation and duplicating an assistant turn.

Common situations: Client SDKs that hardcode tool_use ids; history trimming/merging code that accidentally duplicates an assistant message; importing conversations from another system that re-uses provider call ids.

Related errors


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