mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Normalize Responses tool schemas
Ensure function parameter schemas include an explicit properties object before Responses requests are dispatched through LiteLLM. Keep prompt/MCP permissive schemas compatible with stricter OpenAI-compatible chat validators and cover chat, legacy function, and native Responses tool inputs with regressions.
This commit is contained in:
parent
a4b2848172
commit
272a0d8dfa
6 changed files with 139 additions and 11 deletions
|
|
@ -640,13 +640,11 @@ class ResponsesTransport:
|
|||
response_builtin_tools: Any = None,
|
||||
) -> list[Any]:
|
||||
merged: list[Any] = []
|
||||
if isinstance(tools, list):
|
||||
merged.extend(tools)
|
||||
elif tools:
|
||||
merged.append(tools)
|
||||
|
||||
for source in (response_function_tools, response_builtin_tools):
|
||||
for tool in _as_list(source):
|
||||
for source in (tools, response_function_tools, response_builtin_tools):
|
||||
source_tools = (
|
||||
source if isinstance(source, list) else [source] if source else []
|
||||
)
|
||||
for tool in source_tools:
|
||||
normalized = cls.normalize_response_tool(tool)
|
||||
if normalized:
|
||||
merged.append(normalized)
|
||||
|
|
@ -658,7 +656,12 @@ class ResponsesTransport:
|
|||
tool = {"type": tool}
|
||||
if not isinstance(tool, dict):
|
||||
return None
|
||||
return dict(tool)
|
||||
normalized = dict(tool)
|
||||
if normalized.get("type") == "function":
|
||||
normalized["parameters"] = _normalize_function_parameters(
|
||||
normalized.get("parameters")
|
||||
)
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def prepare_prompt_caching(
|
||||
|
|
@ -800,7 +803,9 @@ class ResponsesTransport:
|
|||
"type": "function",
|
||||
"name": function.get("name", ""),
|
||||
"description": function.get("description", ""),
|
||||
"parameters": function.get("parameters", {}),
|
||||
"parameters": _normalize_function_parameters(
|
||||
function.get("parameters")
|
||||
),
|
||||
}
|
||||
if "strict" in function:
|
||||
response_tool["strict"] = function["strict"]
|
||||
|
|
@ -1206,6 +1211,23 @@ def _response_tool_type(tool: Any) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
def _normalize_function_parameters(parameters: Any) -> dict[str, Any]:
|
||||
if not isinstance(parameters, dict):
|
||||
return _permissive_function_parameters()
|
||||
|
||||
normalized = dict(parameters)
|
||||
normalized.setdefault("type", "object")
|
||||
if normalized.get("type") == "object" and not isinstance(
|
||||
normalized.get("properties"), dict
|
||||
):
|
||||
normalized["properties"] = {}
|
||||
return normalized or _permissive_function_parameters()
|
||||
|
||||
|
||||
def _permissive_function_parameters() -> dict[str, Any]:
|
||||
return {"type": "object", "properties": {}, "additionalProperties": True}
|
||||
|
||||
|
||||
def apply_chat_prompt_cache_markers(
|
||||
messages: list[dict[str, Any]],
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
- Keep provider selection and provider-specific defaults outside this helper; callers pass a resolved LiteLLM model name and kwargs.
|
||||
- Strip Agent Zero internal kwargs before sending requests to LiteLLM.
|
||||
- Do not send orphan tool controls when no tools are present; strict OpenAI-compatible servers can reject empty `tools` arrays.
|
||||
- Normalize function tool parameter schemas with an explicit object `properties` field before Responses requests so OpenAI-compatible chat backends reached through LiteLLM can validate them.
|
||||
- Prefer Responses API when configured, but fallback to Chat Completions when the provider does not support Responses.
|
||||
- Fall back to Chat Completions when a Responses request is rejected before any output by an endpoint-specific or shape-specific Bad Request indicating the provider cannot parse Responses payloads.
|
||||
- Preserve provider-state metadata when Responses API calls succeed, and fall back to local replay when provider state is unsupported.
|
||||
|
|
|
|||
|
|
@ -217,13 +217,17 @@ def _schema_from_any(schema: Any) -> dict[str, Any]:
|
|||
if isinstance(schema, dict):
|
||||
normalized = dict(schema)
|
||||
normalized.setdefault("type", "object")
|
||||
if normalized.get("type") == "object" and not isinstance(
|
||||
normalized.get("properties"), dict
|
||||
):
|
||||
normalized["properties"] = {}
|
||||
normalized.setdefault("additionalProperties", True)
|
||||
return normalized
|
||||
return _permissive_schema()
|
||||
|
||||
|
||||
def _permissive_schema() -> dict[str, Any]:
|
||||
return {"type": "object", "additionalProperties": True}
|
||||
return {"type": "object", "properties": {}, "additionalProperties": True}
|
||||
|
||||
|
||||
def _balanced_json_object(text: str) -> str:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
- Build local function tools from enabled `agent.system.tool.*.md` prompt files.
|
||||
- Local prompt-derived function names prefer explicit `"tool_name"` examples, then the first prompt heading, and only fall back to the prompt filename when the prompt declares no callable name.
|
||||
- Function parameter schemas are object schemas with an explicit `properties` object so OpenAI-compatible servers that validate chat-style tool payloads accept permissive tools.
|
||||
- Preserve original Agent Zero tool names through the native Responses name map.
|
||||
- Keep MCP tool schemas merged after local prompt-derived tools.
|
||||
- Connector remote tools are advertised only when `_a0_connector` runtime metadata says the matching connected CLI capability is currently available.
|
||||
|
|
|
|||
|
|
@ -101,3 +101,48 @@ def test_responses_function_tools_use_prompt_declared_names(monkeypatch, tmp_pat
|
|||
assert name_map["call_subordinate"] == "call_subordinate"
|
||||
assert name_map["behaviour_adjustment"] == "behaviour_adjustment"
|
||||
assert name_map["filename_only"] == "filename_only"
|
||||
assert all(isinstance(tool["parameters"].get("properties"), dict) for tool in tools)
|
||||
|
||||
|
||||
def test_responses_function_tools_add_empty_properties_to_mcp_schemas(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
prompt_root = tmp_path / "prompts"
|
||||
prompt_root.mkdir()
|
||||
|
||||
monkeypatch.setattr(
|
||||
responses_tools.subagents,
|
||||
"get_paths",
|
||||
lambda *args, **kwargs: [str(prompt_root)],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
responses_tools,
|
||||
"_mcp_tools",
|
||||
lambda agent: [
|
||||
(
|
||||
"remote_noop",
|
||||
{
|
||||
"description": "Remote noop",
|
||||
"input_schema": {"type": "object"},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
tools, _name_map = responses_tools.build_responses_function_tools(
|
||||
FakeAgent(prompt_root)
|
||||
)
|
||||
|
||||
assert tools == [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "remote_noop",
|
||||
"description": "Remote noop",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -780,7 +780,7 @@ def test_responses_request_translates_messages_and_params():
|
|||
"type": "function",
|
||||
"name": "lookup",
|
||||
"description": "Search",
|
||||
"parameters": {"type": "object"},
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"strict": True,
|
||||
}
|
||||
]
|
||||
|
|
@ -839,6 +839,61 @@ def test_responses_request_normalizes_reasoning_and_orphan_tool_choice():
|
|||
assert "reasoning" not in request
|
||||
|
||||
|
||||
def test_responses_request_normalizes_function_tool_parameter_shapes():
|
||||
request = litellm_transport.ResponsesTransport.from_chat(
|
||||
[],
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
"name": "legacy_noop",
|
||||
"description": "Legacy function",
|
||||
"parameters": {},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert request["tools"] == [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "legacy_noop",
|
||||
"description": "Legacy function",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
request = litellm_transport.ResponsesTransport.from_chat(
|
||||
[],
|
||||
{
|
||||
"a0_responses_function_tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "native_noop",
|
||||
"description": "Native function",
|
||||
"parameters": {"type": "object"},
|
||||
}
|
||||
],
|
||||
"responses_builtin_tools": [{"type": "web_search"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert request["tools"] == [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "native_noop",
|
||||
"description": "Native function",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
{"type": "web_search"},
|
||||
]
|
||||
|
||||
|
||||
def test_chat_completions_kwargs_omit_empty_tools():
|
||||
kwargs = litellm_transport.ChatCompletionsTransport.prepare_kwargs(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue