fix: preserve strict tool schema property names (#6628)

This commit is contained in:
alex-clawd
2026-07-24 09:49:05 -07:00
committed by GitHub
parent b14d36bfe4
commit c06043f7e8
5 changed files with 69 additions and 17 deletions

View File

@@ -9,7 +9,7 @@ authors = [
requires-python = ">=3.10, <3.14"
dependencies = [
"Pillow~=12.3.0",
"pypdf~=6.13.3",
"pypdf~=6.14.2",
"python-magic>=0.4.27",
"aiocache~=0.12.3",
"aiofiles~=24.1.0",
@@ -19,8 +19,8 @@ dependencies = [
[tool.uv]
exclude-newer = "3 days"
# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff.
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" }
# pypdf 6.14.2 is a security fix newer than the global supply-chain cutoff.
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z" }
[build-system]
requires = ["hatchling"]

View File

@@ -549,7 +549,13 @@ _CLAUDE_STRICT_UNSUPPORTED: Final[tuple[str, ...]] = (
def _strip_keys_recursive(
d: Any, keys: tuple[str, ...], _seen: set[int] | None = None
) -> Any:
"""Recursively delete a fixed set of keys from a schema."""
"""Recursively delete schema metadata keys without deleting property names.
JSON Schema stores fields under a ``properties`` map. A user/tool argument can
legitimately be named ``title``, ``default``, or another metadata key. Those
entries must stay in the map; only metadata inside the schema nodes should be
stripped.
"""
if _seen is None:
_seen = set()
if isinstance(d, dict):
@@ -558,8 +564,12 @@ def _strip_keys_recursive(
_seen.add(id(d))
for key in keys:
d.pop(key, None)
for v in d.values():
_strip_keys_recursive(v, keys, _seen)
for key, value in d.items():
if key == "properties" and isinstance(value, dict):
for property_schema in value.values():
_strip_keys_recursive(property_schema, keys, _seen)
else:
_strip_keys_recursive(value, keys, _seen)
elif isinstance(d, list):
if id(d) in _seen:
return d

View File

@@ -933,7 +933,49 @@ class TestResolveRefsRecursive:
assert resolved["properties"]["x"]["type"] == "integer"
class TestSanitizeRecursiveSchemas:
class TestSanitizeStrictSchemas:
def test_openai_strict_preserves_property_named_title(self) -> None:
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict
schema = {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
"url": {"title": "Url", "type": "string"},
},
"required": ["title", "url"],
}
san = sanitize_tool_params_for_openai_strict(deepcopy(schema))
assert "title" in san["properties"]
assert set(san["required"]) == set(san["properties"].keys())
assert "title" not in san["properties"]["title"]
def test_openai_strict_preserves_nested_property_named_title(self) -> None:
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict
schema = {
"type": "object",
"properties": {
"payload": {
"type": "object",
"properties": {
"title": {"title": "Nested Title", "type": "string"},
},
"required": ["title"],
},
},
"required": ["payload"],
}
san = sanitize_tool_params_for_openai_strict(deepcopy(schema))
payload = san["properties"]["payload"]
assert "title" in payload["properties"]
assert payload["required"] == ["title"]
assert "title" not in payload["properties"]["title"]
def test_anthropic_strict_preserves_recursive_type(self) -> None:
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_anthropic_strict