mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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:
parent
0ce68dedd7
commit
ae2122fda8
2 changed files with 67 additions and 3 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue