fix(vertex/anthropic): handle namespace tools and strip client_metadata for codex compatibility (#29489)

* fix(vertex/anthropic): handle namespace tools and strip client_metadata for codex compatibility

* fix(anthropic): cast nested namespace tools to fix mypy error, skip nameless flat tools
This commit is contained in:
Sameer Kankute 2026-06-05 11:27:16 +05:30 committed by GitHub
parent df704d9016
commit 2b7c97bff6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 132 additions and 1 deletions

View File

@ -918,7 +918,39 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
anthropic_tools = []
mcp_servers = []
for tool in tools:
if "input_schema" in tool: # assume in anthropic format
if tool.get("type") == "namespace":
# 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:
# 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)
@ -1978,6 +2010,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
# 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)
data = {
"model": model,

View File

@ -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