hermes: normalize vision requests for Codex
This commit is contained in:
parent
3469c340c5
commit
5dd13368c6
@ -185,6 +185,15 @@ data:
|
||||
tasks when an objective benefits from persistent Codex or Claude Code CLI
|
||||
execution that survives browser disconnects and can resume after restarts.
|
||||
|
||||
Project checkouts may provide nearer repository instructions, but they do
|
||||
not replace these coordinator-wide rules. Never call `kanban_show` without
|
||||
a known, non-empty task ID. Ad-hoc inspection and acceptance checks do not
|
||||
need a synthetic Kanban lookup, and must load a skill only when its workflow
|
||||
materially applies. Atlas HTTPS Git authentication is already supplied by
|
||||
the runtime-only `GIT_ASKPASS`; use it without reading or exposing the
|
||||
credential. Coordinator guidance lives at
|
||||
`/opt/data/workspace/AGENTS.md` when more detail is needed.
|
||||
|
||||
The Jetson classifier is mandatory for AUTO selection. Switchyard may use
|
||||
local Qwen for bounded low-risk responses and continuity, or spill to a
|
||||
hosted provider when local capability is insufficient. Do not describe a
|
||||
@ -296,8 +305,9 @@ data:
|
||||
|
||||
## Atlas engineering access
|
||||
|
||||
The Atlas organization has private visibility. Its repositories are access-
|
||||
controlled as either private or Gitea-internal, never public, and are
|
||||
The Atlas organization has private visibility. Repository visibility is
|
||||
preserved per project and may be public or private; do not infer a
|
||||
repository's visibility from the organization setting. Repositories are
|
||||
canonical at `https://scm.bstein.dev/atlas/<repo>.git`. HTTPS Git
|
||||
authentication is already supplied through `GIT_ASKPASS`. Verify the remote
|
||||
and cleanly separate pre-existing changes, create a task branch, run the
|
||||
|
||||
@ -221,6 +221,32 @@ def _upstream_headers(token: str) -> dict[str, str]:
|
||||
return headers
|
||||
|
||||
|
||||
def _normalize_input_images(response_input: list[Any]) -> None:
|
||||
"""Normalize Chat-Completions image parts for the Codex Responses API."""
|
||||
for item in response_input:
|
||||
if not isinstance(item, dict) or item.get("type") != "message":
|
||||
continue
|
||||
content = item.get("content")
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for part in content:
|
||||
if not isinstance(part, dict) or part.get("type") not in {
|
||||
"image_url",
|
||||
"input_image",
|
||||
}:
|
||||
continue
|
||||
image_url = part.get("image_url")
|
||||
if isinstance(image_url, dict):
|
||||
detail = image_url.get("detail")
|
||||
image_url = image_url.get("url")
|
||||
if isinstance(detail, str) and detail and "detail" not in part:
|
||||
part["detail"] = detail
|
||||
if not isinstance(image_url, str) or not image_url.strip():
|
||||
raise ValueError("non-empty Responses image URL required")
|
||||
part["type"] = "input_image"
|
||||
part["image_url"] = image_url
|
||||
|
||||
|
||||
def _validate_payload(payload: Any) -> dict[str, Any]:
|
||||
"""Allow only bounded Responses requests for the approved model catalog."""
|
||||
if not isinstance(payload, dict):
|
||||
@ -251,6 +277,7 @@ def _validate_payload(payload: Any) -> dict[str, Any]:
|
||||
payload["input"] = [response_input]
|
||||
elif not isinstance(response_input, list) or not response_input:
|
||||
raise ValueError("non-empty Responses input list required")
|
||||
_normalize_input_images(payload["input"])
|
||||
# Switchyard uses ``max_output_tokens`` to bound the tiny classifier call,
|
||||
# but its Responses translation can also copy that internal option onto the
|
||||
# selected provider request. The first-party subscription Codex endpoint
|
||||
|
||||
@ -76,6 +76,7 @@ def test_chat_config_enables_real_research_compute_and_delegation():
|
||||
def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
|
||||
configmap = _documents(HERMES / "agent-configmap.yaml")[0]
|
||||
instructions = configmap["data"]["AGENTS.md"]
|
||||
soul = configmap["data"]["SOUL.md"]
|
||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||
|
||||
assert (
|
||||
@ -89,11 +90,15 @@ def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
|
||||
assert "must never complete, block, unblock, reclaim" in instructions
|
||||
assert "task's final structured result itself" in instructions
|
||||
assert "Atlas organization has private visibility" in instructions
|
||||
assert "private or Gitea-internal, never public" in instructions
|
||||
assert "may be public or private" in instructions
|
||||
assert "do not infer a\nrepository's visibility" in instructions
|
||||
assert "already supplied through `GIT_ASKPASS`" in instructions
|
||||
assert "Never call `kanban_show` without a known, non-empty task ID" in instructions
|
||||
assert "bounded ad-hoc inspection and acceptance checks may" in instructions
|
||||
assert "load implementation or TDD skills" in instructions
|
||||
assert "Never call `kanban_show` without\na known, non-empty task ID" in soul
|
||||
assert "must load a skill only when its workflow\nmaterially applies" in soul
|
||||
assert "runtime-only `GIT_ASKPASS`" in soul
|
||||
|
||||
|
||||
def test_agent_image_completes_parked_kanban_tasks_atomically():
|
||||
@ -751,6 +756,44 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch):
|
||||
assert module._validate_payload(
|
||||
{"model": "gpt-5.6-terra", "input": response_items}
|
||||
)["input"] is response_items
|
||||
image_items = [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
assert module._validate_payload(
|
||||
{"model": "gpt-5.6-terra", "input": image_items}
|
||||
)["input"][0]["content"][1] == {
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "high",
|
||||
}
|
||||
with pytest.raises(ValueError, match="non-empty Responses image URL"):
|
||||
module._validate_payload(
|
||||
{
|
||||
"model": "gpt-5.6-terra",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_image", "image_url": {"detail": "high"}}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
routed = module._validate_payload(
|
||||
{
|
||||
"model": "route/codex/gpt-5.6-luna/low",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user