diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index b7d90f339..aea2e94dd 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -25,7 +25,7 @@ spec: ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available - ai.bstein.dev/config-rev: "20260811-image-routing" + ai.bstein.dev/config-rev: "20260811-switchyard-responses-input" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: hermes-agent vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens diff --git a/services/hermes/scripts/codex_broker.py b/services/hermes/scripts/codex_broker.py index cfcc84078..99ff93e71 100644 --- a/services/hermes/scripts/codex_broker.py +++ b/services/hermes/scripts/codex_broker.py @@ -105,6 +105,25 @@ def _validate_payload(payload: Any) -> dict[str, Any]: if not model.startswith("gpt-"): raise ValueError("unsupported Codex model") payload["model"] = model + response_input = payload.get("input") + if isinstance(response_input, str): + if not response_input.strip(): + raise ValueError("non-empty Responses input required") + # Switchyard accepts OpenAI Chat Completions requests and translates + # their final text to a scalar Responses input. The first-party Codex + # endpoint is stricter than the public Responses API and only accepts + # a list of typed input items. + payload["input"] = [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": response_input}], + } + ] + elif isinstance(response_input, dict): + payload["input"] = [response_input] + elif not isinstance(response_input, list) or not response_input: + raise ValueError("non-empty Responses input list required") # Tenant conversations must not enter the owner's server-side history. payload["store"] = False payload["stream"] = True diff --git a/testing/tests/test_hermes_chat_quality.py b/testing/tests/test_hermes_chat_quality.py index 423d8b52d..9abe0bc39 100644 --- a/testing/tests/test_hermes_chat_quality.py +++ b/testing/tests/test_hermes_chat_quality.py @@ -499,16 +499,48 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch): assert module._authorized("Bearer wrong") is False assert module._real_model("route/codex/gpt-5.6-sol/xhigh") == "gpt-5.6-sol" payload = module._validate_payload( - {"model": "gpt-5.6-terra", "store": True, "stream": False} + { + "model": "gpt-5.6-terra", + "input": "route this chat turn", + "store": True, + "stream": False, + } ) assert payload["store"] is False assert payload["stream"] is True + assert payload["input"] == [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "route this chat turn"}], + } + ] + response_item = { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "keep this item"}], + } + assert module._validate_payload( + {"model": "gpt-5.6-terra", "input": response_item} + )["input"] == [response_item] + response_items = [response_item] + assert module._validate_payload( + {"model": "gpt-5.6-terra", "input": response_items} + )["input"] is response_items routed = module._validate_payload( - {"model": "route/codex/gpt-5.6-luna/low", "stream": False} + { + "model": "route/codex/gpt-5.6-luna/low", + "input": "use the low route", + "stream": False, + } ) assert routed["model"] == "gpt-5.6-luna" with pytest.raises(ValueError, match="unsupported Codex model"): - module._validate_payload({"model": "unapproved-model"}) + module._validate_payload({"model": "unapproved-model", "input": "hello"}) + with pytest.raises(ValueError, match="non-empty Responses input"): + module._validate_payload({"model": "gpt-5.6-terra", "input": ""}) + with pytest.raises(ValueError, match="non-empty Responses input list"): + module._validate_payload({"model": "gpt-5.6-terra", "input": []}) auth_dir = tmp_path / ".codex" auth_dir.mkdir()