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:
parent
df704d9016
commit
2b7c97bff6
@ -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,
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user