feat: implement strip_openai_specific_schema_fields utility

* Added a new utility function to remove OpenAI-specific fields from schemas, ensuring compatibility with Gemini and Bedrock APIs.
* Updated BedrockCompletion and GeminiCompletion classes to utilize this function for cleaning parameters before API calls.
* Introduced tests to validate the functionality of the new utility, ensuring it correctly strips incompatible fields while preserving required properties.
This commit is contained in:
lorenzejay
2026-02-25 15:26:43 -08:00
parent 168b650226
commit 120f2ef6e7
7 changed files with 591 additions and 0 deletions

View File

@@ -1948,6 +1948,9 @@ class BedrockCompletion(BaseLLM):
) -> list[ConverseToolTypeDef]: ) -> list[ConverseToolTypeDef]:
"""Convert CrewAI tools to Converse API format following AWS specification.""" """Convert CrewAI tools to Converse API format following AWS specification."""
from crewai.llms.providers.utils.common import safe_tool_conversion from crewai.llms.providers.utils.common import safe_tool_conversion
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
converse_tools: list[ConverseToolTypeDef] = [] converse_tools: list[ConverseToolTypeDef] = []
@@ -1961,6 +1964,7 @@ class BedrockCompletion(BaseLLM):
} }
if parameters and isinstance(parameters, dict): if parameters and isinstance(parameters, dict):
parameters = strip_openai_specific_schema_fields(parameters)
input_schema: ToolInputSchema = {"json": parameters} input_schema: ToolInputSchema = {"json": parameters}
tool_spec["inputSchema"] = input_schema tool_spec["inputSchema"] = input_schema

View File

@@ -529,10 +529,16 @@ class GeminiCompletion(BaseLLM):
gemini_tools = [] gemini_tools = []
from crewai.llms.providers.utils.common import safe_tool_conversion from crewai.llms.providers.utils.common import safe_tool_conversion
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
for tool in tools: for tool in tools:
name, description, parameters = safe_tool_conversion(tool, "Gemini") name, description, parameters = safe_tool_conversion(tool, "Gemini")
if parameters:
parameters = strip_openai_specific_schema_fields(parameters)
function_declaration = types.FunctionDeclaration( function_declaration = types.FunctionDeclaration(
name=name, name=name,
description=description, description=description,

View File

@@ -417,6 +417,35 @@ def strip_null_from_types(schema: dict[str, Any]) -> dict[str, Any]:
return schema return schema
def strip_openai_specific_schema_fields(schema: dict[str, Any]) -> dict[str, Any]:
"""Remove OpenAI strict-mode fields that are incompatible with other providers.
OpenAI strict mode requires ``additionalProperties: false`` on every object
and all properties listed in ``required``. Gemini and Bedrock reject these
fields with schema validation errors, so we strip them before handing off to
non-OpenAI providers.
Specifically removes (recursively):
- ``additionalProperties`` — not supported by Gemini / Bedrock tool schemas
Args:
schema: JSON schema dictionary (mutated in place and returned).
Returns:
The modified schema.
"""
if isinstance(schema, dict):
schema.pop("additionalProperties", None)
for value in schema.values():
if isinstance(value, dict):
strip_openai_specific_schema_fields(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
strip_openai_specific_schema_fields(item)
return schema
def generate_model_description( def generate_model_description(
model: type[BaseModel], model: type[BaseModel],
*, *,

View File

@@ -559,6 +559,39 @@ class TestAnthropicNativeToolCalling:
_assert_tools_overlapped() _assert_tools_overlapped()
_MCP_OPTIONAL_FIELDS_TOOL_DEFS = [
{
"name": "filtered_search",
"description": (
"Search a knowledge base. Pass null for any filter you don't need."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string",
},
"filter_type": {
"anyOf": [
{"type": "string", "enum": ["news", "blog", "docs"]},
{"type": "null"},
],
"default": None,
"description": "Optional content-type filter",
},
"page_id": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Optional page UUID",
},
},
"required": ["query"],
},
}
]
# ============================================================================= # =============================================================================
# Google/Gemini Provider Tests # Google/Gemini Provider Tests
# ============================================================================= # =============================================================================
@@ -796,6 +829,9 @@ class TestAzureNativeToolCalling:
_assert_tools_overlapped() _assert_tools_overlapped()
# ============================================================================= # =============================================================================
# Bedrock Provider Tests # Bedrock Provider Tests
# ============================================================================= # =============================================================================
@@ -929,6 +965,135 @@ class TestNativeToolCallingBehavior:
assert hasattr(llm, "supports_function_calling") assert hasattr(llm, "supports_function_calling")
assert llm.supports_function_calling() is True assert llm.supports_function_calling() is True
@pytest.mark.vcr()
def test_gemini_mcp_tool_with_optional_fields_no_schema_error(self) -> None:
"""MCP tools with Optional fields must not cause Gemini schema validation errors.
Regression test for https://github.com/crewAIInc/crewAI/issues/4472.
Before the fix, generate_model_description() added additionalProperties:false
to the tool schema. Gemini rejects this field, raising:
"value at properties.<field> must be a list"
With the fix, strip_openai_specific_schema_fields() removes it before the
schema is sent to the Gemini API.
To record the cassette:
RUN_LIVE_GEMINI_TESTS=true GOOGLE_API_KEY=<key> \\
uv run pytest tests/agents/test_native_tool_calling.py \\
::TestGeminiNativeToolCalling::test_gemini_mcp_tool_with_optional_fields_no_schema_error \\
--record-mode=once -v
"""
from unittest.mock import AsyncMock, patch
from crewai.mcp.config import MCPServerStdio
stdio_config = MCPServerStdio(command="python", args=["fake_mcp_server.py"])
agent = Agent(
role="Knowledge Researcher",
goal="Search a knowledge base and return relevant results",
backstory="You are a helpful researcher who uses the filtered_search tool.",
mcps=[stdio_config],
llm=LLM(model="gemini/gemini-2.5-flash"),
verbose=False,
max_iter=3,
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(
return_value=_MCP_OPTIONAL_FIELDS_TOOL_DEFS
)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.call_tool = AsyncMock(
return_value="Search results: Introduction to agents, Tool integration guide"
)
mock_client_class.return_value = mock_client
task = Task(
description=(
"Search the knowledge base for information about CrewAI agents. "
"No filters needed — just a general search."
),
expected_output="A brief summary of relevant search results.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
assert result is not None
assert result.raw is not None
@pytest.mark.vcr()
def test_bedrock_mcp_tool_with_optional_fields_no_schema_error(self) -> None:
"""MCP tools with Optional fields must not cause Bedrock schema validation errors.
Regression test for https://github.com/crewAIInc/crewAI/issues/4472.
Before the fix, generate_model_description() added additionalProperties:false
to the tool schema. The Bedrock Converse API rejects this field, raising:
"JSON schema is invalid"
With the fix, strip_openai_specific_schema_fields() removes it before the
schema is sent to Bedrock.
Note: MCPClient is mocked — no Bedrock credentials are needed to run this
test. The LLM call is replayed from the VCR cassette.
To record the cassette:
AWS_ACCESS_KEY_ID=<key> AWS_SECRET_ACCESS_KEY=<secret> AWS_DEFAULT_REGION=us-east-1 \\
uv run pytest tests/agents/test_native_tool_calling.py \\
::TestNativeToolCallingBehavior::test_bedrock_mcp_tool_with_optional_fields_no_schema_error \\
--record-mode=once -v
"""
from unittest.mock import AsyncMock, patch
from crewai.mcp.config import MCPServerStdio
stdio_config = MCPServerStdio(command="python", args=["fake_mcp_server.py"])
agent = Agent(
role="Knowledge Researcher",
goal="Search a knowledge base and return relevant results",
backstory="You are a helpful researcher who uses the filtered_search tool.",
mcps=[stdio_config],
llm=LLM(model="bedrock/anthropic.claude-3-haiku-20240307-v1:0"),
verbose=False,
max_iter=3,
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(
return_value=_MCP_OPTIONAL_FIELDS_TOOL_DEFS
)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.call_tool = AsyncMock(
return_value="Search results: Introduction to agents, Tool integration guide"
)
mock_client_class.return_value = mock_client
task = Task(
description=(
"Search the knowledge base for information about CrewAI agents. "
"No filters needed — just a general search."
),
expected_output="A brief summary of relevant search results.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
assert result is not None
assert result.raw is not None
# ============================================================================= # =============================================================================
# Token Usage Tests # Token Usage Tests

View File

@@ -0,0 +1,127 @@
interactions:
- request:
body: '{"messages": [{"role": "user", "content": [{"text": "\nCurrent Task: Search
the knowledge base for information about CrewAI agents. No filters needed \u2014
just a general search.\n\nThis is the expected criteria for your final answer:
A brief summary of relevant search results.\nyou MUST return the actual complete
content as the final answer, not a summary."}]}], "inferenceConfig": {"stopSequences":
["\nObservation:"]}, "system": [{"text": "You are Knowledge Researcher. You
are a helpful researcher who uses the filtered_search tool.\nYour personal goal
is: Search a knowledge base and return relevant results"}], "toolConfig": {"tools":
[{"toolSpec": {"name": "python_fake_mcp_server_py_filtered_search", "description":
"Search a knowledge base. Pass null for any filter you don''t need.", "inputSchema":
{"json": {"properties": {"query": {"description": "Search query string", "title":
"Query", "type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type":
"object"}, {"type": "null"}], "default": null, "description": "Optional content-type
filter", "title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"},
{"type": "object"}, {"type": "null"}], "default": null, "description": "Optional
page UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
"type": "object"}}}}]}}'
headers:
Content-Length:
- '1324'
Content-Type:
- !!binary |
YXBwbGljYXRpb24vanNvbg==
User-Agent:
- X-USER-AGENT-XXX
amz-sdk-invocation-id:
- AMZ-SDK-INVOCATION-ID-XXX
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
authorization:
- AUTHORIZATION-XXX
x-amz-date:
- X-AMZ-DATE-XXX
method: POST
uri: https://bedrock-runtime.us-west-2.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
response:
body:
string: '{"metrics":{"latencyMs":1322},"output":{"message":{"content":[{"text":"Okay,
let''s search the knowledge base for information about CrewAI agents:"},{"toolUse":{"input":{"query":"CrewAI
agents","filter_type":null,"page_id":null},"name":"python_fake_mcp_server_py_filtered_search","toolUseId":"tooluse_KjK6AV9oUkSLKCHlXDoc05"}}],"role":"assistant"}},"stopReason":"tool_use","usage":{"inputTokens":572,"outputTokens":125,"serverToolUsage":{},"totalTokens":697}}'
headers:
Connection:
- keep-alive
Content-Length:
- '458'
Content-Type:
- application/json
Date:
- Wed, 25 Feb 2026 23:26:08 GMT
x-amzn-RequestId:
- X-AMZN-REQUESTID-XXX
status:
code: 200
message: OK
- request:
body: '{"messages": [{"role": "user", "content": [{"text": "\nCurrent Task: Search
the knowledge base for information about CrewAI agents. No filters needed \u2014
just a general search.\n\nThis is the expected criteria for your final answer:
A brief summary of relevant search results.\nyou MUST return the actual complete
content as the final answer, not a summary."}]}, {"role": "assistant", "content":
[{"toolUse": {"toolUseId": "tooluse_KjK6AV9oUkSLKCHlXDoc05", "name": "python_fake_mcp_server_py_filtered_search",
"input": {}}}]}, {"role": "user", "content": [{"toolResult": {"toolUseId": "tooluse_KjK6AV9oUkSLKCHlXDoc05",
"content": [{"text": "Search results: Introduction to agents, Tool integration
guide"}]}}]}, {"role": "user", "content": [{"text": "Analyze the tool result.
If requirements are met, provide the Final Answer. Otherwise, call the next
tool. Deliver only the answer without meta-commentary."}]}], "inferenceConfig":
{"stopSequences": ["\nObservation:"]}, "system": [{"text": "You are Knowledge
Researcher. You are a helpful researcher who uses the filtered_search tool.\nYour
personal goal is: Search a knowledge base and return relevant results"}], "toolConfig":
{"tools": [{"toolSpec": {"name": "python_fake_mcp_server_py_filtered_search",
"description": "Search a knowledge base. Pass null for any filter you don''t
need.", "inputSchema": {"json": {"properties": {"query": {"description": "Search
query string", "title": "Query", "type": "string"}, "filter_type": {"anyOf":
[{"type": "string"}, {"type": "object"}, {"type": "null"}], "default": null,
"description": "Optional content-type filter", "title": "Filter Type"}, "page_id":
{"anyOf": [{"type": "string"}, {"type": "object"}, {"type": "null"}], "default":
null, "description": "Optional page UUID", "title": "Page Id"}}, "required":
["query", "filter_type", "page_id"], "type": "object"}}}}]}}'
headers:
Content-Length:
- '1873'
Content-Type:
- !!binary |
YXBwbGljYXRpb24vanNvbg==
User-Agent:
- X-USER-AGENT-XXX
amz-sdk-invocation-id:
- AMZ-SDK-INVOCATION-ID-XXX
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
authorization:
- AUTHORIZATION-XXX
x-amz-date:
- X-AMZ-DATE-XXX
method: POST
uri: https://bedrock-runtime.us-west-2.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
response:
body:
string: '{"metrics":{"latencyMs":2372},"output":{"message":{"content":[{"text":"<result>\nIntroduction
to agents:\nCrewAI agents are designed to assist humans with a variety of
tasks. They can be integrated with other tools and systems to provide automation,
task completion, and data processing capabilities. Agents have access to information
and can collaborate with each other to achieve objectives.\n\nTool integration
guide:\nThe CrewAI tool integration guide provides instructions for connecting
agents with other applications and services. This allows agents to access
data, trigger actions, and exchange information across different platforms.
The guide covers API integration, event handling, and customization options
to tailor agent behavior.\n</result>"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":680,"outputTokens":130,"serverToolUsage":{},"totalTokens":810}}'
headers:
Connection:
- keep-alive
Content-Length:
- '890'
Content-Type:
- application/json
Date:
- Wed, 25 Feb 2026 23:26:10 GMT
x-amzn-RequestId:
- X-AMZN-REQUESTID-XXX
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,161 @@
interactions:
- request:
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Search the knowledge
base for information about CrewAI agents. No filters needed \u2014 just a general
search.\n\nThis is the expected criteria for your final answer: A brief summary
of relevant search results.\nyou MUST return the actual complete content as
the final answer, not a summary."}], "role": "user"}], "systemInstruction":
{"parts": [{"text": "You are Knowledge Researcher. You are a helpful researcher
who uses the filtered_search tool.\nYour personal goal is: Search a knowledge
base and return relevant results"}], "role": "user"}, "tools": [{"functionDeclarations":
[{"description": "Search a knowledge base. Pass null for any filter you don''t
need.", "name": "python_fake_mcp_server_py_filtered_search", "parameters_json_schema":
{"properties": {"query": {"description": "Search query string", "title": "Query",
"type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type": "object"},
{"type": "null"}], "default": null, "description": "Optional content-type filter",
"title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"}, {"type":
"object"}, {"type": "null"}], "default": null, "description": "Optional page
UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
"type": "object"}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
accept:
- '*/*'
accept-encoding:
- ACCEPT-ENCODING-XXX
connection:
- keep-alive
content-length:
- '1360'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"functionCall\": {\n \"name\": \"python_fake_mcp_server_py_filtered_search\",\n
\ \"args\": {\n \"query\": \"CrewAI agents\"\n
\ }\n },\n \"thoughtSignature\": \"CsMEAb4+9vuUI8bbZhI2K5IFCJz+FZPQ+EP/o+hvgBgIAImbCgAE3lyB3kNCn3FcFFSBSPP0KKDiEqq/J0tZjsSmNolNSZr2qAc2kLpzDkI26PhKOk4X3WQ9JEUuaCopvsFYd4mBgZqLaK8PAASQQX8zFD5X54TCk6RFtood4o82kouQbXPCUZAKiC9V/tkz3Qqe5M+K7W69MKzsKGiQakO5I5UfIKc/KD69rROuf/ZUw5oU+8+0qQVhvp8HuPWyQW815eyVZidwIUfFRaswRzZp48PefjNWKhf/1NcVM5fy+RLq5M88FQZ9zhcY9wjV3H66G439VHPCH1SekLJ7A7se/60Sj7/oXfzl93FZ9kb4RMA2PsEJSFqb+EDVDhEmkRxsqCERSoEm7vSVut7AEJiO4UG3+5986MjV52IOpgdTZxWwUoJbQ9xKH7fov6rEDLa1ExSYLmqvnPJbZEhfR39vVmTM105qIlzFSVOBekOsqc6VCSX5P0iwmlxGib7YTRYFaqyY2ZuEWp4Xw+m82MjBIFRZPHW80tSG285VSTFUx6dxkMsfC1XT0TzqjwmeV+xSQ9fvuu8hM6EO5XN+vHIJSC2WPrN+Hc0WIaZb8Ol1q4BITA7jxAp8RLg7PD4FI7VzzMzyDMdf7kffSVMRGOdaNC8zGfM2KbSP3J2QA/InmM9bUawKsOz+nMLsjTs2fTp2nvMmNRePWlNkKOxV0DUfxCQRPtQOZk/OKd9U6GpCd/omHtGkk8hWcgYwhxQxXsNOqnIb\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
203,\n \"candidatesTokenCount\": 28,\n \"totalTokenCount\": 361,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 203\n
\ }\n ],\n \"thoughtsTokenCount\": 130\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"bICfafeTH_T5-8YP8rnE6Qo\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Wed, 25 Feb 2026 23:06:21 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=1210
Transfer-Encoding:
- chunked
Vary:
- Origin
- X-Origin
- Referer
X-Content-Type-Options:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-XSS-Protection:
- '0'
status:
code: 200
message: OK
- request:
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Search the knowledge
base for information about CrewAI agents. No filters needed \u2014 just a general
search.\n\nThis is the expected criteria for your final answer: A brief summary
of relevant search results.\nyou MUST return the actual complete content as
the final answer, not a summary."}], "role": "user"}, {"parts": [{"functionCall":
{"args": {"query": "CrewAI agents"}, "name": "python_fake_mcp_server_py_filtered_search"}}],
"role": "model"}, {"parts": [{"functionResponse": {"name": "python_fake_mcp_server_py_filtered_search",
"response": {"result": "Search results: Introduction to agents, Tool integration
guide"}}}], "role": "user"}, {"parts": [{"text": "Analyze the tool result. If
requirements are met, provide the Final Answer. Otherwise, call the next tool.
Deliver only the answer without meta-commentary."}], "role": "user"}], "systemInstruction":
{"parts": [{"text": "You are Knowledge Researcher. You are a helpful researcher
who uses the filtered_search tool.\nYour personal goal is: Search a knowledge
base and return relevant results"}], "role": "user"}, "tools": [{"functionDeclarations":
[{"description": "Search a knowledge base. Pass null for any filter you don''t
need.", "name": "python_fake_mcp_server_py_filtered_search", "parameters_json_schema":
{"properties": {"query": {"description": "Search query string", "title": "Query",
"type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type": "object"},
{"type": "null"}], "default": null, "description": "Optional content-type filter",
"title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"}, {"type":
"object"}, {"type": "null"}], "default": null, "description": "Optional page
UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
"type": "object"}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
accept:
- '*/*'
accept-encoding:
- ACCEPT-ENCODING-XXX
connection:
- keep-alive
content-length:
- '1893'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"Search results: Introduction to agents,
Tool integration guide\"\n }\n ],\n \"role\": \"model\"\n
\ },\n \"finishReason\": \"STOP\",\n \"index\": 0\n }\n ],\n
\ \"usageMetadata\": {\n \"promptTokenCount\": 299,\n \"candidatesTokenCount\":
10,\n \"totalTokenCount\": 309,\n \"promptTokensDetails\": [\n {\n
\ \"modality\": \"TEXT\",\n \"tokenCount\": 299\n }\n ]\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"bYCfacPnLsD4-8YP6p64sQs\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Wed, 25 Feb 2026 23:06:22 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=380
Transfer-Encoding:
- chunked
Vary:
- Origin
- X-Origin
- Referer
X-Content-Type-Options:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-XSS-Protection:
- '0'
status:
code: 200
message: OK
version: 1

View File

@@ -309,6 +309,105 @@ class TestOptionalFieldsPreserveNull:
assert "page_id" in params["required"] assert "page_id" in params["required"]
class TestStripOpenAISpecificSchemaFields:
"""Tests for strip_openai_specific_schema_fields.
All tests call the real schema pipeline with real tool instances —
no mocking of internal functions.
"""
def test_removes_additional_properties_at_root(self) -> None:
"""additionalProperties must be absent after stripping."""
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
tools = [MCPStyleTool()]
schemas, _ = convert_tools_to_openai_schema(tools)
params = schemas[0]["function"]["parameters"]
# Confirm it's present in the OpenAI-format schema
assert params.get("additionalProperties") is False
clean = strip_openai_specific_schema_fields(params)
assert "additionalProperties" not in clean
def test_removes_additional_properties_recursively(self) -> None:
"""Nested objects must also have additionalProperties removed."""
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
class NestedInput(BaseModel):
config: dict[str, Any] = Field(default_factory=dict)
query: str = Field(description="query")
class NestedTool(BaseTool):
name: str = "nested_tool"
description: str = "tool with nested object"
args_schema: type[BaseModel] = NestedInput
def _run(self, **kwargs: Any) -> str:
return "ok"
schemas, _ = convert_tools_to_openai_schema([NestedTool()])
params = schemas[0]["function"]["parameters"]
clean = strip_openai_specific_schema_fields(params)
def has_additional_properties(obj: Any) -> bool:
if isinstance(obj, dict):
if "additionalProperties" in obj:
return True
return any(has_additional_properties(v) for v in obj.values())
if isinstance(obj, list):
return any(has_additional_properties(i) for i in obj)
return False
assert not has_additional_properties(clean)
def test_preserves_required_and_properties(self) -> None:
"""required and properties must survive the strip."""
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
tools = [MCPStyleTool()]
schemas, _ = convert_tools_to_openai_schema(tools)
params = schemas[0]["function"]["parameters"]
clean = strip_openai_specific_schema_fields(params)
assert "properties" in clean
assert "required" in clean
assert "query" in clean["properties"]
def test_gemini_bedrock_path_extracts_clean_parameters(self) -> None:
"""extract_tool_info returns the same parameters dict that
strip_openai_specific_schema_fields will clean — verifying the
full Gemini/Bedrock conversion pipeline produces a valid schema."""
from crewai.llms.providers.utils.common import extract_tool_info
from crewai.utilities.pydantic_schema_utils import (
strip_openai_specific_schema_fields,
)
tools = [MCPStyleTool()]
schemas, _ = convert_tools_to_openai_schema(tools)
# extract_tool_info is what Gemini/Bedrock call internally
_name, _desc, parameters = extract_tool_info(schemas[0])
assert parameters.get("additionalProperties") is False, (
"Raw OpenAI schema should have additionalProperties=false"
)
clean = strip_openai_specific_schema_fields(parameters)
assert "additionalProperties" not in clean
# null types preserved for optional fields
assert any(
opt.get("type") == "null"
for opt in clean["properties"]["page_id"].get("anyOf", [])
)
class TestSummarizeMessages: class TestSummarizeMessages:
"""Tests for summarize_messages function.""" """Tests for summarize_messages function."""