diff --git a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py index 6096ee5d01..7543305f04 100644 --- a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py +++ b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py @@ -16,6 +16,7 @@ ) from crewai.tools import BaseTool from crewai.utilities.import_utils import require +from crewai.utilities.pydantic_schema_utils import force_additional_properties_false from crewai.utilities.string_utils import sanitize_tool_name @@ -135,7 +136,9 @@ async def wrapper(context_wrapper: Any, arguments: Any) -> Any: for tool in tools: schema: dict[str, Any] = tool.args_schema.model_json_schema() - schema.update({"additionalProperties": False, "type": "object"}) + schema = force_additional_properties_false(schema) + + schema.update({"type": "object"}) openai_tool: OpenAIFunctionTool = cast( OpenAIFunctionTool, diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py index 78269f98ab..c3c5f6292f 100644 --- a/lib/crewai/src/crewai/llms/providers/openai/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai/completion.py @@ -1521,13 +1521,16 @@ def _convert_tools_for_interference( ) -> list[dict[str, Any]]: """Convert CrewAI tool format to OpenAI function calling format.""" from crewai.llms.providers.utils.common import safe_tool_conversion + from crewai.utilities.pydantic_schema_utils import ( + force_additional_properties_false, + ) openai_tools = [] for tool in tools: name, description, parameters = safe_tool_conversion(tool, "OpenAI") - openai_tool = { + openai_tool: dict[str, Any] = { "type": "function", "function": { "name": name, @@ -1537,10 +1540,11 @@ def _convert_tools_for_interference( } if parameters: - if isinstance(parameters, dict): - openai_tool["function"]["parameters"] = parameters # type: ignore - else: - openai_tool["function"]["parameters"] = dict(parameters) + params_dict = ( + parameters if isinstance(parameters, dict) else dict(parameters) + ) + params_dict = force_additional_properties_false(params_dict) + openai_tool["function"]["parameters"] = params_dict openai_tools.append(openai_tool) return openai_tools diff --git a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py index 69354742b7..2b50caea82 100644 --- a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py +++ b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py @@ -127,6 +127,36 @@ def add_key_in_dict_recursively( return d +def force_additional_properties_false(d: Any) -> Any: + """Force additionalProperties=false on all object-type dicts recursively. + + OpenAI strict mode requires all objects to have additionalProperties=false. + This function overwrites any existing value to ensure compliance. + + Also ensures objects have properties and required arrays, even if empty, + as OpenAI strict mode requires these for all object types. + + Args: + d: The dictionary/list to modify. + + Returns: + The modified dictionary/list. + """ + if isinstance(d, dict): + if d.get("type") == "object": + d["additionalProperties"] = False + if "properties" not in d: + d["properties"] = {} + if "required" not in d: + d["required"] = [] + for v in d.values(): + force_additional_properties_false(v) + elif isinstance(d, list): + for i in d: + force_additional_properties_false(i) + return d + + def fix_discriminator_mappings(schema: dict[str, Any]) -> dict[str, Any]: """Replace '#/$defs/...' references in discriminator.mapping with just the model name. @@ -278,13 +308,7 @@ def generate_model_description(model: type[BaseModel]) -> dict[str, Any]: """ json_schema = model.model_json_schema(ref_template="#/$defs/{model}") - json_schema = add_key_in_dict_recursively( - json_schema, - key="additionalProperties", - value=False, - criteria=lambda d: d.get("type") == "object" - and "additionalProperties" not in d, - ) + json_schema = force_additional_properties_false(json_schema) json_schema = resolve_refs(json_schema) @@ -378,6 +402,9 @@ def create_model_from_schema( # type: ignore[no-any-unimported] """ effective_root = root_schema or json_schema + json_schema = force_additional_properties_false(json_schema) + effective_root = force_additional_properties_false(effective_root) + if "allOf" in json_schema: json_schema = _merge_all_of_schemas(json_schema["allOf"], effective_root) if "title" not in json_schema and "title" in (root_schema or {}):