zylon-ai/private-gpt · error · ValueError
Tool result blocks must match the tool use IDs in the same…
Error message
Tool result blocks must match the tool use IDs in the same message. Found tool use IDs: {tool_uses_ids}, but tool result IDs: {tool_results_ids} What it means
Final consistency check of the ChatMessages validator: after scanning all messages, the set of tool_use ids and the set of tool_result ids must be identical — every tool call must have exactly one result within the same request. The error lists both sets so you can see which id is missing a result (present in tool uses but not results) or which tool_use was never paired.
Solutions
- Append a ToolResultBlock for every ToolUseBlock id shown in 'Found tool use IDs' but missing from 'tool result IDs' (even a synthetic 'skipped' result)
- Prune history on tool_use/tool_result pair boundaries so pairing always stays complete
- Never submit the assistant tool-call turn before the tool result is available — send both in the same request
Example fix
# before (tool_use tu_1 has no result)
[assistant(tu_1), user("thanks")]
# after
[assistant(tu_1), user(tool_result tu_1 = "skipped"), user("thanks")] Defensive patterns
Strategy: validation
Validate before calling
use_ids = {b.id for m in messages for b in (m.content or []) if isinstance(b, ToolUseBlock)}
result_ids = {b.tool_use_id for m in messages for b in (m.content or []) if isinstance(b, ToolResultBlock)}
if use_ids != result_ids:
missing_results = use_ids - result_ids
for uid in missing_results:
append_synthetic_tool_result(uid, "skipped") Type guard
def tool_pairs_are_complete(messages: list) -> bool:
use_ids, result_ids = set(), set()
for m in messages:
for b in (m.get("content", []) if isinstance(m, dict) else (m.content or [])):
t = b.get("type") if isinstance(b, dict) else getattr(b, "type", None)
if t == "tool_use":
use_ids.add(b.get("id") if isinstance(b, dict) else b.id)
if t == "tool_result":
result_ids.add(b.get("tool_use_id") if isinstance(b, dict) else b.tool_use_id)
return use_ids == result_ids Try / catch
try:
ChatMessages(messages=msgs)
except ValueError as e:
if "must match the tool use IDs" in str(e):
backfill_missing_tool_results(msgs)
else:
raise Prevention
- Prune history in tool_use+tool_result pairs, never singly
- Always submit the result turn together with the pending tool call
- Diff the two id sets client-side before each send
When it happens
Trigger: An assistant ToolUseBlock exists but no ToolResultBlock with that id appears anywhere in the messages (result turn was truncated, still pending, or omitted); or the pairing check fires after passing the earlier per-block checks because ids match individually but one direction is incomplete.
Common situations: Truncating history from the tail and cutting the last tool_result; sending an in-flight conversation where the tool result has not been appended yet; hand-crafted histories that include the assistant tool call but forget the result turn.
Related errors
- Duplicate tool use ID found
- Tool result block references an unknown tool use ID
- No user message found in request.
- INVALID_REQUEST_AUDIO_MAX_NUM_ERROR
- REQUEST_TOO_LARGE_USER_MSG
AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15).
Data as JSON: /api/errors/14b178423f93f981.
Report an issue: GitHub.
Appendix: source
Thrown at private_gpt/server/chat/chat_models.py:415
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."
f" Found tool use IDs: {tool_uses_ids}, but tool result IDs: {tool_results_ids}"
)
return self
View on GitHub (pinned to 4a030776a3)