{"record":{"id":"ad71ff95351f376c","repo":"zylon-ai/private-gpt","slug":"duplicate-tool-use-id-found-block-id","errorCode":null,"errorMessage":"Duplicate tool use ID found: {block.id}","messagePattern":"Duplicate tool use ID found: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":422,"severity":"error","filePath":"private_gpt/server/chat/chat_models.py","lineNumber":398,"sourceCode":"                    f\" Provided tools: {self.tools}\"\n                    f\" Unique tool names: {set(tool_names)}\"\n                )\n\n        # Check tool use and result blocks\n        tool_uses_ids: set[str] = set()\n        tool_results_ids: set[str] = set()\n        for message in self.messages:\n            if isinstance(message.content, list):\n                for block in message.content:\n                    if block is None:\n                        raise ValueError(\"Block cannot be None\")\n                    elif isinstance(block, ToolUseBlock):\n                        if message.role != \"assistant\":\n                            raise ValueError(\n                                f\"Tool use blocks can only be used in assistant messages: {message}\"\n                            )\n                        if block.id in tool_uses_ids:\n                            raise ValueError(f\"Duplicate tool use ID found: {block.id}\")\n                        tool_uses_ids.add(block.id)\n\n                    elif isinstance(block, ToolResultBlock):\n                        if block.tool_use_id not in tool_uses_ids:\n                            raise ValueError(\n                                f\"Tool result block references an unknown tool use ID: {block.tool_use_id}\"\n                            )\n                        tool_results_ids.add(block.tool_use_id)\n\n                    elif isinstance(block, TLDRBlock):\n                        if message.role != \"assistant\":\n                            raise ValueError(\n                                f\"TLDR blocks can only be used in assistant messages: {message}\"\n                            )\n\n        if tool_results_ids != tool_uses_ids:\n            raise ValueError(\n                \"Tool result blocks must match the tool use IDs in the same message.\"","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/chat/chat_models.py#L380-L416","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Inspect your messages payload and de-duplicate assistant turns containing tool use blocks before sending","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"],"exampleFix":"# before\n{\"role\": \"assistant\", \"content\": [{\"type\": \"tool_use\", \"id\": \"tool_1\", \"name\": \"search\", \"input\": {}}]}\n# ...later another assistant message reuses \"tool_1\"\n\n# after\nimport uuid\nblock_id = str(uuid.uuid4())  # unique per tool_use block","handlingStrategy":"validation","validationCode":"ids = [b.id for m in messages for b in (m.content or []) if getattr(b, \"type\", None) == \"tool_use\"]\nassert len(ids) == len(set(ids)), f\"duplicate tool_use ids: {[i for i in ids if ids.count(i) > 1]}\"","typeGuard":"def has_unique_tool_use_ids(messages: list) -> bool:\n    seen: set[str] = set()\n    for m in messages:\n        for b in (m.get(\"content\") or [] if isinstance(m, dict) else m.content or []):\n            bid = b.get(\"id\") if isinstance(b, dict) else getattr(b, \"id\", None)\n            if (b.get(\"type\") if isinstance(b, dict) else getattr(b, \"type\", None)) == \"tool_use\":\n                if bid in seen:\n                    return False\n                seen.add(bid)\n    return True","tryCatchPattern":"try:\n    chat = ChatMessages(messages=msgs)\nexcept ValueError as e:\n    if \"Duplicate tool use ID\" in str(e):\n        dedupe_tool_use_blocks(msgs)  # regenerate ids\n    else:\n        raise","preventionTips":["Always mint tool_use ids with uuid4","Never copy assistant messages when replaying or forking a conversation","Run a uniqueness check over tool_use ids before each request"],"tags":["chat","tool-use","validation","messages"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}