fix: schema compaction must not drop property names that match DROP_KEYS (#785)

Fixes #759

## Summary

`_compact_openai_tool_schema_value()` strips every key matching
`_OPENAI_TOOL_SCHEMA_DROP_KEYS` (which includes `title`, `readOnly`,
`deprecated`, `writeOnly`, etc.) regardless of where in the schema tree
it appears. This is wrong when those same strings are used as **property
names** inside a `properties` object — they're valid business fields,
not annotation metadata.

The result is an invalid strict schema sent upstream:
```
"required key 'title' not in properties"
```

**Root cause (single function, two lines):**
```python
# before — drops "title" everywhere, even as a property name
for key, child in value.items():
    if key in _OPENAI_TOOL_SCHEMA_DROP_KEYS:
        continue
    compacted[key] = _compact_openai_tool_schema_value(child)
```

**Fix — add `_parent_key` context, skip drop only when not inside
`properties`:**
```python
def _compact_openai_tool_schema_value(value, _parent_key=None):
    ...
    for key, child in value.items():
        if _parent_key != "properties" and key in _OPENAI_TOOL_SCHEMA_DROP_KEYS:
            continue
        compacted[key] = _compact_openai_tool_schema_value(child, key)
```

Schema-level annotations (e.g. `title: "ReadFileParameters"` at schema
root) are **still stripped**. Only property names whose string value
happens to match a drop-key are preserved.

## Test plan

- [x] Added
`test_openai_tool_schema_compaction_preserves_property_named_title` in
`tests/test_openai_responses_context_compaction.py` — reproduces the
exact OMP `eval` tool schema from the issue report
- [x] All 9 existing compaction tests still pass (including
`test_openai_tool_schema_compaction_preserves_invocation_shape` which
verifies schema-level `title` is still stripped)

```
tests/test_openai_responses_context_compaction.py::test_openai_tool_schema_compaction_preserves_invocation_shape PASSED
tests/test_openai_responses_context_compaction.py::test_openai_tool_schema_compaction_preserves_property_named_title PASSED
tests/test_openai_responses_context_compaction.py::test_openai_tool_schema_compaction_is_deterministic PASSED
9 passed
```

## Real behavior proof

- **OS**: macOS darwin arm64, Python 3.11.0
- **Tested**: ran the new and existing compaction tests locally against
the patched handler
- **Not tested**: live OMP / Venice.ai / Codex endpoint (no API key for
those); the fix is a pure schema-transform function with no network side
effects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashish 2026-06-09 14:29:18 -07:00 committed by GitHub
parent 0ce68dedd7
commit ae2122fda8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 3 deletions

View file

@ -215,23 +215,27 @@ def _json_byte_len(value: Any) -> int:
def _compact_openai_tool_schema_value(
value: Any,
_parent_key: str | None = None,
) -> Any:
if isinstance(value, list):
return [_compact_openai_tool_schema_value(item) for item in value]
return [_compact_openai_tool_schema_value(item, _parent_key) for item in value]
if not isinstance(value, dict):
return value
compacted: dict[str, Any] = {}
for key, child in value.items():
if key in _OPENAI_TOOL_SCHEMA_DROP_KEYS:
# Don't drop keys that are property *names* inside a JSON Schema
# `properties` object — only drop them when they are schema annotations.
# e.g. a tool with a field literally named "title" must not be stripped.
if _parent_key != "properties" and key in _OPENAI_TOOL_SCHEMA_DROP_KEYS:
continue
if key == "description" and isinstance(child, str):
compacted[key] = " ".join(child.split())
continue
compacted[key] = _compact_openai_tool_schema_value(child)
compacted[key] = _compact_openai_tool_schema_value(child, key)
return compacted

View file

@ -99,6 +99,66 @@ def test_openai_tool_schema_compaction_preserves_invocation_shape() -> None:
assert tool["parameters"]["properties"]["path"]["description"] == " ".join(verbose.split())
def test_openai_tool_schema_compaction_preserves_property_named_title() -> None:
"""Issue #759: drop-key list must not strip property *names* under `properties`.
Schema annotations like ``title: "ReadFileParameters"`` on a schema object
are safe to drop. But a tool that has a field literally called ``title``
(or ``readOnly``, ``deprecated``, etc.) must survive compaction; removing
it while leaving ``required: ["title"]`` produces an invalid strict schema
that upstream (OpenAI / Codex) rejects.
"""
payload = {
"tools": [
{
"type": "function",
"name": "eval",
"description": "Evaluate cells.",
"parameters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "EvalParameters",
"type": "object",
"properties": {
"cells": {
"type": "array",
"items": {
"type": "object",
"title": "CellItem",
"properties": {
"language": {"type": "string"},
"code": {"type": "string"},
"title": {"type": "string"},
},
"required": ["language", "code", "title"],
},
}
},
"required": ["cells"],
},
}
]
}
compacted, modified, before, after = _compact_openai_responses_tools(payload)
assert modified is True
assert after < before
params = compacted["tools"][0]["parameters"]
# Schema-level annotations are still dropped.
assert "title" not in params
assert "$schema" not in params
items = params["properties"]["cells"]["items"]
# "title" as a JSON Schema annotation on the items object is dropped.
assert "title" not in items
# "title" as a *property name* inside properties must be preserved.
assert "title" in items["properties"], (
"property named 'title' was incorrectly stripped by compaction"
)
assert items["required"] == ["language", "code", "title"]
def test_openai_tool_schema_compaction_is_deterministic() -> None:
payload = {
"tools": [