Skip to content

Commit bdf21c9

Browse files
committed
fix(anthropic): avoid sanitized tool id collisions
1 parent 4d7c207 commit bdf21c9

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

nanobot/providers/anthropic_provider.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import hashlib
67
import re
78
import secrets
89
import string
@@ -37,7 +38,9 @@ def _sanitize_tool_id(tid: str) -> str:
3738
"""
3839
if not tid or _VALID_TOOL_ID.match(tid):
3940
return tid
40-
return re.sub(r"[^a-zA-Z0-9_-]", "_", tid)
41+
safe_prefix = re.sub(r"[^a-zA-Z0-9_-]", "_", tid)[:48].strip("_") or "toolu"
42+
digest = hashlib.sha1(tid.encode()).hexdigest()[:8]
43+
return f"{safe_prefix}_{digest}"
4144

4245

4346
class AnthropicProvider(LLMProvider):

tests/providers/test_anthropic_tool_result.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,41 @@ def test_convert_assistant_message_repairs_history_tool_arguments():
9494

9595
assert blocks[0]["type"] == "tool_use"
9696
assert blocks[0]["input"] == {"path": "foo.txt"}
97+
98+
99+
def test_anthropic_sanitizes_invalid_tool_ids_consistently():
100+
"""Invalid restored IDs must be valid for Anthropic and keep pairs matched."""
101+
blocks = AnthropicProvider._assistant_blocks({
102+
"role": "assistant",
103+
"content": None,
104+
"tool_calls": [{
105+
"id": "call_abc|rs.same",
106+
"function": {"name": "read_file", "arguments": "{}"},
107+
}],
108+
})
109+
result = AnthropicProvider._tool_result_block({
110+
"role": "tool",
111+
"tool_call_id": "call_abc|rs.same",
112+
"content": "ok",
113+
})
114+
115+
tool_id = blocks[0]["id"]
116+
assert tool_id == result["tool_use_id"]
117+
assert tool_id != "call_abc|rs.same"
118+
assert all(ch.isalnum() or ch in "_-" for ch in tool_id)
119+
120+
121+
def test_anthropic_sanitized_tool_ids_avoid_simple_collisions():
122+
"""Replacement-only sanitizing would collapse these two ids to call_a."""
123+
blocks = AnthropicProvider._assistant_blocks({
124+
"role": "assistant",
125+
"content": None,
126+
"tool_calls": [
127+
{"id": "call.a", "function": {"name": "a", "arguments": "{}"}},
128+
{"id": "call|a", "function": {"name": "b", "arguments": "{}"}},
129+
],
130+
})
131+
132+
ids = [block["id"] for block in blocks if block["type"] == "tool_use"]
133+
assert len(ids) == len(set(ids)) == 2
134+
assert all(all(ch.isalnum() or ch in "_-" for ch in tool_id) for tool_id in ids)

0 commit comments

Comments
 (0)