fix(cron): drop top-level oneOf so OpenAI Codex/Responses accept tool schema

PR #3125 added a top-level `oneOf` branch to `_CRON_PARAMETERS` to
advertise per-action required fields. OpenAI Codex/Responses rejects
`oneOf`/`anyOf`/`allOf`/`enum`/`not` at the root of function
parameters, so any agent that registers the cron tool now fails to
start with:

    HTTP 400: Invalid schema for function 'cron': schema must have
    type 'object' and not have 'oneOf'/'anyOf'/'allOf'/'enum'/'not'
    at the top level.

Remove the top-level `oneOf`. The original intent of #3125 (stop LLMs
from looping on the #3113 contract mismatch) is preserved by:

  - `validate_params` — runtime-enforces `message` for `action='add'`
    and `job_id` for `action='remove'`
  - field descriptions — each schema field already flags
    "REQUIRED when action='...'" so the LLM sees the contract

The regression test is updated to lock the invariant in the other
direction: the top-level schema must not contain
`oneOf`/`anyOf`/`allOf`/`not`, and the REQUIRED hints must stay on
`message` and `job_id`.

Verified:
  - tests/cron/              70 passed
  - tests/agent/test_loop_cron_timezone.py + tests/providers/  232 passed

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
coldxiangyu 2026-04-19 11:07:34 +08:00 committed by Xubin Ren
parent 97ae9cb318
commit 7527961b19
2 changed files with 17 additions and 39 deletions

View File

@ -43,30 +43,12 @@ _CRON_PARAMETERS = tool_parameters_schema(
required=["action"],
description=(
"Action-specific parameters: add requires a non-empty message plus one schedule "
"(every_seconds, cron_expr, or at); remove requires job_id; list only needs action."
"(every_seconds, cron_expr, or at); remove requires job_id; list only needs action. "
"Per-action requirements are enforced at runtime (see field descriptions) so the "
"top-level schema stays compatible with providers (e.g. OpenAI Codex/Responses) that "
"reject oneOf/anyOf/allOf/enum/not at the root of function parameters."
),
)
_CRON_PARAMETERS["oneOf"] = [
{
"properties": {
"action": {"enum": ["add"]},
"message": {"type": "string", "minLength": 1},
},
"required": ["action", "message"],
},
{
"properties": {
"action": {"enum": ["list"]},
},
"required": ["action"],
},
{
"properties": {
"action": {"enum": ["remove"]},
},
"required": ["action", "job_id"],
},
]
@tool_parameters(_CRON_PARAMETERS)

View File

@ -348,24 +348,20 @@ def test_add_job_can_disable_delivery(tmp_path) -> None:
def test_cron_schema_advertises_action_specific_requirements(tmp_path) -> None:
tool = _make_tool(tmp_path)
# Only ``action`` is required at the schema root — per-action requirements
# are enforced at runtime via ``validate_params`` and surfaced to the LLM
# through field descriptions. We intentionally do NOT set top-level
# ``oneOf``/``anyOf``/``allOf``/``enum``/``not``: OpenAI Codex/Responses
# reject those at the root of function parameters (#3265 regression).
assert tool.parameters["required"] == ["action"]
assert tool.parameters["oneOf"] == [
{
"properties": {
"action": {"enum": ["add"]},
"message": {"type": "string", "minLength": 1},
},
"required": ["action", "message"],
},
{
"properties": {"action": {"enum": ["list"]}},
"required": ["action"],
},
{
"properties": {"action": {"enum": ["remove"]}},
"required": ["action", "job_id"],
},
]
for disallowed in ("oneOf", "anyOf", "allOf", "not"):
assert disallowed not in tool.parameters, (
f"Top-level '{disallowed}' is rejected by OpenAI Codex/Responses tool schemas"
)
message_desc = tool.parameters["properties"]["message"]["description"]
assert "REQUIRED" in message_desc and "action='add'" in message_desc
job_id_desc = tool.parameters["properties"]["job_id"]["description"]
assert "REQUIRED" in job_id_desc and "action='remove'" in job_id_desc
def test_validate_params_requires_message_only_for_add(tmp_path) -> None: