fix: anthropic tool sanitation (#9310)
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user