Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,39 @@ def _map_tools(
anthropic_tools = []
mcp_servers = []
for tool in tools:
if "input_schema" in tool: # assume in anthropic format
if tool.get("type") == "namespace":
Comment thread
Sameerlite marked this conversation as resolved.
Resolved
# Namespace is a grouping container (e.g. codex's multi_agent_v1).
# Extract its nested tools and map them individually.
for nested in tool.get("tools") or []:
if "input_schema" in nested:
# Already in Anthropic format.
anthropic_tools.append(nested)
elif "function" not in nested and "name" in nested:
Comment thread
Sameerlite marked this conversation as resolved.
Resolved
# Flat format: {type, name, description, parameters, ...}.
# Normalize to OpenAI-wrapped format before mapping.
wrapped = cast(
ChatCompletionToolParam,
{
"type": nested.get("type", "function"),
"function": {
k: v for k, v in nested.items() if k != "type"
},
},
)
nested_tool, nested_mcp = self._map_tool_helper(wrapped)
if nested_tool is not None:
anthropic_tools.append(nested_tool)
if nested_mcp is not None:
mcp_servers.append(nested_mcp)
elif "function" in nested:
nested_tool, nested_mcp = self._map_tool_helper(
cast(ChatCompletionToolParam, nested)
)
if nested_tool is not None:
anthropic_tools.append(nested_tool)
if nested_mcp is not None:
mcp_servers.append(nested_mcp)
elif "input_schema" in tool: # assume in anthropic format
anthropic_tools.append(tool)
else: # assume openai tool call
new_tool, mcp_server_tool = self._map_tool_helper(tool)
Expand Down Expand Up @@ -1978,6 +2010,7 @@ def transform_request(

# Remove internal LiteLLM parameters that should not be sent to Anthropic API
optional_params.pop("is_vertex_request", None)
optional_params.pop("client_metadata", None)
Comment thread
Sameerlite marked this conversation as resolved.
Resolved

data = {
"model": model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5090,3 +5090,101 @@ def test_map_tool_helper_collision_prefers_definitions_over_components_schemas()
# Cross-namespace ref *also* resolves to the `definitions` body because
# ``unpack_defs`` keys by last path segment -- documented limitation.
assert transformed["input_schema"]["properties"]["from_components"] == expected


def test_namespace_tool_flat_nested_tools_are_extracted():
"""Codex sends nested tools in flat format {type, name, description, parameters} with no 'function' wrapper.
These must be normalized and mapped without raising KeyError: 'function'."""
config = AnthropicConfig()
tools = [
{
"type": "namespace",
"name": "multi_agent_v1",
"tools": [
{
"type": "function",
"name": "close_agent",
"description": "Close an agent.",
"strict": False,
"parameters": {
"type": "object",
"properties": {"target": {"type": "string"}},
"required": ["target"],
"additionalProperties": False,
},
},
],
}
]
anthropic_tools, _ = config._map_tools(tools)
assert len(anthropic_tools) == 1
assert anthropic_tools[0]["name"] == "close_agent"


def test_namespace_tool_nested_tools_are_extracted():
"""Codex sends type='namespace' wrapping nested tools in Anthropic format.
The namespace container must be dropped and its nested tools extracted individually.
"""
config = AnthropicConfig()
tools = [
{
"type": "namespace",
"name": "multi_agent_v1",
"description": "Tools for spawning and managing sub-agents.",
"tools": [
{
"name": "close_agent",
"type": "custom",
"description": "Close an agent.",
"input_schema": {
"type": "object",
"properties": {"target": {"type": "string"}},
"required": ["target"],
},
},
{
"name": "resume_agent",
"type": "custom",
"description": "Resume a closed agent.",
"input_schema": {
"type": "object",
"properties": {"id": {"type": "string"}},
"required": ["id"],
},
},
],
},
{
"type": "function",
"function": {
"name": "exec_command",
"description": "Run a command.",
"parameters": {
"type": "object",
"properties": {"cmd": {"type": "string"}},
"required": ["cmd"],
},
},
},
]
anthropic_tools, mcp_servers = config._map_tools(tools)
names = [t["name"] for t in anthropic_tools]
assert "close_agent" in names
assert "resume_agent" in names
assert "exec_command" in names
assert "multi_agent_v1" not in names
assert len(anthropic_tools) == 3
assert mcp_servers == []


def test_client_metadata_stripped_from_anthropic_request():
"""client_metadata passed by codex must not reach the Anthropic (or Vertex Anthropic) payload."""
config = AnthropicConfig()
result = config.transform_request(
model="claude-3-5-haiku-20241022",
messages=[{"role": "user", "content": "hello"}],
optional_params={"max_tokens": 10, "client_metadata": {"originator": "codex"}},
litellm_params={},
headers={},
)
assert "client_metadata" not in result
Loading