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

View File

@@ -172,7 +172,7 @@ info = "Commits must follow Conventional Commits 1.0.0."
[tool.uv]
exclude-newer = "3 days"
# These security fixes are newer than the global supply-chain cutoff.
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
# composio-core pins rich<14 but textual requires rich>=14.
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
@@ -183,7 +183,7 @@ exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T
# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1).
# cryptography 46.0.6 has CVE-2026-39892; force 46.0.7+.
# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p.
# pypdf <6.13.3 has GHSA-jm82-fx9c-mx94; force 6.13.3+.
# pypdf <6.14.2 has GHSA-jm82-fx9c-mx94 and GHSA-5qjq-93h5-hrgp/GHSA-55h5-xmcq-c37v/GHSA-g867-7843-wf8q/GHSA-5xf7-4p34-54qr; force 6.14.2+.
# uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+.
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
@@ -213,7 +213,7 @@ override-dependencies = [
"urllib3>=2.7.0",
"transformers>=5.4.0; python_version >= '3.10'",
"cryptography>=46.0.7",
"pypdf>=6.13.3,<7",
"pypdf>=6.14.2,<7",
"uv>=0.11.15,<1",
"python-multipart>=0.0.27,<1",
"gitpython>=3.1.51,<4",

14
uv.lock generated
View File

@@ -13,13 +13,13 @@ resolution-markers = [
]
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer = "2026-07-21T05:40:28.001333135Z"
exclude-newer-span = "P3D"
[options.exclude-newer-package]
msgpack = "2026-06-20T00:00:00Z"
langsmith = "2026-06-20T00:00:00Z"
pypdf = "2026-06-18T00:00:00Z"
pypdf = "2026-06-24T00:00:00Z"
pydantic-settings = "2026-06-20T00:00:00Z"
[manifest]
@@ -48,7 +48,7 @@ overrides = [
{ name = "pip", specifier = ">=26.1.2" },
{ name = "pyasn1", specifier = ">=0.6.4" },
{ name = "pydantic-settings", specifier = ">=2.14.2" },
{ name = "pypdf", specifier = ">=6.13.3,<7" },
{ name = "pypdf", specifier = ">=6.14.2,<7" },
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
{ name = "rich", specifier = ">=13.7.1" },
{ name = "setuptools", specifier = ">=83.0.0" },
@@ -1595,7 +1595,7 @@ requires-dist = [
{ name = "aiofiles", specifier = "~=24.1.0" },
{ name = "av", specifier = "~=13.0.0" },
{ name = "pillow", specifier = "~=12.3.0" },
{ name = "pypdf", specifier = "~=6.13.3" },
{ name = "pypdf", specifier = "~=6.14.2" },
{ name = "python-magic", specifier = ">=0.4.27" },
{ name = "tinytag", specifier = "~=2.2.1" },
]
@@ -7181,14 +7181,14 @@ wheels = [
[[package]]
name = "pypdf"
version = "6.13.3"
version = "6.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" }
sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" },
{ url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" },
]
[[package]]