Python preflight for a Sume bulk body: count, keys, bytes, caps
Check a Sume bulk-run body offline before the POST: 1 to 100 items, concurrency 1 to 16, 64 input keys, 2 MiB input, 4 MiB body and a spend cap in range.

A bulk create validates every item before it makes a queue, so one bad row out of 100 turns the whole call into a 400 invalid_request with the bad position in details.index. You can catch the same mistakes on your own machine in a short function: concurrency from 1 to 16, one to 100 items, each item carrying at least one of instruction, input, previous_run_id or attachments, input an object of at most 64 top-level keys and 2 MiB, a spend cap above 0 and at most 500, and a whole body under 4 MiB.
The rules below come from the Format API reference. The function does not replace the server's check, which also knows your Format's schema and your plan. It exists so a 3,000-row spreadsheet fails in your terminal in a second, with every problem listed, instead of one problem at a time through the API.
The checker
Pass a JSON file that holds the items array. The exit status is 1 when anything is wrong, so it also works as a CI step in front of the real call.
import json, sys
def size(x):
return len(json.dumps(x, separators=(",", ":"), ensure_ascii=False).encode())
def check(items, concurrency=4):
errs = []
if not 1 <= concurrency <= 16:
errs.append("concurrency must be 1 to 16")
if not 1 <= len(items) <= 100:
errs.append(f"items must be 1 to 100, got {len(items)}")
for i, it in enumerate(items):
if not any(k in it for k in ("instruction", "input", "previous_run_id", "attachments")):
errs.append(f"item {i}: no instruction, input, previous_run_id or attachments")
if "output_schema" in it and "response_format" in it:
errs.append(f"item {i}: output_schema and response_format together")
cap = it.get("generation_spend_cap_usd")
if cap is not None and not 0 < cap <= 500:
errs.append(f"item {i}: spend cap must be above 0 and at most 500")
inp = it.get("input")
if inp is not None and not isinstance(inp, dict):
errs.append(f"item {i}: input must be an object")
elif inp and (len(inp) > 64 or size(inp) > 2097152):
errs.append(f"item {i}: input over 64 keys or 2097152 bytes")
if size({"concurrency": concurrency, "items": items}) > 4 * 1024 * 1024:
errs.append("body over 4 MiB")
return errs
if __name__ == "__main__":
problems = check(json.load(open(sys.argv[1])))
print("\n".join(problems) or "ok")
sys.exit(1 if problems else 0)What each check mirrors
The input size is measured on the compact serialization: no spaces after commas or colons, and UTF-8 bytes rather than characters. That is how the docs state the 2097152-byte limit, and it is why size() passes separators and ensure_ascii=False. A default json.dumps pads with spaces and escapes non-ASCII characters, which makes a payload look larger than the server counts it.
The two body-level limits behave differently. The 2 MiB input limit is a 400. The 4 MiB request limit is a 413 payload_too_large with details.limit_bytes, and the fix is to send media by URL, or by a shared asset id, rather than inline.
The spend cap check accepts any value above 0 and up to 500. A cap of 0 is rejected, and null is treated as the maximum of 500. Leave the field off and the run inherits the Format's own cap instead.
| Rule | Limit | API answer when broken |
|---|---|---|
concurrency | 1 to 16 | 400 invalid_request |
items | 1 to 100 | 400 invalid_request |
| Item content | at least one of instruction, input, previous_run_id, attachments | 400 with details.index |
input shape | object, at most 64 top-level keys | 400 with details.index |
input size | 2097152 compact UTF-8 bytes | 400 with details.index |
generation_spend_cap_usd | above 0, at most 500 | 400 |
| Request body | 4 MiB | 413 payload_too_large |
What it cannot know
The checker does not look inside attachment URLs. One dead URL in an item still fails the whole create, so if your rows carry media links, fetch them first. It also cannot know whether the wallet can fund the batch: that is a 402 insufficient_credits at create, and nothing runs.
A clean result is permission to send, not a promise it will succeed. A queue that is accepted can still end with failed items, since each child is its own run.
Sources
Related posts
More in Developers
- Sume job webhook receiver in plain Python, no framework
A 30-line stdlib http.server receiver for Sume job webhooks: raw-body HMAC, 300-second window, rotation-safe header, refuses an empty secret, answers 204.
- Python ThreadPoolExecutor: submit 12 Wan 3.0 clips, four at a time
Stdlib-only Python: ThreadPoolExecutor with 4 workers posts Wan 3.0 jobs to /v1/videos, one Idempotency-Key per shot, results printed in completion order.
- Python urllib: honor retry-after on a Sume 429, back off without it
A stdlib retry for GET calls that sleeps for retry-after when the 429 carries it and for a capped exponential delay when it does not, with jitter.
- Python urllib: POST /v1/images, 200 or 202, after Imagen 4 Fast
imagen-4.0-fast-generate-001 ended Aug 17. A stdlib Python call to Sume's image route that reads the status code, then polls the job when the answer is 202.
Written by Sume