mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix: strip Codex-hostile top-level schema combinators
This commit is contained in:
parent
69d025e4a7
commit
3924cb408b
2 changed files with 99 additions and 0 deletions
|
|
@ -302,3 +302,61 @@ def test_strip_none_returns_zero():
|
||||||
tools, stripped = strip_pattern_and_format(None)
|
tools, stripped = strip_pattern_and_format(None)
|
||||||
assert tools is None
|
assert tools is None
|
||||||
assert stripped == 0
|
assert stripped == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_top_level_allof_stripped_for_codex_backend_compat():
|
||||||
|
"""OpenAI Codex backend rejects top-level allOf/oneOf/anyOf/enum/not."""
|
||||||
|
tools = [_tool("memory", {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {"type": "string", "enum": ["add", "replace"]},
|
||||||
|
"content": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["action"],
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {"properties": {"action": {"const": "add"}}, "required": ["action"]},
|
||||||
|
"then": {"required": ["content"]},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})]
|
||||||
|
out = sanitize_tool_schemas(tools)
|
||||||
|
params = out[0]["function"]["parameters"]
|
||||||
|
assert "allOf" not in params
|
||||||
|
# Properties and required survive.
|
||||||
|
assert params["required"] == ["action"]
|
||||||
|
assert "content" in params["properties"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_top_level_oneof_anyof_enum_not_stripped():
|
||||||
|
"""All five forbidden top-level combinators are dropped."""
|
||||||
|
tools = [_tool("t", {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"x": {"type": "string"}},
|
||||||
|
"oneOf": [{"required": ["x"]}],
|
||||||
|
"anyOf": [{"required": ["x"]}],
|
||||||
|
"enum": ["bogus-top-level"],
|
||||||
|
"not": {"required": ["y"]},
|
||||||
|
})]
|
||||||
|
out = sanitize_tool_schemas(tools)
|
||||||
|
params = out[0]["function"]["parameters"]
|
||||||
|
for key in ("oneOf", "anyOf", "enum", "not"):
|
||||||
|
assert key not in params, f"{key} should be stripped from top level"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_allof_preserved():
|
||||||
|
"""Combinators inside a property's schema are preserved (only top is strict)."""
|
||||||
|
tools = [_tool("t", {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"mode": {"type": "string"}},
|
||||||
|
"allOf": [{"required": ["mode"]}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})]
|
||||||
|
out = sanitize_tool_schemas(tools)
|
||||||
|
nested = out[0]["function"]["parameters"]["properties"]["config"]
|
||||||
|
assert "allOf" in nested
|
||||||
|
assert nested["allOf"] == [{"required": ["mode"]}]
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,47 @@ def _sanitize_single_tool(tool: dict) -> dict:
|
||||||
# argument coercion (``model_tools._schema_allows_null``) can still
|
# argument coercion (``model_tools._schema_allows_null``) can still
|
||||||
# map a model-emitted ``"null"`` string to Python ``None``.
|
# map a model-emitted ``"null"`` string to Python ``None``.
|
||||||
fn["parameters"] = strip_nullable_unions(fn["parameters"], keep_nullable_hint=True)
|
fn["parameters"] = strip_nullable_unions(fn["parameters"], keep_nullable_hint=True)
|
||||||
|
# Strip top-level combinators that strict backends (OpenAI's Codex
|
||||||
|
# endpoint at chatgpt.com/backend-api/codex) reject outright. Nested
|
||||||
|
# combinators inside properties are preserved.
|
||||||
|
fn["parameters"] = _strip_top_level_combinators(
|
||||||
|
fn["parameters"], path=fn.get("name", "<tool>")
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_TOP_LEVEL_FORBIDDEN_KEYS = ("allOf", "anyOf", "oneOf", "enum", "not")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_top_level_combinators(params: dict, *, path: str = "<tool>") -> dict:
|
||||||
|
"""Drop combinator keywords from the top-level of a function parameters schema.
|
||||||
|
|
||||||
|
OpenAI's Codex backend (``chatgpt.com/backend-api/codex``) is stricter
|
||||||
|
than the public Functions API and rejects requests with::
|
||||||
|
|
||||||
|
Invalid schema for function 'X': schema must have type 'object' and
|
||||||
|
not have 'oneOf'/'anyOf'/'allOf'/'enum'/'not' at the top level.
|
||||||
|
|
||||||
|
These keywords are typically used for conditional required-fields hints
|
||||||
|
(``allOf: [{if: ..., then: {required: [...]}}]``). Removing them at the
|
||||||
|
top level discards the hint but does not change which argument *values*
|
||||||
|
are valid — the tool handler always re-validates required fields.
|
||||||
|
|
||||||
|
Only the *top* level is stripped; combinators nested inside a property's
|
||||||
|
schema are preserved (the strict rule only applies to the outermost
|
||||||
|
parameters object).
|
||||||
|
"""
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
return params
|
||||||
|
out = dict(params)
|
||||||
|
for key in _TOP_LEVEL_FORBIDDEN_KEYS:
|
||||||
|
if key in out:
|
||||||
|
logger.debug(
|
||||||
|
"schema_sanitizer[%s]: stripped top-level %r combinator "
|
||||||
|
"from tool parameters (strict-backend compat)",
|
||||||
|
path, key,
|
||||||
|
)
|
||||||
|
out.pop(key, None)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue