fix: anthropic tool sanitation (#9310)

This commit is contained in:
Ari Webb
2026-02-05 16:31:12 -08:00
committed by Caren Thomas
parent 6f746c5225
commit 85ee7ed7b4
3 changed files with 33 additions and 4 deletions

View File

@@ -524,7 +524,17 @@ def openai_chat_completions_request(
log_event(name="llm_request_sent", attributes=data)
chat_completion = client.chat.completions.create(**data)
log_event(name="llm_response_received", attributes=chat_completion.model_dump())
return ChatCompletionResponse(**chat_completion.model_dump())
response = ChatCompletionResponse(**chat_completion.model_dump())
# Override tool_call IDs to ensure cross-provider compatibility (matches streaming path behavior)
# Some models (e.g. Kimi via OpenRouter) generate IDs like 'Read:93' which violate Anthropic's pattern
for choice in response.choices:
if choice.message.tool_calls:
for tool_call in choice.message.tool_calls:
if tool_call.id is not None:
tool_call.id = get_tool_call_id()
return response
def prepare_openai_payload(chat_completion_request: ChatCompletionRequest):

View File

@@ -64,7 +64,7 @@ from letta.schemas.letta_message_content import (
get_letta_message_content_union_str_json_schema,
)
from letta.system import unpack_message
from letta.utils import parse_json, validate_function_response
from letta.utils import parse_json, sanitize_tool_call_id, validate_function_response
def truncate_tool_return(content: Optional[str], limit: Optional[int]) -> Optional[str]:
@@ -1973,7 +1973,7 @@ class Message(BaseMessage):
content.append(
{
"type": "tool_use",
"id": tool_call.id,
"id": sanitize_tool_call_id(tool_call.id),
"name": tool_call.function.name,
"input": tool_call_input,
}
@@ -2017,7 +2017,7 @@ class Message(BaseMessage):
content.append(
{
"type": "tool_result",
"tool_use_id": resolved_tool_call_id,
"tool_use_id": sanitize_tool_call_id(resolved_tool_call_id),
"content": tool_result_content,
}
)

View File

@@ -491,6 +491,25 @@ def get_tool_call_id() -> str:
return str(uuid.uuid4())[:TOOL_CALL_ID_MAX_LEN]
# Pattern for valid tool_call_id (required by Anthropic: ^[a-zA-Z0-9_-]+$)
TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
def sanitize_tool_call_id(tool_id: str) -> str:
"""Ensure tool_call_id matches cross-provider requirements:
- Anthropic: pattern ^[a-zA-Z0-9_-]+$
- OpenAI: max length 29 characters
Some models (e.g. Kimi via OpenRouter) generate IDs like 'Read:93' which
contain invalid characters. This sanitizes them for cross-provider compatibility.
"""
# Replace invalid characters with underscores
if not TOOL_CALL_ID_PATTERN.match(tool_id):
tool_id = re.sub(r"[^a-zA-Z0-9_-]", "_", tool_id)
# Truncate to max length
return tool_id[:TOOL_CALL_ID_MAX_LEN]
def assistant_function_to_tool(assistant_message: dict) -> dict:
assert "function_call" in assistant_message
new_msg = copy.deepcopy(assistant_message)