fix: keep duplicate id repair in Anthropic provider

maintainer edit: move duplicate tool_use history repair out of AgentRunner and into Anthropic message conversion, reusing the OpenAI-compatible queue-mapping approach locally without broadening the shared runner path.
This commit is contained in:
chengyongru
2026-06-24 10:21:48 +08:00
committed by Xubin Ren
parent 853aecdb97
commit 0e9861558a
4 changed files with 109 additions and 149 deletions
+60 -6
View File
@@ -7,6 +7,7 @@ import hashlib
import re
import secrets
import string
from collections import deque
from collections.abc import Awaitable, Callable
from typing import Any
@@ -156,6 +157,42 @@ class AnthropicProvider(LLMProvider):
"""Return ``(system, anthropic_messages)``."""
system: str | list[dict[str, Any]] = ""
raw: list[dict[str, Any]] = []
seen_tool_ids: set[str] = set()
duplicate_counts: dict[str, int] = {}
pending_tool_ids: dict[str, deque[str]] = {}
def unique_tool_id(value: Any) -> str:
raw_id = str(value) if value else _gen_tool_id()
mapped_id = _sanitize_tool_id(raw_id)
if mapped_id and mapped_id not in seen_tool_ids:
seen_tool_ids.add(mapped_id)
if value:
pending_tool_ids.setdefault(str(value), deque()).append(mapped_id)
return mapped_id
seed = mapped_id or _gen_tool_id()
duplicate_counts[seed] = duplicate_counts.get(seed, 1) + 1
suffix = duplicate_counts[seed]
while True:
candidate = f"{seed}__dedupe_{suffix}"
if candidate not in seen_tool_ids:
seen_tool_ids.add(candidate)
if value:
pending_tool_ids.setdefault(str(value), deque()).append(candidate)
return candidate
suffix += 1
def map_tool_result_id(value: Any) -> str:
if not value:
return _sanitize_tool_id(value or "")
raw_id = str(value)
queue = pending_tool_ids.get(raw_id)
if queue:
mapped_id = queue.popleft()
if not queue:
pending_tool_ids.pop(raw_id, None)
return mapped_id
return _sanitize_tool_id(raw_id)
for msg in messages:
role = msg.get("role", "")
@@ -166,7 +203,7 @@ class AnthropicProvider(LLMProvider):
continue
if role == "tool":
block = self._tool_result_block(msg)
block = self._tool_result_block(msg, map_tool_result_id=map_tool_result_id)
if raw and raw[-1]["role"] == "user":
prev_c = raw[-1]["content"]
if isinstance(prev_c, list):
@@ -180,7 +217,10 @@ class AnthropicProvider(LLMProvider):
continue
if role == "assistant":
raw.append({"role": "assistant", "content": self._assistant_blocks(msg)})
raw.append({
"role": "assistant",
"content": self._assistant_blocks(msg, map_tool_id=unique_tool_id),
})
continue
if role == "user":
@@ -193,11 +233,20 @@ class AnthropicProvider(LLMProvider):
return system, self._merge_consecutive(raw)
@staticmethod
def _tool_result_block(msg: dict[str, Any]) -> dict[str, Any]:
def _tool_result_block(
msg: dict[str, Any],
*,
map_tool_result_id: Callable[[Any], str] | None = None,
) -> dict[str, Any]:
content = msg.get("content")
tool_call_id = msg.get("tool_call_id", "")
block: dict[str, Any] = {
"type": "tool_result",
"tool_use_id": _sanitize_tool_id(msg.get("tool_call_id", "")),
"tool_use_id": (
map_tool_result_id(tool_call_id)
if map_tool_result_id is not None
else _sanitize_tool_id(tool_call_id)
),
}
if isinstance(content, list):
block["content"] = AnthropicProvider._convert_user_content(content)
@@ -208,7 +257,11 @@ class AnthropicProvider(LLMProvider):
return block
@staticmethod
def _assistant_blocks(msg: dict[str, Any]) -> list[dict[str, Any]]:
def _assistant_blocks(
msg: dict[str, Any],
*,
map_tool_id: Callable[[Any], str] | None = None,
) -> list[dict[str, Any]]:
blocks: list[dict[str, Any]] = []
content = msg.get("content")
@@ -231,9 +284,10 @@ class AnthropicProvider(LLMProvider):
continue
func = tc.get("function", {})
args = func.get("arguments", "{}")
raw_id = tc.get("id") or _gen_tool_id()
blocks.append({
"type": "tool_use",
"id": _sanitize_tool_id(tc.get("id") or _gen_tool_id()),
"id": map_tool_id(raw_id) if map_tool_id is not None else _sanitize_tool_id(raw_id),
"name": func.get("name", ""),
"input": tool_arguments_object_for_replay(args),
})