From 5a17614b51cdbb8a14e0e5ef51ef792e7a6ddef3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 13 Jul 2026 02:09:15 +0000 Subject: [PATCH] Reject native batch seeds outside the JSON-safe range --- studio/backend/models/inference.py | 15 ++++++++++++++ studio/backend/tests/test_diffusion_routes.py | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index a6800087ec..862942e883 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -2069,6 +2069,21 @@ class DiffusionGenerateRequest(BaseModel): raise ValueError("must be a multiple of 16") return value + @model_validator(mode = "after") + def _batch_seeds_json_safe(self) -> "DiffusionGenerateRequest": + # A batch derives per-image seeds as seed, seed+1, ... seed+batch_size-1 (sd.cpp and + # diffusers both advance the seed per image). The base seed is bounded to 2**53-1 so it + # round-trips through the JSON gallery recipe, but the derived top-of-batch seed is not: + # with an explicit seed near the cap it can exceed Number.MAX_SAFE_INTEGER, where the + # frontend rounds it and a restored recipe replays a different image. Reject at the + # boundary so an API client can't persist an unreplayable seed. + if self.seed is not None and self.seed + self.batch_size - 1 > 2**53 - 1: + raise ValueError( + "seed + batch_size - 1 must not exceed 2**53 - 1 so every per-image seed " + "stays JSON-safe (lower the seed or the batch_size)" + ) + return self + class GalleryImage(BaseModel): """A persisted image's full generation recipe (embedded in the PNG too).""" diff --git a/studio/backend/tests/test_diffusion_routes.py b/studio/backend/tests/test_diffusion_routes.py index 34b4061fd0..a5a825ee00 100644 --- a/studio/backend/tests/test_diffusion_routes.py +++ b/studio/backend/tests/test_diffusion_routes.py @@ -340,6 +340,26 @@ def test_generate_rejects_non_multiple_of_16(client): assert ok.status_code == 200 +def test_generate_rejects_batch_seed_past_json_safe_range(client): + client.post( + "/api/inference/images/load", json = {"model_path": "x/z-image", "gguf_filename": "q.gguf"} + ) + # An explicit seed at the JS-safe cap with a batch derives per-image seeds + # (seed+1 ...) that exceed Number.MAX_SAFE_INTEGER and no longer round-trip + # through the gallery JSON recipe, so the request is rejected at the boundary. + over = client.post( + "/api/inference/images/generate", + json = {"prompt": "p", "seed": 2**53 - 1, "batch_size": 2}, + ) + assert over.status_code == 422 + # The top-of-batch seed lands exactly on the cap: still JSON-safe, so accepted. + ok = client.post( + "/api/inference/images/generate", + json = {"prompt": "p", "seed": 2**53 - 2, "batch_size": 2}, + ) + assert ok.status_code == 200 + + def test_non_gguf_load_restricted_to_unsloth(client): # gguf_filename is optional now; with none, the load is a full-pipeline kind, which # is gated to unsloth/* repos. A non-unsloth repo (no filename) is rejected -> 400.