hermes: accept routed base64 vision

This commit is contained in:
jenkins 2026-08-16 07:12:11 -03:00
parent 8ada8e062e
commit 8f7b57419a
3 changed files with 57 additions and 7 deletions

View File

@ -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: "20260816-routed-vision-v2"
ai.bstein.dev/config-rev: "20260816-routed-vision-v3"
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9010"

View File

@ -223,6 +223,34 @@ def _upstream_headers(token: str) -> dict[str, str]:
def _normalize_input_images(response_input: list[Any]) -> None:
"""Normalize Chat-Completions image parts for the Codex Responses API."""
def image_url_value(part: dict[str, Any]) -> tuple[str | None, str | None]:
"""Return a Responses URL from URL-object or base64-source forms."""
raw = part.get("image_url")
if isinstance(raw, str):
return raw, None
candidates = [raw, part.get("source")]
for candidate in candidates:
if not isinstance(candidate, dict):
continue
detail = candidate.get("detail")
url = candidate.get("url")
if isinstance(url, str) and url.strip():
return url, detail if isinstance(detail, str) else None
data = candidate.get("data")
media_type = candidate.get("media_type") or candidate.get("mime_type")
if (
isinstance(data, str)
and data
and isinstance(media_type, str)
and media_type.startswith("image/")
and all(character not in media_type for character in "\r\n;, ")
):
return f"data:{media_type};base64,{data}", (
detail if isinstance(detail, str) else None
)
return None, None
for item in response_input:
if not isinstance(item, dict):
continue
@ -231,20 +259,19 @@ def _normalize_input_images(response_input: list[Any]) -> None:
continue
for part in content:
if not isinstance(part, dict) or part.get("type") not in {
"image",
"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
image_url, detail = image_url_value(part)
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
part.pop("source", None)
def _validate_payload(payload: Any) -> dict[str, Any]:

View File

@ -801,6 +801,29 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch):
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
"detail": "auto",
}
switchyard_base64_items = [
{
"role": "user",
"content": [
{"type": "input_text", "text": "What color is this?"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "cHJpdmF0ZQ==",
},
},
],
}
]
normalized_base64 = module._validate_payload(
{"model": "gpt-5.6-terra", "input": switchyard_base64_items}
)["input"][0]["content"][1]
assert normalized_base64 == {
"type": "input_image",
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
}
with pytest.raises(ValueError, match="non-empty Responses image URL"):
module._validate_payload(
{