hermes: accept routed base64 vision
This commit is contained in:
parent
8ada8e062e
commit
8f7b57419a
@ -25,7 +25,7 @@ spec:
|
|||||||
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
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/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/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/scrape: "true"
|
||||||
prometheus.io/path: /metrics
|
prometheus.io/path: /metrics
|
||||||
prometheus.io/port: "9010"
|
prometheus.io/port: "9010"
|
||||||
|
|||||||
@ -223,6 +223,34 @@ def _upstream_headers(token: str) -> dict[str, str]:
|
|||||||
|
|
||||||
def _normalize_input_images(response_input: list[Any]) -> None:
|
def _normalize_input_images(response_input: list[Any]) -> None:
|
||||||
"""Normalize Chat-Completions image parts for the Codex Responses API."""
|
"""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:
|
for item in response_input:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
@ -231,20 +259,19 @@ def _normalize_input_images(response_input: list[Any]) -> None:
|
|||||||
continue
|
continue
|
||||||
for part in content:
|
for part in content:
|
||||||
if not isinstance(part, dict) or part.get("type") not in {
|
if not isinstance(part, dict) or part.get("type") not in {
|
||||||
|
"image",
|
||||||
"image_url",
|
"image_url",
|
||||||
"input_image",
|
"input_image",
|
||||||
}:
|
}:
|
||||||
continue
|
continue
|
||||||
image_url = part.get("image_url")
|
image_url, detail = image_url_value(part)
|
||||||
if isinstance(image_url, dict):
|
if isinstance(detail, str) and detail and "detail" not in part:
|
||||||
detail = image_url.get("detail")
|
part["detail"] = 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():
|
if not isinstance(image_url, str) or not image_url.strip():
|
||||||
raise ValueError("non-empty Responses image URL required")
|
raise ValueError("non-empty Responses image URL required")
|
||||||
part["type"] = "input_image"
|
part["type"] = "input_image"
|
||||||
part["image_url"] = image_url
|
part["image_url"] = image_url
|
||||||
|
part.pop("source", None)
|
||||||
|
|
||||||
|
|
||||||
def _validate_payload(payload: Any) -> dict[str, Any]:
|
def _validate_payload(payload: Any) -> dict[str, Any]:
|
||||||
|
|||||||
@ -801,6 +801,29 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch):
|
|||||||
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||||
"detail": "auto",
|
"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"):
|
with pytest.raises(ValueError, match="non-empty Responses image URL"):
|
||||||
module._validate_payload(
|
module._validate_payload(
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user