Skip to content

Commit 5548b4f

Browse files
jnjpngLetta Bot
andauthored
fix: mcp tool schema formatting for anthropic streaming [LET-4165]
* fix * update commnet --------- Co-authored-by: Letta Bot <[email protected]>
1 parent d242620 commit 5548b4f

File tree

1 file changed

+60
-1
lines changed

1 file changed

+60
-1
lines changed

letta/llm_api/anthropic_client.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,16 +618,75 @@ def convert_tools_to_anthropic_format(tools: List[OpenAITool]) -> List[dict]:
618618
"""
619619
formatted_tools = []
620620
for tool in tools:
621+
# Get the input schema
622+
input_schema = tool.function.parameters or {"type": "object", "properties": {}, "required": []}
623+
624+
# Clean up the properties in the schema
625+
# The presence of union types / default fields seems Anthropic to produce invalid JSON for tool calls
626+
if isinstance(input_schema, dict) and "properties" in input_schema:
627+
cleaned_properties = {}
628+
for prop_name, prop_schema in input_schema.get("properties", {}).items():
629+
if isinstance(prop_schema, dict):
630+
cleaned_properties[prop_name] = _clean_property_schema(prop_schema)
631+
else:
632+
cleaned_properties[prop_name] = prop_schema
633+
634+
# Create cleaned input schema
635+
cleaned_input_schema = {
636+
"type": input_schema.get("type", "object"),
637+
"properties": cleaned_properties,
638+
}
639+
640+
# Only add required field if it exists and is non-empty
641+
if "required" in input_schema and input_schema["required"]:
642+
cleaned_input_schema["required"] = input_schema["required"]
643+
else:
644+
cleaned_input_schema = input_schema
645+
621646
formatted_tool = {
622647
"name": tool.function.name,
623648
"description": tool.function.description if tool.function.description else "",
624-
"input_schema": tool.function.parameters or {"type": "object", "properties": {}, "required": []},
649+
"input_schema": cleaned_input_schema,
625650
}
626651
formatted_tools.append(formatted_tool)
627652

628653
return formatted_tools
629654

630655

656+
def _clean_property_schema(prop_schema: dict) -> dict:
657+
"""Clean up a property schema by removing defaults and simplifying union types."""
658+
cleaned = {}
659+
660+
# Handle type field - simplify union types like ["null", "string"] to just "string"
661+
if "type" in prop_schema:
662+
prop_type = prop_schema["type"]
663+
if isinstance(prop_type, list):
664+
# Remove "null" from union types to simplify
665+
# e.g., ["null", "string"] becomes "string"
666+
non_null_types = [t for t in prop_type if t != "null"]
667+
if len(non_null_types) == 1:
668+
cleaned["type"] = non_null_types[0]
669+
elif len(non_null_types) > 1:
670+
# Keep as array if multiple non-null types
671+
cleaned["type"] = non_null_types
672+
else:
673+
# If only "null" was in the list, default to string
674+
cleaned["type"] = "string"
675+
else:
676+
cleaned["type"] = prop_type
677+
678+
# Copy over other fields except 'default'
679+
for key, value in prop_schema.items():
680+
if key not in ["type", "default"]: # Skip 'default' field
681+
if key == "properties" and isinstance(value, dict):
682+
# Recursively clean nested properties
683+
cleaned["properties"] = {k: clean_property_schema(v) if isinstance(v, dict) else v for k, v in value.items()}
684+
else:
685+
cleaned[key] = value
686+
687+
return cleaned
688+
689+
631690
def is_heartbeat(message: dict, is_ping: bool = False) -> bool:
632691
"""Check if the message is an automated heartbeat ping"""
633692

0 commit comments

Comments
 (0)