fix(intern-decision-serve): 0.1.2 treats empty/null images as absent

Audit finding on 0.1.1: a Jev client that always sends an images array with no
images was rejected for nothing. Only a non-empty value is 422 now. Also aligns
the contract's response example with the wire (model is the name@revision
string, not an object).

Re-accepted live: images []/null -> 200, ["a.png"] -> 422; JevBench all 202/231,
hard 83/111, 0 changed rows across the bench's r1..r4 (924).
This commit is contained in:
vh
2026-09-30 13:04:31 -07:00
parent da50696fe8
commit 1866c003e8
14 changed files with 2893 additions and 11 deletions
@@ -32,7 +32,7 @@ Every POST takes and returns JSON and needs `Authorization: Bearer <token>`. `GE
| GET | `/health` | none | 200 `{status: "ok", model, vram_cap_gib, max_tokens, max_decisions, max_questions_per_call: 16, chunking, workloads: []}` |
| POST | `/decide` | `{id, state, question, options[2..16], orderings?, workload?}` | 200 one decision result |
| POST | `/decide/shared` | `{state, decisions: [{id, question, options, orderings?}], workload?}` | 200 `{results: [...], timing: {...}}`, results in request order |
| POST | `/v1/systemone` | `{state, model?, questions: {"<id>": {type, instructions, criteria?}}, images? (rejected)}` | 200 the engine's own response (`answers`, `usage`, `model`) — see § POST /v1/systemone |
| POST | `/v1/systemone` | `{state, model?, questions: {"<id>": {type, instructions, criteria?}}, images? (422 iff non-empty)}` | 200 the engine's own response (`answers`, `usage`, `model`) — see § POST /v1/systemone |
`options` items are `{id, description}`. Validation is SemIf's, re-stated here because SemIf is
gone. `id` and `question` are nonempty strings. `state` is a nonempty string, object or array,
@@ -50,11 +50,11 @@ the answers.
```
request {"state": ..., "model": "<ignored>",
"questions": {"<id>": {"type": "noul"|"choice"|"score", "instructions": ..., "criteria": ...}},
"images": <rejected if present>}
"images": <422 iff a non-empty value; [] and null count as absent>}
response {"answers": {"<id>": {"type": "noul", "noul": p, ...}
| {"type": "choice", "choice": label, "probabilities": {...}, ...}
| {"type": "score", "score": v, "probabilities": {...}, ...}},
"usage": {...}, "model": {"name": ..., "revision": ...}}
"usage": {...}, "model": "<name>@<revision>"}
```
- `model` is accepted with **any** value (Jev clients send `"jev-latest"`) and ignored. The
@@ -67,9 +67,11 @@ response {"answers": {"<id>": {"type": "noul", "noul": p, ...}
`calibration`, `backend`) ride along — additive fields Jev clients ignore.
- **Limits, all enforced before the forward pass:** 1..16 questions per request (NO chunking —
in Jev semantics the questions of one call share one prompt and chunking would silently
change that); 422 with a clear message above 16. `images` present → 422 `images not
supported` (the vision tower is dropped, INV-7). Over `MAX_TOKENS` → 422, the model's own
pre-forward check. The same bearer token (401), the same single inference thread, the same
change that); 422 with a clear message above 16. A non-empty `images` → 422 `images not
supported` (the vision tower is dropped, INV-7); `[]` and `null` count as absent — a client
that always sends the field must not be rejected for nothing (audit finding 2026-09-30).
Over `MAX_TOKENS` → 422, the model's own pre-forward check. The same bearer token (401),
the same single inference thread, the same
`MAX_QUEUE` (429), `MAX_BODY_BYTES` (413), VRAM cap with 503 `out_of_memory` + recovery.
- Per-question shape validation (type must be noul/choice/score; choice criteria an object;
score criteria a list/object; 1..16 options; nonempty state; unique field names) is the