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 b6cda1009..7543305f0 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,7 +16,7 @@ from crewai.agents.agent_adapters.openai_agents.protocols import ( ) from crewai.tools import BaseTool from crewai.utilities.import_utils import require -from crewai.utilities.pydantic_schema_utils import add_key_in_dict_recursively +from crewai.utilities.pydantic_schema_utils import force_additional_properties_false from crewai.utilities.string_utils import sanitize_tool_name @@ -136,13 +136,7 @@ class OpenAIAgentToolAdapter(BaseToolAdapter): for tool in tools: schema: dict[str, Any] = tool.args_schema.model_json_schema() - schema = add_key_in_dict_recursively( - schema, - key="additionalProperties", - value=False, - criteria=lambda d: d.get("type") == "object" - and "additionalProperties" not in d, - ) + schema = force_additional_properties_false(schema) schema.update({"type": "object"}) diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py index 78269f98a..c3c5f6292 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 @@ class OpenAICompletion(BaseLLM): ) -> 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 @@ class OpenAICompletion(BaseLLM): } 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 9e1982e44..2b50caea8 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,20 +402,8 @@ def create_model_from_schema( # type: ignore[no-any-unimported] """ effective_root = root_schema or json_schema - 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, - ) - effective_root = add_key_in_dict_recursively( - effective_root, - key="additionalProperties", - value=False, - criteria=lambda d: d.get("type") == "object" - and "additionalProperties" not in d, - ) + 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)