gateway: find the model where the endpoint actually names it

/v1/ocr names its engine in a multipart field called `engine`, not `model`.
The gateway only ever looked for `model`, so an OCR request carried no model as
far as it was concerned: per-model placement resolved correctly, the test
reported 'target: its own pod', and then the gateway refused with 'nothing
configures ocr to run remotely'. Two layers disagreeing about the same request.

_MODEL_FIELD maps the endpoints whose field is named something else, and the
extractor takes the field name for JSON, multipart and urlencoded alike.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 25437516
...@@ -258,8 +258,14 @@ def _any_remote_configured_uncached() -> bool: ...@@ -258,8 +258,14 @@ def _any_remote_configured_uncached() -> bool:
return False return False
def _model_from_body(body: bytes, content_type: str) -> str: def _model_from_body(body: bytes, content_type: str, field: str = "model") -> str:
"""Pull the requested model out of a JSON or multipart body.""" """Pull the requested model out of a JSON or multipart body.
``field`` because not every endpoint calls it "model": /v1/ocr names its
engine in a field called `engine`, so a per-model placement for OCR resolved
correctly and was then refused by the gateway, which could not see which
engine the request was for.
"""
if not body: if not body:
return "" return ""
ctype = (content_type or "").lower() ctype = (content_type or "").lower()
...@@ -268,18 +274,26 @@ def _model_from_body(body: bytes, content_type: str) -> str: ...@@ -268,18 +274,26 @@ def _model_from_body(body: bytes, content_type: str) -> str:
obj = json.loads(body) obj = json.loads(body)
except Exception: except Exception:
return "" return ""
return str(obj.get("model") or "") if isinstance(obj, dict) else "" return str(obj.get(field) or "") if isinstance(obj, dict) else ""
if "multipart/form-data" in ctype: if "multipart/form-data" in ctype:
m = _MULTIPART_MODEL.search(body[:262144]) pat = (_MULTIPART_MODEL if field == "model"
else re.compile(rb'name="' + field.encode() +
rb'"\r?\n(?:[^\r\n]*\r?\n)*?\r?\n([^\r\n]*)',
re.IGNORECASE))
m = pat.search(body[:262144])
if m: if m:
return m.group(1).decode("utf-8", "replace").strip() return m.group(1).decode("utf-8", "replace").strip()
if "application/x-www-form-urlencoded" in ctype: if "application/x-www-form-urlencoded" in ctype:
from urllib.parse import parse_qs from urllib.parse import parse_qs
vals = parse_qs(body.decode("utf-8", "replace")).get("model") vals = parse_qs(body.decode("utf-8", "replace")).get(field)
return (vals or [""])[0] return (vals or [""])[0]
return "" return ""
#: Endpoints whose "which model" field is not called `model`.
_MODEL_FIELD = {"/v1/ocr": "engine", "/v1/ocr/batch": "engine"}
class Target: class Target:
"""Where a request is going: a fixed URL, or a pod pool to borrow one from.""" """Where a request is going: a fixed URL, or a pod pool to borrow one from."""
...@@ -333,10 +347,11 @@ def resolve_target(path: str, method: str, query: str, body: bytes, ...@@ -333,10 +347,11 @@ def resolve_target(path: str, method: str, query: str, body: bytes,
base = file_origin(path[len("/v1/files/"):]) base = file_origin(path[len("/v1/files/"):])
return Target(url=base, reason="generated file") if base else None return Target(url=base, reason="generated file") if base else None
cap = capability_for(path) cap = capability_for(path)
model = _model_from_body(body, content_type) field = _MODEL_FIELD.get(path, "model")
model = _model_from_body(body, content_type, field)
if not model and query: if not model and query:
from urllib.parse import parse_qs from urllib.parse import parse_qs
model = (parse_qs(query).get("model") or [""])[0] model = (parse_qs(query).get(field) or [""])[0]
# Per model first, so two models of the same kind can be placed differently. # Per model first, so two models of the same kind can be placed differently.
where, detail = model_placement(model) where, detail = model_placement(model)
......
...@@ -1615,3 +1615,23 @@ def test_a_200_carrying_no_media_is_not_a_pass(): ...@@ -1615,3 +1615,23 @@ def test_a_200_carrying_no_media_is_not_a_pass():
assert _empty_result(json.dumps({"url": "/v1/files/x.mp4"}).encode(), "video") == "" assert _empty_result(json.dumps({"url": "/v1/files/x.mp4"}).encode(), "video") == ""
for cap in ("voice", "audio_gen", "video"): for cap in ("voice", "audio_gen", "video"):
assert "no media" in _empty_result(json.dumps({"ok": True}).encode(), cap) assert "no media" in _empty_result(json.dumps({"ok": True}).encode(), cap)
def test_the_gateway_finds_the_ocr_engine_field():
"""/v1/ocr names its engine in a field called `engine`, not `model`. The
gateway could not see it, so a per-model OCR placement resolved correctly
and was then refused: 'nothing configures ocr to run remotely'."""
from codai.api.remote_gateway import _model_from_body, _MODEL_FIELD
assert _MODEL_FIELD["/v1/ocr"] == "engine"
body = (b'--B\r\nContent-Disposition: form-data; name="engine"\r\n\r\nsurya\r\n'
b'--B\r\nContent-Disposition: form-data; name="file"; '
b'filename="a.png"\r\n\r\n\x89PNG\r\n--B--\r\n')
ct = "multipart/form-data; boundary=B"
assert _model_from_body(body, ct, "engine") == "surya"
# and the default field is still `model`, absent here
assert _model_from_body(body, ct) == ""
# JSON bodies honour the field too.
assert _model_from_body(b'{"engine":"doctr"}', "application/json", "engine") == "doctr"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment