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
|
tasks when an objective benefits from persistent Codex or Claude Code CLI
|
||||||
execution that survives browser disconnects and can resume after restarts.
|
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
|
The Jetson classifier is mandatory for AUTO selection. Switchyard may use
|
||||||
local Qwen for bounded low-risk responses and continuity, or spill to a
|
local Qwen for bounded low-risk responses and continuity, or spill to a
|
||||||
hosted provider when local capability is insufficient. Do not describe a
|
hosted provider when local capability is insufficient. Do not describe a
|
||||||
@ -296,8 +305,9 @@ data:
|
|||||||
|
|
||||||
## Atlas engineering access
|
## Atlas engineering access
|
||||||
|
|
||||||
The Atlas organization has private visibility. Its repositories are access-
|
The Atlas organization has private visibility. Repository visibility is
|
||||||
controlled as either private or Gitea-internal, never public, and are
|
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
|
canonical at `https://scm.bstein.dev/atlas/<repo>.git`. HTTPS Git
|
||||||
authentication is already supplied through `GIT_ASKPASS`. Verify the remote
|
authentication is already supplied through `GIT_ASKPASS`. Verify the remote
|
||||||
and cleanly separate pre-existing changes, create a task branch, run the
|
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
|
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]:
|
def _validate_payload(payload: Any) -> dict[str, Any]:
|
||||||
"""Allow only bounded Responses requests for the approved model catalog."""
|
"""Allow only bounded Responses requests for the approved model catalog."""
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
@ -251,6 +277,7 @@ def _validate_payload(payload: Any) -> dict[str, Any]:
|
|||||||
payload["input"] = [response_input]
|
payload["input"] = [response_input]
|
||||||
elif not isinstance(response_input, list) or not response_input:
|
elif not isinstance(response_input, list) or not response_input:
|
||||||
raise ValueError("non-empty Responses input list required")
|
raise ValueError("non-empty Responses input list required")
|
||||||
|
_normalize_input_images(payload["input"])
|
||||||
# Switchyard uses ``max_output_tokens`` to bound the tiny classifier call,
|
# Switchyard uses ``max_output_tokens`` to bound the tiny classifier call,
|
||||||
# but its Responses translation can also copy that internal option onto the
|
# but its Responses translation can also copy that internal option onto the
|
||||||
# selected provider request. The first-party subscription Codex endpoint
|
# 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():
|
def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
|
||||||
configmap = _documents(HERMES / "agent-configmap.yaml")[0]
|
configmap = _documents(HERMES / "agent-configmap.yaml")[0]
|
||||||
instructions = configmap["data"]["AGENTS.md"]
|
instructions = configmap["data"]["AGENTS.md"]
|
||||||
|
soul = configmap["data"]["SOUL.md"]
|
||||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||||
|
|
||||||
assert (
|
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 "must never complete, block, unblock, reclaim" in instructions
|
||||||
assert "task's final structured result itself" in instructions
|
assert "task's final structured result itself" in instructions
|
||||||
assert "Atlas organization has private visibility" 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 "already supplied through `GIT_ASKPASS`" in instructions
|
||||||
assert "Never call `kanban_show` without a known, non-empty task ID" 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 "bounded ad-hoc inspection and acceptance checks may" in instructions
|
||||||
assert "load implementation or TDD skills" 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():
|
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(
|
assert module._validate_payload(
|
||||||
{"model": "gpt-5.6-terra", "input": response_items}
|
{"model": "gpt-5.6-terra", "input": response_items}
|
||||||
)["input"] is 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(
|
routed = module._validate_payload(
|
||||||
{
|
{
|
||||||
"model": "route/codex/gpt-5.6-luna/low",
|
"model": "route/codex/gpt-5.6-luna/low",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user