Commit 0518b34 for stable-diffusion.cpp
commit 0518b34374761cb82aeda5ad3c2f0686fdad2cec
Author: leejet <leejet714@gmail.com>
Date: Wed Oct 21 00:17:30 2026 +0800
feat(server): add recipe-based model management (#2176)
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3814594
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+/start-server.sh text eol=lf
+/start-server.bat text eol=crlf
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b717e56..54582e8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,6 +26,8 @@ on:
"ggml",
"examples/server/frontend",
"examples/server/frontend/**",
+ "start-server.sh",
+ "start-server.bat",
]
pull_request:
types: [opened, synchronize, reopened]
@@ -44,6 +46,8 @@ on:
"ggml",
"examples/server/frontend",
"examples/server/frontend/**",
+ "start-server.sh",
+ "start-server.bat",
]
env:
@@ -107,7 +111,8 @@ jobs:
run: |
cp ggml/LICENSE ./build/bin/ggml.txt
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
- zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/*
+ chmod +x start-server.sh
+ zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* ./start-server.sh ./start-server.bat
- name: Upload artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
@@ -170,7 +175,8 @@ jobs:
run: |
cp ggml/LICENSE ./build/bin/ggml.txt
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
- zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-vulkan.zip ./build/bin/*
+ chmod +x start-server.sh
+ zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-vulkan.zip ./build/bin/* ./start-server.sh ./start-server.bat
- name: Upload artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
@@ -325,7 +331,8 @@ jobs:
run: |
cp ggml/LICENSE ./build/bin/ggml.txt
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
- zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/*
+ chmod +x start-server.sh
+ zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* ./start-server.sh ./start-server.bat
- name: Upload artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
@@ -420,7 +427,7 @@ jobs:
ls .\build\bin
throw "Can't find stable-diffusion.dll"
}
- 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-${{ matrix.build }}-x64.zip $filePath
+ 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-${{ matrix.build }}-x64.zip $filePath .\start-server.sh .\start-server.bat
- name: Copy and pack Cuda runtime
id: pack_cuda_runtime
@@ -575,7 +582,7 @@ jobs:
- name: Pack artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
run: |
- 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\*
+ 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\* .\start-server.sh .\start-server.bat
- name: Upload artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
@@ -737,7 +744,8 @@ jobs:
run: |
cp ggml/LICENSE ./build/bin/ggml.txt
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
- zip -y -r sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip ./build/bin
+ chmod +x start-server.sh
+ zip -y -r sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip ./build/bin ./start-server.sh ./start-server.bat
- name: Upload artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
diff --git a/.gitignore b/.gitignore
index 80599b3..b55ea0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,10 @@ tests/
.cache/
*.swp
*.bat
+!/start-server.bat
+/start-server.local.sh
+/start-server.local.bat
+/start-server.local.ps1
*.bin
*.exe
*.gguf
diff --git a/AGENTS.md b/AGENTS.md
index 4953e22..a480408 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -83,6 +83,8 @@ documentation.
and debugging.
* `examples/common/` - shared example support code.
* `examples/server/` - server application built on top of the library.
+ Before modifying built-in recipes, read and follow
+ [the built-in recipe policy](examples/server/recipes/README.md).
* `examples/server/frontend/` - git submodule containing independent frontend
code. Avoid modifying it unless explicitly requested.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f4c067..ca64ad1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,6 +33,17 @@ PRs should include:
The [UIs list in the README](README.md#uis) only includes open-source projects that use `stable-diffusion.cpp` as a backend. Do not submit PRs to add closed-source UIs.
+## Server Recipes
+
+Before adding or changing a built-in server recipe, read and follow the
+[built-in recipe policy](examples/server/recipes/README.md). The policy governs
+native editing, community editing LoRAs, distinct task checkpoints, external
+adapters, native components and layer-count variants. Do not add recipes solely
+for style or acceleration LoRAs, separately stored native components, or added
+or removed model layers. Explain the task, component compatibility or capability
+difference that justifies a new recipe, and verify its required components,
+feature declarations and embedded API representation.
+
## Code Style
Format code according to the repository style before submitting changes.
diff --git a/examples/server/CMakeLists.txt b/examples/server/CMakeLists.txt
index 302a8d6..58260be 100644
--- a/examples/server/CMakeLists.txt
+++ b/examples/server/CMakeLists.txt
@@ -68,6 +68,8 @@ add_executable(${TARGET}
../common/media_io.cpp
main.cpp
runtime.cpp
+ model_catalog.cpp
+ routes_models.cpp
async_jobs.cpp
routes_index.cpp
routes_openai.cpp
@@ -88,6 +90,17 @@ else()
message(STATUS "HAVE_INDEX_HTML disabled")
endif()
+file(GLOB BUILTIN_RECIPE_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/recipes/*.json")
+set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${BUILTIN_RECIPE_FILES})
+# Keep recipes in separate literals to stay within MSVC's string size limit.
+set(SD_BUILTIN_RECIPES "")
+foreach(RECIPE_FILE IN LISTS BUILTIN_RECIPE_FILES)
+ file(READ "${RECIPE_FILE}" RECIPE_JSON)
+ string(APPEND SD_BUILTIN_RECIPES "R\"recipes(${RECIPE_JSON})recipes\",\n")
+endforeach()
+configure_file(recipes_builtin.h.in "${CMAKE_CURRENT_BINARY_DIR}/recipes_builtin.h" @ONLY)
+target_include_directories(${TARGET} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+
install(TARGETS ${TARGET} RUNTIME)
target_link_libraries(${TARGET} PRIVATE stable-diffusion ${CMAKE_THREAD_LIBS_INIT})
if(SD_WEBP)
diff --git a/examples/server/README.md b/examples/server/README.md
index 7ead1d9..1bb49a5 100644
--- a/examples/server/README.md
+++ b/examples/server/README.md
@@ -1,3 +1,248 @@
+# Model selection
+
+Start without model arguments to select models in the web UI:
+
+```sh
+sd-server --model-dir ./models
+```
+
+From a source checkout, you can also use `sh ./start-server.sh` on Linux/macOS
+or `.\start-server.bat` on Windows. The launchers look for the server relative
+to the script: beside the script, then in `bin/Release/`, `bin/`,
+`build/bin/Release/`, and `build/bin/`, in that order.
+For a different build directory, set `SD_SERVER_PATH` to the executable path:
+
+```sh
+SD_SERVER_PATH="./build_vulkan/bin/sd-server" sh ./start-server.sh --listen-port 8080
+```
+
+```powershell
+$env:SD_SERVER_PATH = ".\build_vulkan\bin\Release\sd-server.exe"
+.\start-server.bat --listen-port 8080
+```
+
+Binary release archives include both launchers. Keep the launchers under version
+control and put personal settings in a separate file beside them:
+
+- Windows batch: `start-server.local.bat`
+- Linux/macOS: `start-server.local.sh`
+
+These local files are ignored by Git and excluded from release archives. They
+are loaded automatically after the launcher's defaults. They can override
+`SD_SERVER_PATH`, `MODEL_DIR`, and `COMMANDLINE_ARGS`; there is no need to edit
+the tracked launchers. For example:
+
+```bat
+@echo off
+set "MODEL_DIR=D:\stable-diffusion-webui\models"
+set "COMMANDLINE_ARGS=--listen-port 8080 --offload-to-cpu"
+```
+
+```sh
+MODEL_DIR="/data/stable-diffusion-webui/models"
+COMMANDLINE_ARGS="--listen-port 8080 --offload-to-cpu"
+```
+
+Each launcher prints the resolved command before execution and forwards the
+server's exit code.
+
+`SD_SERVER_PATH` inherits the environment value by default; leave it empty for
+automatic discovery, or set it in the local file to select another executable.
+Settings are applied in this order: launcher defaults, local configuration,
+then invocation arguments. Repeated `--model-dir` arguments add roots, so they
+do not replace the `MODEL_DIR` root. Local configuration files are shell scripts
+and use the corresponding shell's syntax.
+
+The launchers preserve the current working directory, so the default
+`MODEL_DIR="./models"` is relative to where you start the launcher. Arguments
+passed when invoking the launcher follow these defaults. Repeated `--model-dir`
+arguments add search roots rather than replacing `MODEL_DIR`; set `MODEL_DIR` in
+the local configuration to change the first root. Other single-value options can override the defaults.
+In the shell script, `COMMANDLINE_ARGS` is a
+whitespace-separated list; pass values containing spaces as quoted arguments
+when invoking the script. `MODEL_DIR` itself supports spaces in both scripts.
+A leading `--version` bypasses the configured defaults. An explicit
+`SD_SERVER_PATH` is used without falling back to another build if it is invalid.
+
+Choose a recipe, select the required component files, and click **Load models**.
+Recipe and component selectors support searching by name or relative file ID.
+Use the arrow keys and Enter to select, or Escape to cancel a search. With
+**Apply recipe defaults** enabled, selecting a recipe immediately applies its
+declared generation defaults while preserving prompts, input images and fields
+not specified by the recipe. Disable it to keep your current parameters when
+switching recipes. This preference defaults to enabled and is saved locally.
+Model loading and server-info refreshes do not reapply defaults or reset the form.
+Recipes declaring `video_frames` also switch the form to Video Generation;
+other recipes switch to Image Generation. Generation is enabled after the
+selected recipe is loaded. Changing a selection does not reload the model
+until you click Load.
+The server can also start with the existing model arguments shown below;
+existing API clients then work without selecting a recipe. The UI automatically
+selects **Command-line model (all features)** and uses the startup generation
+defaults. This runtime-only recipe exposes all controls for the supported
+image/video mode. Selecting another recipe previews a model switch; selecting
+the command-line entry again restores its generation defaults. The entry is
+removed when the command-line context is unloaded or replaced.
+
+Built-in recipes cover SD 1/2/3/3.5, SDXL and distilled checkpoints;
+FLUX 1/2/3, Chroma, Qwen Image and editing variants; Z-Image, Anima, ERNIE,
+LENS, PiD and other supported image families; AnimateDiff, Wan, LTX,
+HunyuanVideo, LingBot Video and MiniMax-H3. Model-specific text encoder
+labels identify the expected encoder family and size. Loading a video model enables **Video Generation**; I2V also requires a
+start image. The Models panel can be collapsed and remembers its open state.
+Its header shows elapsed loading time from the status API and keeps the final
+time after success or failure, including after a page refresh.
+Built-ins are embedded in the
+binary from [recipes/](recipes/), with one JSON file per recipe. Adding or
+editing a built-in requires rebuilding the server. User recipes are read from
+`<model-dir>/recipes/*.json`. A valid user recipe with the same ID replaces
+the entire built-in recipe. Across roots, the first valid user recipe wins;
+overrides are logged. Duplicate user IDs within one root are rejected; invalid
+recipes are reported without removing a valid recipe from another root or the
+built-in version. Slots need only `id` and
+`required`; the server supplies labels and search directories. Use `label`
+only when a recipe needs a more specific display name.
+
+Recipes use `features` to enable specialized controls and `disabled_features`
+to turn off shared defaults. Disabled features take precedence. The UI hides
+unsupported fields, empty sections and irrelevant sampler/cache options, and
+omits hidden values from generation requests. See the
+[feature declarations](api.md#recipe-feature-declarations) for the defaults
+and complete list of supported names.
+
+Built-in additions and changes must follow the [recipe policy](recipes/README.md).
+Split recipes by task weights or added capabilities, not by style or acceleration
+LoRAs. Quantizations, file formats and compatible layer-count variants share a
+recipe. Native encoders, projectors and connectors stay in the model's recipe.
+Community editing recipes require selecting the matching LoRA separately; a
+recipe does not enable it automatically.
+
+See [generation defaults and sources](recipes/defaults.md) for the selected
+publisher recommendations and reference-code defaults, with step defaults capped
+at 20. Recipes link to the
+original publisher's Hugging Face repository where one is available. DiT
+recipes default to Euler, except for models requiring a specialized sampler.
+
+Compatible recipes offer a TAE slot alongside VAE. When the checkpoint does not
+bundle a VAE, select at least one of VAE or TAE. The Models panel shows a reminder
+below the model selectors until this requirement is met. A selected TAE is used for
+encoding and decoding, including when a full VAE is also selected; use complete,
+model-compatible TAE weights rather than decoder-only preview weights. Qwen
+Image 2.1 uses the same recipe for its full VAE and TAEQI2.1 alternatives.
+
+The following variants have dedicated recipes:
+
+| Model | Recipe IDs / notes |
+| --- | --- |
+| Z-Image L2P | `z-image-l2p`; no VAE |
+| LLaDA-Image | `llada-image` (base) and `llada-image-turbo`; use matching encoders and connectors |
+| PixArt Alpha | `pixart-alpha-512`, `pixart-alpha-1024`; Alpha VAE scale and positional settings |
+| PiD / PiD 1.5 | `pid` / `pid-15` for Flux or Z-Image VAE; append `-sd3`, `-flux2`, or `-qwen-image` for other latent formats |
+| SDXL adapters | `sdxl-ip-adapter` supports classic and Plus ViT-H adapters; `sdxl-controlnet` requires compatible SDXL weights |
+| FLUX.1 Fill / Depth | `flux1-fill` uses Init Image and Mask Image; `flux1-depth` uses a precomputed depth map in Control Image |
+| SD2 / SDXL inpainting | `sd2-inpainting`, `sdxl-inpainting` |
+| SD-Turbo | `sd-turbo`; single-file SD2.1-derived checkpoint |
+| Community editing LoRAs | `anima-edit`, `krea2-raw-edit`, `krea2-turbo-edit`; apply a matching LoRA after loading |
+| HunyuanVideo 1.5 | `hunyuan-video-15-t2v`, `hunyuan-video-15-i2v`; choose the task's checkpoint |
+| Wan 2.1 / compatible single Wan 2.2 experts | `wan21-t2v`, `wan21-i2v`; the I2V recipe also accepts compatible SkyReels V2 weights |
+| Qwen Image 2.1 Turbo | `qwen-image-21-turbo`; standalone Turbo weights with the fixed 8-step schedule, Euler and CFG 1 |
+| MOSS Image | `moss-image-base`, `moss-image-sft`; choose the corresponding checkpoint |
+
+PiD always requires a reference image and uses the first one. Its recipes expose
+reference inputs rather than ordinary image-to-image inputs. IP-Adapter recipes
+expose a separate IP-Adapter image and strength. FLUX Canny/Depth consume Control
+Image directly without a separate ControlNet; the UI does not calculate edge
+or depth maps. Their strength is controlled by guidance, not ControlNet strength.
+Krea2 edit recipes use the default `krea2_ostris_edit` reference preset, so the
+editing LoRA must match that convention. Ordinary Anima and Krea2 recipes do not
+expose reference-image editing inputs.
+
+Fill/Depth guidance defaults follow the [Diffusers FLUX documentation](https://huggingface.co/docs/diffusers/api/pipelines/flux).
+SD-Turbo uses the [model card's one-step configuration](https://huggingface.co/stabilityai/sd-turbo),
+with CFG disabled using scale 1 in sd.cpp.
+
+SVD is not included: legacy model recognition and UNet branches remain, but the
+current runner/conditioning setup does not provide a dedicated SVD image-conditioning
+path. Adding a recipe alone would not establish a working SVD pipeline.
+
+Reference images, masks, audio, first/last video frames, and generation options
+such as upscaling remain generation inputs. Hardware and memory options remain
+startup arguments. Tiny SD/Tiny SD2 and Diffusers LCM directories must first be
+converted into single-file checkpoints for the file catalog. LCM checkpoints
+and LoRAs use the matching SD family recipe; select LCM sampling and adjust
+steps/CFG for the chosen weights.
+Metadata inspection, conversion, standalone upscaling and ADetailer are CLI
+workflows rather than model-loading recipes. ESRGAN and LTX latent upscalers
+are selected through generation upscaling settings with `--hires-upscalers-dir`;
+textual inversion uses the startup `--embd-dir` option.
+
+Repeat `--model-dir` to share multiple model directories, including an existing
+SD WebUI model directory:
+
+```sh
+sd-server --model-dir ./models --model-dir /data/stable-diffusion-webui/models
+```
+
+With no `--model-dir`, the server uses `./models`. Explicit roots replace that
+default and are scanned in argument order. Duplicate canonical roots and file
+paths are scanned once. Every root can contain a `recipes/` directory.
+Directory names are matched case-insensitively on all platforms:
+
+| Category | Directory names |
+| --- | --- |
+| Checkpoints | `checkpoints`, `Stable-diffusion` |
+| VAE | `vae`, `VAE` |
+| LoRA | `loras`, `Lora` |
+| ControlNet | `controlnet`, `ControlNet` |
+
+Other categories keep their existing directory names. Classification uses only
+the containing directory: `*.vae.safetensors`, `*.vae.ckpt`, and similar names
+inside `checkpoints/` or `Stable-diffusion/` remain checkpoints.
+
+Place model files in category directories, for example:
+
+```text
+models/
+ checkpoints/
+ diffusion_models/
+ text_encoders/
+ vae/
+ clip_vision/
+ loras/
+ recipes/
+ my-flux.json
+```
+
+Subdirectories within a model category are supported. Use **Refresh files &
+recipes** after adding files or changing recipes. Scanning lists file names
+without loading model weights. Category membership does not guarantee
+compatibility; the model loader performs the final compatibility check.
+Symlinks resolving outside the model directory are rejected. Dropdown labels
+omit the category prefix while retaining subdirectory names. Identical display
+paths are displayed as full file paths, with source directories before the
+filename instead of a hash suffix. The same applies to duplicate LoRA paths. File IDs include a
+root qualifier for additional roots; clients should use the returned IDs.
+Empty optional categories do not display missing-file hints.
+
+Place Tiny AutoEncoder (TAE) files in `vae/` and select them in the **TAE**
+slot. The legacy `vae_approx/` directory is also scanned for TAE selections.
+
+LoRAs are discovered automatically in both `loras/` and `Lora/` under every
+model root. Use `--lora-model-dir` to replace automatic discovery with one
+explicit LoRA directory. Duplicate names remain separate selectable entries. The LoRA selectors support
+searching by name or path with the same keyboard controls as model selectors.
+
+The server keeps one model context loaded. Loading or unloading is rejected
+while generation requests or queued jobs are pending. A switch validates the
+selection before unloading the old context, then loads the replacement.
+If loading fails, the service stays online with no active model and reports
+the error. Backend and memory options supplied at startup apply to subsequent
+loads. Recipe defaults apply to new loads; explicitly supplied generation
+request parameters take precedence.
+
+See [the API reference](api.md#model-management) for recipe fields, supported
+slots, management endpoints, and compatibility behavior.
+
# Example
The following example starts `sd-server` with a standalone diffusion model, VAE, and LLM text encoder:
diff --git a/examples/server/api.md b/examples/server/api.md
index f2f6d32..bb42523 100644
--- a/examples/server/api.md
+++ b/examples/server/api.md
@@ -59,6 +59,293 @@ Current endpoints include:
- `POST /sdcpp/v1/vid_gen`
- `POST /sdcpp/v1/upscale`
+## Model management
+
+Model management is additive: existing generation request bodies, successful
+responses, job polling and cancellation remain unchanged. All three API families
+use the current loaded context. Starting with the existing model arguments
+preserves command-line preloading and its generation defaults; it does not
+require a recipe.
+
+Recipe defaults apply when a generation request omits the corresponding
+parameters. Image editing endpoints retain their existing input-image size
+inference when dimensions are omitted.
+
+Without a loaded model, generation endpoints return HTTP 503 with
+`{"error":"No model is loaded","code":"model_not_loaded"}`.
+While loading/unloading, endpoints that access the context return HTTP 503
+with `code: "model_loading"`. These errors retain the existing string-valued
+`error` field. Job polling/cancellation, catalog queries and model status remain
+available during loading. Model lists are empty when no model is loaded;
+`/v1/models` retains the `sd-cpp-local` ID when ready.
+
+| Method | Endpoint | Response / behavior |
+| --- | --- | --- |
+| GET | `/sdcpp/v1/recipes` | `{recipes: [...], errors: [...]}` |
+| GET | `/sdcpp/v1/model-files` | `{files: [{id, name, category, root, relative_path}], errors: [...]}` |
+| POST | `/sdcpp/v1/models/refresh` | Rescan recipes and files; return `{errors: [...]}` |
+| GET | `/sdcpp/v1/models/status` | Current state, recipe, selection, error, loading time and optional revision |
+| POST | `/sdcpp/v1/models/load` | Load the submitted recipe and selection; return status after completion |
+| POST | `/sdcpp/v1/models/unload` | Release the active context; return empty status |
+
+Load request:
+
+```json
+{
+ "recipe": "flux1-dev",
+ "models": {
+ "diffusion_model": "diffusion_models/flux1-dev.gguf",
+ "clip_l": "text_encoders/clip_l.safetensors",
+ "t5xxl": "text_encoders/t5xxl.safetensors",
+ "vae": "vae/ae.safetensors"
+ }
+}
+```
+
+File IDs are opaque strings returned by `model-files`. The first model root
+retains the existing relative IDs, such as `checkpoints/nested/model.gguf`.
+Additional roots use `@<root-id>/<relative-path>` so identical paths in different
+roots remain selectable. Root qualifiers derive from the canonical root path;
+adding unrelated roots or refreshing the catalog does not change them. Moving
+a root changes its qualifier. Do not construct IDs or use display names in load
+requests.
+
+The display `name` omits the physical category directory (including aliases such
+as `Stable-diffusion`). Duplicate display paths use the full absolute file path,
+with the source directories before the filename and no hash suffix.
+`root` is an opaque source identifier, and
+`relative_path` is the file path beneath that root. Duplicate canonical files
+appear only once, using the first root that discovers them. Categories depend
+on directories, never on filename suffixes such as `.vae.safetensors`.
+
+Repeated `--model-dir` arguments merge roots in order; absent arguments default
+to `./models`. User recipes override built-ins; the earliest root containing a
+valid user recipe wins across roots. Duplicate recipe IDs within one root are
+rejected. Directory aliases do not change recipe slot categories.
+
+LoRA lists retain relative `path` IDs for `loras/` in the first model root and
+for an explicit `--lora-model-dir`, and qualify other directories. The additive `display_path` field provides a readable
+relative path, or the full absolute file path when needed to distinguish duplicates.
+Continue sending `path` in structured LoRA requests. `--lora-model-dir` overrides
+automatic discovery across all model roots.
+Absolute paths, unknown files, wrong categories and
+paths escaping the configured root are rejected. Optional slots can be omitted
+or set to an empty string. Load/unload returns HTTP 409 (`model_busy`) while
+requests or queued jobs are pending. Invalid configurations return HTTP 400
+(`invalid_model_config`) before unloading the current context. An initialization
+failure returns HTTP 422 (`model_load_failed`) and leaves no active context.
+The load HTTP request waits for completion; a separate client can poll status
+while it runs. Refreshing the catalog never modifies an already loaded context.
+
+Status uses `state: empty | loading | ready | error`, `recipe` (ID or null),
+`models` (slot-to-file-ID mapping), and `error` (string or null). A monotonically
+increasing `revision` identifies runtime transitions within this server process.
+Command-line preloading keeps `origin: command_line` and `recipe: null` for
+compatibility, and adds `runtime_recipe`: a read-only `@command-line` recipe
+with all feature declarations enabled. It is only available while that context
+is loaded and cannot be submitted to `models/load`. The UI selects it by default
+and uses command-line generation defaults, not defaults from a catalog recipe.
+Capabilities also include this status as an additive `runtime` field.
+
+`load_elapsed_seconds` is the server-measured duration in seconds (a number or
+null). It updates while a model is loading and freezes on success or failure.
+Timing starts after selection validation and includes releasing the previous
+model. It resets for each load and is null before any load and during/after
+unloading. Command-line preloading reports its completed initialization time.
+Poll `/sdcpp/v1/models/status` to display elapsed time, including while loading;
+timer updates alone do not change `revision`.
+
+### Recipe feature declarations
+
+Recipes may declare `features` and `disabled_features` arrays. The server starts
+with shared defaults, enables features declared by component slots and `features`,
+then applies `disabled_features` last. A disabled feature always wins. Unknown
+names, non-array values and duplicate names within an array are rejected.
+Both fields are optional; existing recipes remain valid.
+
+```json
+{
+ "features": ["flow_shift", "distilled_guidance"],
+ "disabled_features": ["clip_skip", "lora"]
+}
+```
+
+Default-enabled features are `img2img`, `mask_image`, `lora`, `vae_tiling`,
+`cache`, `cache_dit` and `preview`. All other features below default to false:
+
+| Feature | Controls |
+| --- | --- |
+| `flow_shift` | Flow shift, where used by the selected scheduler |
+| `distilled_guidance` | Distilled guidance embedding |
+| `shifted_timestep` | SDXL timestep shifting |
+| `img_cfg` | Image CFG |
+| `slg` | Skip-layer guidance fields |
+| `clip_skip` | CLIP Skip |
+| `img2img`, `mask_image` | Image initialization/strength and masks |
+| `ref_images` | Reference image inputs |
+| `controlnet` | Control image and ControlNet strength |
+| `control_image` | Control image without a separate ControlNet (FLUX Canny/Depth) |
+| `ip_adapter` | IP-Adapter image and strength |
+| `init_image`, `end_image` | Video start/end frames |
+| `control_frames`, `vace` | Video control frames and VACE strength |
+| `high_noise` | High-noise pass, MoE boundary and high-noise LoRAs |
+| `lora`, `vae_tiling` | LoRA overrides and VAE tiling |
+| `cache`, `cache_unet`, `cache_dit` | Cache panel and architecture-specific algorithms |
+| `preview`, `preview_tae` | Preview panel and TAE preview option |
+
+A `control_net` slot enables `controlnet`, `ip_adapter` enables `ip_adapter`,
+`high_noise_diffusion_model` enables `high_noise`, and `tae`/`taesd` enables `preview_tae`, unless explicitly disabled.
+ControlNet controls are shown when previewing a supporting recipe, even before
+a model file is selected. High-noise and TAE controls additionally require their
+component to be selected. Runtime capabilities require the component to be loaded.
+PuLID disables SLG while selected.
+Image-only and video-only inputs are filtered by the active generation mode.
+Eta depends on the selected sampler; SCM fields only apply to DBCache,
+TaylorSeer and Cache-DIT. Empty panels and advanced sections are hidden.
+
+`GET /sdcpp/v1/recipes` adds a resolved boolean `effective_features` object to
+each recipe. This is response-only metadata, like `source` and slot `categories`;
+omit it when writing a user recipe file. The defaults are maintained by the
+server, not duplicated by the frontend.
+
+`GET /sdcpp/v1/capabilities` adds `generation_features` for the loaded context,
+derived from the active recipe and component availability without inferring
+features from model-family names. When no model is loaded, every feature is
+present and false. The UI can still preview a selected recipe using its
+`effective_features` before loading.
+Existing `features` / `features_by_mode` keys are retained and their model-specific
+values reflect these declarations. A loaded recipe is snapshotted: refreshing
+recipe files does not change the active context's capabilities until reloading.
+Command-line preloading uses the runtime-only all-features recipe instead of model-family inference
+and component paths; variant-specific recipe declarations are more precise.
+
+These declarations govern the Web UI and advertised capabilities, not API
+request validation. Existing generation APIs continue accepting their existing
+fields. The Web UI omits unsupported/hidden fields and ignores hidden image
+inputs when building requests, so switching recipes cannot submit stale options.
+
+### User recipe format
+
+Contributions to the built-in catalog must follow the
+[built-in recipe policy](recipes/README.md). Personal user recipes may define
+workflow-specific presets without adding them to the built-in catalog.
+
+Each UTF-8 JSON file in `<model-dir>/recipes/` defines one complete recipe.
+The maximum recipe file size is 1 MiB. Unknown fields, invalid types and
+duplicate slots are errors. Built-ins in `examples/server/recipes/*.json` use
+the same one-recipe-per-file schema:
+
+```json
+{
+ "schema_version": 1,
+ "id": "my-flux",
+ "name": "My FLUX",
+ "description": "FLUX.1 dev",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-dev",
+ "slots": [
+ {"id": "diffusion_model", "required": true},
+ {"id": "clip_l", "required": true},
+ {"id": "t5xxl", "required": true},
+ {"id": "vae", "required": false},
+ {"id": "tae", "required": false}
+ ],
+ "required_any": [["vae", "tae"]],
+ "defaults": {"width": 1024, "height": 1024, "cfg_scale": 1.0, "distilled_guidance": 3.5, "sampling_method": "euler", "steps": 20}
+}
+```
+
+IDs contain 1–128 ASCII letters, digits, dots, underscores or hyphens.
+`name`, `schema_version: 1` and `slots` are required; `description` and
+`model_url`, `required_any`, `defaults`, `context`, `features` and `disabled_features` are optional. Each slot requires only `id` and boolean `required`.
+The server supplies the default label and search directories for each slot.
+An optional nonempty `label` overrides the display name, for example
+`{"id": "diffusion_model", "label": "Low-noise diffusion model", "required": true}`.
+Legacy `category` fields remain accepted, but must match one of the slot's
+supported directories; they cannot redefine or restrict its search directories.
+At least one required `model` or `diffusion_model` slot is necessary.
+
+`model_url` is an optional HTTPS Hugging Face model repository link, shown beside
+the recipe description. Built-ins link to the model's author/publisher, not repacks.
+`required_any` contains groups of declared slot IDs. At least one model in each
+group must be selected. For example, `"required_any": [["vae", "tae"]]` with both
+slots marked `required: false` allows either the full VAE or a compatible TAE.
+Selecting both is allowed; the current server uses TAE for encoding/decoding when
+TAE is selected. Checkpoint recipes with a bundled VAE do not need this group.
+Use complete, model-compatible TAE weights; decoder-only preview weights cannot
+replace a full VAE. Pixel-space models and unsupported latent formats have no
+TAE slot.
+
+| Slot IDs | Search directories |
+| --- | --- |
+| `model` | `checkpoints` |
+| `diffusion_model`, `high_noise_diffusion_model`, `uncond_diffusion_model` | `diffusion_models` |
+| `clip_l`, `clip_g`, `t5xxl`, `llm`, `audio_encoder`, `embeddings_connectors` | `text_encoders` |
+| `clip_vision`, `llm_vision` | `clip_vision` |
+| `vae`, `audio_vae` | `vae` |
+| `tae` (legacy alias: `taesd`) | `vae` or `vae_approx` |
+| `tokenizer` | `tokenizers` |
+| `control_net` | `controlnet` |
+| `ip_adapter` | `ipadapter` |
+| `motion_module` | `motion_modules` |
+| `photo_maker` | `photomaker` |
+| `pulid_weights` | `pulid` |
+
+Built-in recipes use `tae` for the Tiny AutoEncoder slot. TAE selections can use files from either `vae/` or the legacy
+`vae_approx/` directory. Existing recipes and load requests using `taesd`
+remain supported. Do not specify both `tae` and `taesd` in the same recipe
+or load request.
+
+Model files can use `.gguf`, `.safetensors`, `.sft`, `.ckpt`, `.pt`, `.pth`
+or `.bin` extensions. The tokenizer category lists `.json` files only.
+Custom recipes compose components supported by the backend; they do not add
+new model architectures.
+
+Optional `context` settings apply to each model load and override the matching
+startup setting. Supported fields are `vae_format` (`auto`, `flux`, `sd3`,
+`flux2`, or `wan`) and `model_args` (a string in the same format as
+`--model-args`; replaces the startup string). Other startup settings remain
+unchanged. Switching to a recipe that omits these fields restores the startup
+values. For example, PiD uses `{"vae_format": "flux"}` and Qwen Image Edit
+2511 uses `{"model_args": "qwen_image_zero_cond_t=true"}`.
+
+Supported defaults:
+
+- Integer `width`/`height` (64–4096), `steps` (1–1000), `video_frames`
+ (1–4096), `fps` (1–240), `shifted_timestep` (0–1000), and
+ `high_noise_steps` (1–1000 or -1 for automatic).
+- Numeric `cfg_scale`, `img_cfg_scale`, `distilled_guidance`, `flow_shift`,
+ `high_noise_cfg_scale`, and `high_noise_flow_shift` (0–1000).
+- Numeric `strength` (0–1), used for image-to-image denoising, and
+ `moe_boundary` (0–1), used for automatic Wan MoE expert switching.
+- `custom_sigmas`: an empty array or 2–1001 finite, nonnegative numbers.
+ A nonempty array overrides the generated schedule; the sigma count minus one
+ determines the actual step count. Include the final zero. An empty array uses
+ the generated schedule. Loaded recipe sigmas apply to generation requests
+ unless `sample_params.custom_sigmas` overrides them; switching recipes restores
+ that recipe's schedule or the startup default.
+- Supported `sampling_method`/`scheduler` and
+ `high_noise_sampling_method`/`high_noise_scheduler` names.
+
+Other generation parameters continue to use startup defaults unless explicitly
+supplied by the generation request. Built-in Wan recipes provide video defaults;
+use `/sdcpp/v1/vid_gen` after loading a video model.
+LoRAs are merged from `loras/` and `Lora/` in every model root unless
+`--lora-model-dir` is supplied.
+
+Recipe responses fill in each slot's `label`, `category` (the legacy value,
+if supplied, or the first search directory), and `categories` (the complete
+server-defined list of search directories). Clients should filter model files
+using `categories`; `category` remains available for older clients.
+For example, `{"id": "tae", "required": false}` expands to
+`{"id": "tae", "required": false, "label": "TAE", "category": "vae", "categories": ["vae", "vae_approx"]}`.
+
+Recipe responses also add `source: builtin | user` and, for user recipes, `file`.
+Do not include `source`, `file`, or slot `categories` in a recipe file. A valid user recipe
+replaces a built-in with the same ID in full; inheritance/partial merging is
+not supported. Invalid recipes are skipped. Multiple valid user files sharing
+an ID are all skipped, preserving any built-in with that ID. Catalog errors
+are returned as `{file, message}` entries.
+
## `sd_cpp_extra_args`
`sd_cpp_extra_args` is an extension mechanism for the compatibility APIs.
@@ -413,7 +700,7 @@ Common job shape:
"id": "job_01HTXYZABC",
"kind": "img_gen",
"status": "queued",
- "created": 1775401200,
+ "created": 1775401200000,
"started": null,
"completed": null,
"queue_position": 2,
@@ -437,6 +724,11 @@ Field types:
| `error` | `object \| null` |
| `preview` | `object \| null` |
+`created`, `started`, and `completed` are integer Unix timestamps in
+**milliseconds**. Subtract `started` from `completed` and divide by 1000 to get
+the elapsed generation time in seconds, including output encoding and excluding
+queue time.
+
`preview` sub-fields:
| Field | Type | Notes |
@@ -512,6 +804,7 @@ Shared nested fields:
| --- | --- |
| `loras[].name` | `string` |
| `loras[].path` | `string` |
+| `loras[].display_path` | `string` (display only) |
`upscalers`
@@ -666,7 +959,7 @@ Example response:
"id": "job_01HTXYZABC",
"kind": "img_gen",
"status": "queued",
- "created": 1775401200,
+ "created": 1775401200000,
"poll_url": "/sdcpp/v1/jobs/job_01HTXYZABC"
}
```
@@ -994,9 +1287,9 @@ Example completed job:
"id": "job_01HTXYZABC",
"kind": "img_gen",
"status": "completed",
- "created": 1775401200,
- "started": 1775401203,
- "completed": 1775401215,
+ "created": 1775401200000,
+ "started": 1775401203000,
+ "completed": 1775401215237,
"queue_position": 0,
"result": {
"output_format": "png",
@@ -1020,9 +1313,9 @@ Example failed job:
"id": "job_01HTXYZABC",
"kind": "img_gen",
"status": "failed",
- "created": 1775401200,
- "started": 1775401203,
- "completed": 1775401204,
+ "created": 1775401200000,
+ "started": 1775401203000,
+ "completed": 1775401204000,
"queue_position": 0,
"result": null,
"error": {
@@ -1041,9 +1334,9 @@ Example cancelled job:
"id": "job_01HTXYZABC",
"kind": "img_gen",
"status": "cancelled",
- "created": 1775401200,
+ "created": 1775401200000,
"started": null,
- "completed": 1775401202,
+ "completed": 1775401202000,
"queue_position": 0,
"result": null,
"error": {
@@ -1079,7 +1372,7 @@ Example response:
"id": "job_01HTXYZVID",
"kind": "vid_gen",
"status": "queued",
- "created": 1775401200,
+ "created": 1775401200000,
"poll_url": "/sdcpp/v1/jobs/job_01HTXYZVID"
}
```
@@ -1407,9 +1700,9 @@ Example completed job:
"id": "job_01HTXYZVID",
"kind": "vid_gen",
"status": "completed",
- "created": 1775401200,
- "started": 1775401203,
- "completed": 1775401215,
+ "created": 1775401200000,
+ "started": 1775401203000,
+ "completed": 1775401215237,
"queue_position": 0,
"result": {
"output_format": "webm",
@@ -1433,9 +1726,9 @@ Example failed job:
"id": "job_01HTXYZVID",
"kind": "vid_gen",
"status": "failed",
- "created": 1775401200,
- "started": 1775401203,
- "completed": 1775401204,
+ "created": 1775401200000,
+ "started": 1775401203000,
+ "completed": 1775401204000,
"queue_position": 0,
"result": null,
"error": {
@@ -1454,9 +1747,9 @@ Example cancelled job:
"id": "job_01HTXYZVID",
"kind": "vid_gen",
"status": "cancelled",
- "created": 1775401200,
+ "created": 1775401200000,
"started": null,
- "completed": 1775401202,
+ "completed": 1775401202000,
"queue_position": 0,
"result": null,
"error": {
diff --git a/examples/server/async_jobs.cpp b/examples/server/async_jobs.cpp
index 8840808..7f3c8a1 100644
--- a/examples/server/async_jobs.cpp
+++ b/examples/server/async_jobs.cpp
@@ -88,7 +88,7 @@ const char* async_job_status_name(AsyncJobStatus status) {
}
void purge_expired_jobs(AsyncJobManager& manager) {
- const int64_t now = unix_timestamp_now();
+ const int64_t now = unix_timestamp_now_ms();
for (auto it = manager.expired_jobs.begin(); it != manager.expired_jobs.end();) {
if (it->second <= now) {
@@ -108,8 +108,8 @@ void purge_expired_jobs(AsyncJobManager& manager) {
int64_t ttl_seconds = job->status == AsyncJobStatus::Completed
? manager.completed_ttl_seconds
: manager.failed_ttl_seconds;
- if (now - job->completed_at >= ttl_seconds) {
- manager.expired_jobs[job->id] = now + std::max<int64_t>(ttl_seconds, 60);
+ if (now - job->completed_at >= ttl_seconds * 1000) {
+ manager.expired_jobs[job->id] = now + std::max<int64_t>(ttl_seconds, 60) * 1000;
it = manager.jobs.erase(it);
} else {
++it;
@@ -130,7 +130,7 @@ size_t count_pending_jobs(const AsyncJobManager& manager) {
std::string make_async_job_id(AsyncJobManager& manager) {
std::ostringstream oss;
- oss << "job_" << std::hex << unix_timestamp_now() << "_" << std::setw(8)
+ oss << "job_" << std::hex << unix_timestamp_now_ms() << "_" << std::setw(8)
<< std::setfill('0') << manager.next_id++;
return oss.str();
}
@@ -143,7 +143,7 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
manager.queue.erase(new_end, manager.queue.end());
job.status = AsyncJobStatus::Cancelled;
- job.completed_at = unix_timestamp_now();
+ job.completed_at = unix_timestamp_now_ms();
job.result_images_b64.clear();
job.result_media_b64.clear();
job.result_media_mime_type.clear();
@@ -373,7 +373,7 @@ void async_job_worker(ServerRuntime& runtime) {
job = it->second;
job->status = AsyncJobStatus::Generating;
- job->started_at = unix_timestamp_now();
+ job->started_at = unix_timestamp_now_ms();
}
std::vector<std::string> output_images;
@@ -405,7 +405,7 @@ void async_job_worker(ServerRuntime& runtime) {
continue;
}
- job->completed_at = unix_timestamp_now();
+ job->completed_at = unix_timestamp_now_ms();
if (ok) {
job->status = AsyncJobStatus::Completed;
job->result_images_b64 = std::move(output_images);
diff --git a/examples/server/async_jobs.h b/examples/server/async_jobs.h
index 2fe79bb..049a9d7 100644
--- a/examples/server/async_jobs.h
+++ b/examples/server/async_jobs.h
@@ -32,7 +32,7 @@ struct AsyncGenerationJob {
std::string id;
AsyncJobKind kind = AsyncJobKind::ImgGen;
AsyncJobStatus status = AsyncJobStatus::Queued;
- int64_t created_at = unix_timestamp_now();
+ int64_t created_at = unix_timestamp_now_ms();
int64_t started_at = 0;
int64_t completed_at = 0;
ImgGenJobRequest img_gen;
diff --git a/examples/server/frontend b/examples/server/frontend
index 8e7c115..e36e34a 160000
--- a/examples/server/frontend
+++ b/examples/server/frontend
@@ -1 +1 @@
-Subproject commit 8e7c11505925c0dbf9aaea56a3a5058b8690d66b
+Subproject commit e36e34a2e2204e3df654b16cadc112d8ce8f3174
diff --git a/examples/server/main.cpp b/examples/server/main.cpp
index ddfd733..1708776 100644
--- a/examples/server/main.cpp
+++ b/examples/server/main.cpp
@@ -10,6 +10,7 @@
#include "async_jobs.h"
#include "common/common.h"
#include "common/resource_owners.hpp"
+#include "model_catalog.h"
#include "routes.h"
#include "runtime.h"
@@ -50,7 +51,7 @@ static void parse_args(int argc,
const bool random_seed_requested = default_gen_params.seed < 0;
if (!svr_params.resolve_and_validate() ||
- !ctx_params.resolve_and_validate(IMG_GEN) ||
+ !ctx_params.resolve_and_validate(ctx_params.model_path.empty() && ctx_params.diffusion_model_path.empty() ? METADATA : IMG_GEN) ||
!default_gen_params.resolve_and_validate(IMG_GEN,
ctx_params.lora_model_dir,
ctx_params.hires_upscalers_dir)) {
@@ -84,14 +85,19 @@ int main(int argc, const char** argv) {
LOG_VERBOSE("version: %s", version_string().c_str());
LOG_VERBOSE("%s", sd_get_system_info());
- LOG_VERBOSE("%s", svr_params.to_string().c_str());
+ LOG_DEBUG("%s", svr_params.to_string().c_str());
LOG_DEBUG("%s", ctx_params.to_string().c_str());
LOG_DEBUG("%s", default_gen_params.to_string().c_str());
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false);
- SDCtxPtr sd_ctx(new_sd_ctx(&sd_ctx_params));
+ ServerModels models;
+ if (!ctx_params.model_path.empty() || !ctx_params.diffusion_model_path.empty()) {
+ const auto started = std::chrono::steady_clock::now();
+ models.context.reset(new_sd_ctx(&sd_ctx_params));
+ models.status["load_elapsed_seconds"] = std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
+ }
- if (sd_ctx == nullptr) {
+ if ((!ctx_params.model_path.empty() || !ctx_params.diffusion_model_path.empty()) && !models.context) {
LOG_ERROR("new_sd_ctx_t failed");
return 1;
}
@@ -104,7 +110,7 @@ int main(int argc, const char** argv) {
std::mutex upscaler_mutex;
AsyncJobManager async_job_manager;
ServerRuntime runtime = {
- sd_ctx.get(),
+ models.context.get(),
&sd_ctx_mutex,
&svr_params,
&ctx_params,
@@ -114,8 +120,16 @@ int main(int argc, const char** argv) {
&upscaler_cache,
&upscaler_mutex,
&async_job_manager,
+ &models,
};
+ try {
+ initialize_server_models(runtime);
+ } catch (const std::exception& e) {
+ LOG_ERROR("model catalog initialization failed: %s", e.what());
+ return 1;
+ }
+
std::thread async_worker(async_job_worker, std::ref(runtime));
httplib::Server svr;
@@ -147,6 +161,7 @@ int main(int argc, const char** argv) {
register_openai_api_endpoints(svr, runtime);
register_sdapi_endpoints(svr, runtime);
register_sdcpp_api_endpoints(svr, runtime);
+ register_model_endpoints(svr, runtime);
LOG_INFO("listening on: http://%s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port);
svr.listen(svr_params.listen_ip, svr_params.listen_port);
diff --git a/examples/server/model_catalog.cpp b/examples/server/model_catalog.cpp
new file mode 100644
index 0000000..a3c5920
--- /dev/null
+++ b/examples/server/model_catalog.cpp
@@ -0,0 +1,664 @@
+#include "model_catalog.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cmath>
+#include <fstream>
+#include <iomanip>
+#include <set>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+#include "common/log.h"
+#include "recipes_builtin.h"
+
+namespace fs = std::filesystem;
+
+struct ModelSlot {
+ const char* id;
+ const char* label;
+ std::vector<std::string> categories;
+ std::string SDContextParams::*field;
+};
+
+static const ModelSlot k_model_slots[] = {
+ {"model", "Checkpoint", {"checkpoints"}, &SDContextParams::model_path},
+ {"diffusion_model", "Diffusion model", {"diffusion_models"}, &SDContextParams::diffusion_model_path},
+ {"high_noise_diffusion_model", "High-noise diffusion model", {"diffusion_models"}, &SDContextParams::high_noise_diffusion_model_path},
+ {"uncond_diffusion_model", "Unconditional diffusion model", {"diffusion_models"}, &SDContextParams::uncond_diffusion_model_path},
+ {"clip_l", "CLIP-L", {"text_encoders"}, &SDContextParams::clip_l_path},
+ {"clip_g", "CLIP-G", {"text_encoders"}, &SDContextParams::clip_g_path},
+ {"clip_vision", "CLIP Vision", {"clip_vision"}, &SDContextParams::clip_vision_path},
+ {"t5xxl", "T5", {"text_encoders"}, &SDContextParams::t5xxl_path},
+ {"llm", "LLM Text Encoder", {"text_encoders"}, &SDContextParams::llm_path},
+ {"llm_vision", "LLM Vision Encoder", {"clip_vision"}, &SDContextParams::llm_vision_path},
+ {"tokenizer", "Tokenizer", {"tokenizers"}, &SDContextParams::tokenizer},
+ {"vae", "VAE", {"vae"}, &SDContextParams::vae_path},
+ {"audio_vae", "Audio VAE", {"vae"}, &SDContextParams::audio_vae_path},
+ {"audio_encoder", "Audio Encoder", {"text_encoders"}, &SDContextParams::audio_encoder_path},
+ {"embeddings_connectors", "Embeddings Connectors", {"text_encoders"}, &SDContextParams::embeddings_connectors_path},
+ {"tae", "TAE", {"vae", "vae_approx"}, &SDContextParams::taesd_path},
+ {"taesd", "TAE", {"vae_approx", "vae"}, &SDContextParams::taesd_path},
+ {"control_net", "ControlNet", {"controlnet"}, &SDContextParams::control_net_path},
+ {"ip_adapter", "IP-Adapter", {"ipadapter"}, &SDContextParams::ip_adapter_path},
+ {"motion_module", "Motion Module", {"motion_modules"}, &SDContextParams::motion_module_path},
+ {"photo_maker", "PhotoMaker", {"photomaker"}, &SDContextParams::photo_maker_path},
+ {"pulid_weights", "PuLID", {"pulid"}, &SDContextParams::pulid_weights_path},
+};
+
+static const ModelSlot* find_model_slot(const std::string& id) {
+ for (const auto& slot : k_model_slots) {
+ if (id == slot.id) {
+ return &slot;
+ }
+ }
+ return nullptr;
+}
+
+static bool matches_model_category(const ModelSlot& slot, const std::string& category) {
+ return std::find(slot.categories.begin(), slot.categories.end(), category) != slot.categories.end();
+}
+
+static void clear_model_paths(SDContextParams& params) {
+ for (const auto& slot : k_model_slots) {
+ (params.*slot.field).clear();
+ }
+}
+
+std::string model_path_key(const fs::path& path) {
+#ifdef _WIN32
+ const auto value = path.generic_wstring();
+ const int length = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, value.data(),
+ static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr, 0);
+ if (length > 0) {
+ std::wstring folded(length, L'\0');
+ LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, value.data(),
+ static_cast<int>(value.size()), &folded[0], length, nullptr, nullptr, 0);
+ return fs::path(folded).generic_u8string();
+ }
+#endif
+ return path.generic_u8string();
+}
+
+bool model_path_inside(const fs::path& root, const fs::path& path) {
+ auto parent = root.begin();
+ auto child = path.begin();
+ for (; parent != root.end(); ++parent, ++child) {
+ if (child == path.end() || model_path_key(*parent) != model_path_key(*child)) {
+ return false;
+ }
+ }
+ return child != path.end();
+}
+
+ModelRoot make_model_root(const fs::path& path, bool primary) {
+ ModelRoot root;
+ const auto absolute = fs::absolute(path);
+ std::error_code ec;
+ root.path = fs::weakly_canonical(absolute, ec);
+ if (ec)
+ root.path = absolute.lexically_normal();
+ root.primary = primary;
+ uint64_t hash = 14695981039346656037ULL;
+ for (unsigned char c : model_path_key(root.path)) {
+ hash ^= c;
+ hash *= 1099511628211ULL;
+ }
+ std::ostringstream id;
+ id << '@' << std::hex << std::setfill('0') << std::setw(16) << hash;
+ root.id = id.str();
+ root.label = root.path.filename().u8string() + " [" + root.id.substr(1, 8) + "]";
+ return root;
+}
+
+std::vector<ModelRoot> get_model_roots(const SDSvrParams& params) {
+ const auto directories = params.model_dirs.empty() ? std::vector<std::string>{"models"} : params.model_dirs;
+ std::vector<ModelRoot> roots;
+ std::set<std::string> seen;
+ for (size_t i = 0; i < directories.size(); ++i) {
+ auto root = make_model_root(fs::u8path(directories[i]), i == 0);
+ if (seen.insert(model_path_key(root.path)).second)
+ roots.push_back(std::move(root));
+ }
+ return roots;
+}
+
+std::vector<fs::path> model_category_directories(const ModelRoot& root, const std::string& category) {
+ std::vector<fs::path> directories;
+ if (!fs::exists(root.path))
+ return directories;
+ for (const auto& entry : fs::directory_iterator(root.path)) {
+ auto name = entry.path().filename().u8string();
+ std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
+ if (name == category || (category == "checkpoints" && name == "stable-diffusion") ||
+ (category == "loras" && name == "lora")) {
+ directories.push_back(entry.path());
+ }
+ }
+ std::sort(directories.begin(), directories.end(), [&](const fs::path& a, const fs::path& b) {
+ const bool a_standard = a.filename().u8string() == category;
+ const bool b_standard = b.filename().u8string() == category;
+ return a_standard != b_standard ? a_standard : a < b;
+ });
+ return directories;
+}
+
+static bool valid_identifier(const std::string& id) {
+ return !id.empty() && id.size() <= 128 &&
+ std::all_of(id.begin(), id.end(), [](unsigned char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.';
+ });
+}
+
+static void check_keys(const json& object, const std::set<std::string>& keys) {
+ if (!object.is_object()) {
+ throw std::runtime_error("expected an object");
+ }
+ for (auto it = object.begin(); it != object.end(); ++it) {
+ if (!keys.count(it.key())) {
+ throw std::runtime_error("unknown field: " + it.key());
+ }
+ }
+}
+
+static void apply_recipe_defaults(const json& defaults, SDGenerationParams& params) {
+ check_keys(defaults, {"width", "height", "steps", "cfg_scale", "strength", "distilled_guidance", "sampling_method", "scheduler", "custom_sigmas",
+ "video_frames", "fps", "flow_shift", "moe_boundary", "high_noise_steps", "high_noise_cfg_scale",
+ "high_noise_flow_shift", "high_noise_sampling_method", "high_noise_scheduler", "img_cfg_scale", "shifted_timestep"});
+ for (const char* key : {"width", "height", "steps", "video_frames", "fps", "high_noise_steps", "shifted_timestep"}) {
+ if (!defaults.contains(key)) {
+ continue;
+ }
+ if (!defaults[key].is_number_integer()) {
+ throw std::runtime_error(std::string(key) + " must be an integer");
+ }
+ const int64_t value = defaults[key].get<int64_t>();
+ const std::string field = key;
+ const bool dimension = field == "width" || field == "height";
+ const int64_t maximum = dimension || field == "video_frames" ? 4096 : field == "fps" ? 240
+ : 1000;
+ const int64_t minimum = dimension ? 64 : field == "shifted_timestep" ? 0
+ : 1;
+ if ((value < minimum || value > maximum) && !(field == "high_noise_steps" && value == -1)) {
+ throw std::runtime_error(std::string(key) + " is out of range");
+ }
+ if (std::string(key) == "width")
+ params.width = static_cast<int>(value);
+ if (std::string(key) == "height")
+ params.height = static_cast<int>(value);
+ if (std::string(key) == "steps")
+ params.sample_params.sample_steps = static_cast<int>(value);
+ if (field == "shifted_timestep")
+ params.sample_params.shifted_timestep = static_cast<int>(value);
+ if (field == "video_frames")
+ params.video_frames = static_cast<int>(value);
+ if (field == "fps")
+ params.fps = static_cast<int>(value);
+ if (field == "high_noise_steps")
+ params.high_noise_sample_params.sample_steps = static_cast<int>(value);
+ }
+ for (const char* key : {"cfg_scale", "strength", "moe_boundary", "img_cfg_scale", "distilled_guidance", "flow_shift", "high_noise_cfg_scale", "high_noise_flow_shift"}) {
+ if (!defaults.contains(key)) {
+ continue;
+ }
+ if (!defaults[key].is_number()) {
+ throw std::runtime_error(std::string(key) + " must be a number");
+ }
+ const float value = defaults[key].get<float>();
+ if (!std::isfinite(value) || value < 0 || value > ((std::string(key) == "strength" || std::string(key) == "moe_boundary") ? 1.f : 1000.f)) {
+ throw std::runtime_error(std::string(key) + " is out of range");
+ }
+ if (std::string(key) == "strength")
+ params.strength = value;
+ if (std::string(key) == "moe_boundary")
+ params.moe_boundary = value;
+ if (std::string(key) == "cfg_scale")
+ params.sample_params.guidance.txt_cfg = value;
+ if (std::string(key) == "img_cfg_scale")
+ params.sample_params.guidance.img_cfg = value;
+ if (std::string(key) == "distilled_guidance")
+ params.sample_params.guidance.distilled_guidance = value;
+ if (std::string(key) == "flow_shift")
+ params.sample_params.flow_shift = value;
+ if (std::string(key) == "high_noise_cfg_scale")
+ params.high_noise_sample_params.guidance.txt_cfg = value;
+ if (std::string(key) == "high_noise_flow_shift")
+ params.high_noise_sample_params.flow_shift = value;
+ }
+ if (defaults.contains("custom_sigmas")) {
+ const auto& sigmas = defaults.at("custom_sigmas");
+ if (!sigmas.is_array() || sigmas.size() == 1 || sigmas.size() > 1001)
+ throw std::runtime_error("custom_sigmas must be empty or contain 2-1001 values");
+ for (const auto& sigma : sigmas) {
+ if (!sigma.is_number() || !std::isfinite(sigma.get<float>()) || sigma.get<float>() < 0)
+ throw std::runtime_error("custom_sigmas must contain finite nonnegative numbers");
+ }
+ params.custom_sigmas = sigmas.get<std::vector<float>>();
+ }
+ for (const char* key : {"sampling_method", "high_noise_sampling_method"}) {
+ if (!defaults.contains(key))
+ continue;
+ auto& sample = std::string(key) == "sampling_method" ? params.sample_params : params.high_noise_sample_params;
+ const auto name = defaults.at(key).get<std::string>();
+ bool found = false;
+ for (int i = 0; i < SAMPLE_METHOD_COUNT; ++i) {
+ if (name == sd_sample_method_name(static_cast<sample_method_t>(i))) {
+ sample.sample_method = static_cast<sample_method_t>(i);
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ throw std::runtime_error(std::string("unknown ") + key);
+ }
+ for (const char* key : {"scheduler", "high_noise_scheduler"}) {
+ if (!defaults.contains(key))
+ continue;
+ auto& sample = std::string(key) == "scheduler" ? params.sample_params : params.high_noise_sample_params;
+ const auto name = defaults.at(key).get<std::string>();
+ bool found = false;
+ for (int i = 0; i < SCHEDULER_COUNT; ++i) {
+ if (name == sd_scheduler_name(static_cast<scheduler_t>(i)) ||
+ (name == "normal" && i == DISCRETE_SCHEDULER)) {
+ sample.scheduler = static_cast<scheduler_t>(i);
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ throw std::runtime_error(std::string("unknown ") + key);
+ }
+}
+
+static void apply_recipe_context(const json& settings, SDContextParams& params) {
+ check_keys(settings, {"vae_format", "model_args"});
+ if (settings.contains("vae_format")) {
+ const auto format = settings.at("vae_format").get<std::string>();
+ if (format != "auto" && format != "flux" && format != "sd3" && format != "flux2" && format != "wan")
+ throw std::runtime_error("unknown vae_format");
+ params.vae_format = format;
+ }
+ if (settings.contains("model_args")) {
+ params.model_args = settings.at("model_args").get<std::string>();
+ }
+}
+
+static const std::map<std::string, bool> k_recipe_features = {
+ {"img2img", true},
+ {"mask_image", true},
+ {"lora", true},
+ {"vae_tiling", true},
+ {"cache", true},
+ {"cache_dit", true},
+ {"preview", true},
+ {"flow_shift", false},
+ {"distilled_guidance", false},
+ {"shifted_timestep", false},
+ {"img_cfg", false},
+ {"slg", false},
+ {"clip_skip", false},
+ {"ref_images", false},
+ {"controlnet", false},
+ {"control_image", false},
+ {"ip_adapter", false},
+ {"init_image", false},
+ {"end_image", false},
+ {"control_frames", false},
+ {"vace", false},
+ {"high_noise", false},
+ {"cache_unet", false},
+ {"preview_tae", false},
+};
+
+static void validate_recipe_features(const json& recipe) {
+ for (const char* key : {"features", "disabled_features"}) {
+ if (!recipe.contains(key))
+ continue;
+ const auto& values = recipe.at(key);
+ if (!values.is_array() || values.size() > k_recipe_features.size())
+ throw std::runtime_error(std::string(key) + " must be an array of feature names");
+ std::set<std::string> seen;
+ for (const auto& value : values) {
+ const auto name = value.get<std::string>();
+ if (!k_recipe_features.count(name) || !seen.insert(name).second)
+ throw std::runtime_error("unknown or duplicate feature: " + name);
+ }
+ }
+}
+
+json resolve_recipe_features(const json& recipe) {
+ json features = k_recipe_features;
+ for (const auto& slot : recipe.at("slots")) {
+ const auto id = slot.at("id").get<std::string>();
+ if (id == "ip_adapter")
+ features["ip_adapter"] = true;
+ if (id == "control_net")
+ features["controlnet"] = true;
+ if (id == "high_noise_diffusion_model")
+ features["high_noise"] = true;
+ if (id == "tae" || id == "taesd")
+ features["preview_tae"] = true;
+ }
+ for (const auto& name : recipe.value("features", json::array()))
+ features[name.get<std::string>()] = true;
+ for (const auto& name : recipe.value("disabled_features", json::array()))
+ features[name.get<std::string>()] = false;
+ return features;
+}
+
+json model_generation_features(const ServerRuntime& runtime) {
+ if (!runtime.sd_ctx || runtime.models->active_recipe.is_null()) {
+ json features = k_recipe_features;
+ for (auto& feature : features)
+ feature = false;
+ return features;
+ }
+ json features = resolve_recipe_features(runtime.models->active_recipe);
+ if (runtime.models->active_recipe.value("source", "") == "command_line")
+ return features;
+ // Optional components only become usable once selected and loaded.
+ features["ip_adapter"] = features.value("ip_adapter", false) && !runtime.ctx_params->ip_adapter_path.empty();
+ features["controlnet"] = features.value("controlnet", false) && !runtime.ctx_params->control_net_path.empty();
+ features["high_noise"] = features.value("high_noise", false) && !runtime.ctx_params->high_noise_diffusion_model_path.empty();
+ features["preview_tae"] = features.value("preview_tae", false) && !runtime.ctx_params->taesd_path.empty();
+ if (!runtime.ctx_params->pulid_weights_path.empty())
+ features["slg"] = false;
+ return features;
+}
+
+static void validate_recipe(const json& recipe) {
+ check_keys(recipe, {"schema_version", "id", "name", "description", "model_url", "slots", "required_any", "defaults", "context", "features", "disabled_features"});
+ if (!recipe.at("schema_version").is_number_integer() || recipe.at("schema_version") != 1 ||
+ !valid_identifier(recipe.at("id").get<std::string>()) || recipe.at("name").get<std::string>().empty()) {
+ throw std::runtime_error("invalid schema_version, id or name");
+ }
+ if (recipe.contains("description") && !recipe["description"].is_string()) {
+ throw std::runtime_error("description must be a string");
+ }
+ if (recipe.contains("model_url")) {
+ const auto url = recipe.at("model_url").get<std::string>();
+ const std::string prefix = "https://huggingface.co/";
+ if (url.size() <= prefix.size() || url.size() > 2048 || url.rfind(prefix, 0) != 0 ||
+ std::any_of(url.begin(), url.end(), [](unsigned char c) { return std::isspace(c) || std::iscntrl(c); })) {
+ throw std::runtime_error("model_url must be a Hugging Face HTTPS URL");
+ }
+ }
+ const auto& slots = recipe.at("slots");
+ if (!slots.is_array() || slots.empty() || slots.size() > 32) {
+ throw std::runtime_error("slots must be a nonempty array of at most 32 entries");
+ }
+ std::set<std::string> ids;
+ bool has_required_model = false;
+ for (const auto& slot : slots) {
+ check_keys(slot, {"id", "label", "category", "required"});
+ const auto id = slot.at("id").get<std::string>();
+ const auto* spec = find_model_slot(id);
+ if (!spec || !ids.insert(id == "taesd" ? "tae" : id).second ||
+ (slot.contains("category") && !matches_model_category(*spec, slot.at("category").get<std::string>())) ||
+ (slot.contains("label") && slot.at("label").get<std::string>().empty()) || !slot.at("required").is_boolean()) {
+ throw std::runtime_error("invalid or duplicate model slot: " + id);
+ }
+ has_required_model |= (id == "model" || id == "diffusion_model") && slot["required"].get<bool>();
+ }
+ if (!has_required_model) {
+ throw std::runtime_error("a required model or diffusion_model slot is needed");
+ }
+ if (recipe.contains("required_any")) {
+ const auto& groups = recipe.at("required_any");
+ if (!groups.is_array() || groups.size() > 32)
+ throw std::runtime_error("required_any must be an array of at most 32 slot groups");
+ for (const auto& group : groups) {
+ if (!group.is_array() || group.size() < 2 || group.size() > slots.size())
+ throw std::runtime_error("required_any groups need at least two declared slots");
+ std::set<std::string> seen;
+ for (const auto& value : group) {
+ const auto id = value.get<std::string>();
+ if (!seen.insert(id).second || std::none_of(slots.begin(), slots.end(), [&](const json& slot) { return slot.at("id") == id; }))
+ throw std::runtime_error("unknown or duplicate required_any slot: " + id);
+ }
+ }
+ }
+ validate_recipe_features(recipe);
+ SDContextParams context;
+ apply_recipe_context(recipe.value("context", json::object()), context);
+ SDGenerationParams defaults;
+ apply_recipe_defaults(recipe.value("defaults", json::object()), defaults);
+}
+
+static json normalize_recipe(json recipe) {
+ validate_recipe(recipe);
+ recipe["effective_features"] = resolve_recipe_features(recipe);
+ for (auto& slot : recipe.at("slots")) {
+ const auto& spec = *find_model_slot(slot.at("id").get<std::string>());
+ slot["label"] = slot.value("label", std::string(spec.label));
+ slot["category"] = slot.value("category", spec.categories.front());
+ slot["categories"] = spec.categories;
+ }
+ return recipe;
+}
+
+static bool supported_model_file(const fs::path& path, const std::string& category) {
+ std::string ext = path.extension().u8string();
+ std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); });
+ if (category == "tokenizers") {
+ return ext == ".json";
+ }
+ return ext == ".gguf" || ext == ".safetensors" || ext == ".sft" ||
+ ext == ".ckpt" || ext == ".pt" || ext == ".pth" || ext == ".bin";
+}
+
+void refresh_model_catalog(ServerRuntime& runtime) {
+ auto& models = *runtime.models;
+ std::lock_guard<std::mutex> lock(models.catalog_mutex);
+ std::map<std::string, json> recipes;
+ std::map<std::string, ModelFile> files;
+ json errors = json::array();
+ auto report = [&](const std::string& file, const std::string& message) {
+ errors.push_back({{"file", file}, {"message", message}});
+ };
+ for (const char* builtin : k_builtin_recipes) {
+ auto recipe = normalize_recipe(json::parse(builtin));
+ const auto id = recipe.at("id").get<std::string>();
+ recipe["source"] = "builtin";
+ recipes[id] = std::move(recipe);
+ }
+
+ std::set<std::string> seen_files;
+ for (const auto& source : get_model_roots(*runtime.svr_params)) {
+ const auto& root = source.path;
+ auto display_path = [&](const fs::path& path) {
+ return source.id + "/" + path.lexically_relative(root).generic_u8string();
+ };
+ const fs::path recipe_dir = root / "recipes";
+ std::map<std::string, std::vector<json>> user_recipes;
+ try {
+ if (fs::exists(recipe_dir)) {
+ if (!model_path_inside(root, fs::canonical(recipe_dir)))
+ throw std::runtime_error("recipes directory escapes model directory");
+ for (const auto& entry : fs::directory_iterator(recipe_dir)) {
+ const std::string display = display_path(entry.path());
+ if (entry.path().extension() != ".json")
+ continue;
+ try {
+ if (!entry.is_regular_file() || !model_path_inside(root, fs::canonical(entry.path())))
+ throw std::runtime_error("recipe must be a file inside the model directory");
+ if (entry.file_size() > 1024 * 1024)
+ throw std::runtime_error("recipe exceeds 1 MiB");
+ std::ifstream input(entry.path(), std::ios::binary);
+ json recipe;
+ input >> recipe;
+ recipe = normalize_recipe(std::move(recipe));
+ recipe["source"] = "user";
+ recipe["file"] = entry.path().filename().u8string();
+ recipe["root"] = source.id;
+ const auto id = recipe.at("id").get<std::string>();
+ user_recipes[id].push_back(std::move(recipe));
+ } catch (const std::exception& e) {
+ report(display, e.what());
+ }
+ }
+ }
+ } catch (const std::exception& e) {
+ report(display_path(recipe_dir), e.what());
+ }
+ for (auto& item : user_recipes) {
+ if (item.second.size() != 1) {
+ for (const auto& recipe : item.second)
+ report(source.id + "/recipes/" + recipe["file"].get<std::string>(), "duplicate user recipe id: " + item.first);
+ continue;
+ }
+ const auto existing = recipes.find(item.first);
+ if (existing != recipes.end()) {
+ if (existing->second["source"] == "user") {
+ LOG_WARN("recipe '%s' from %s is shadowed by an earlier model directory", item.first.c_str(), source.label.c_str());
+ continue;
+ }
+ LOG_INFO("user recipe '%s' from %s overrides the builtin", item.first.c_str(), source.label.c_str());
+ }
+ recipes[item.first] = std::move(item.second.front());
+ }
+
+ std::set<std::string> categories;
+ for (const auto& slot : k_model_slots)
+ categories.insert(slot.categories.begin(), slot.categories.end());
+ for (const auto& category : categories) {
+ try {
+ for (const auto& directory : model_category_directories(source, category)) {
+ if (!model_path_inside(root, fs::canonical(directory))) {
+ report(display_path(directory), "category directory escapes model directory");
+ continue;
+ }
+ for (fs::recursive_directory_iterator it(directory), end; it != end; ++it) {
+ std::error_code path_error;
+ const fs::path canonical = fs::canonical(it->path(), path_error);
+ if (path_error || !model_path_inside(root, canonical)) {
+ it.disable_recursion_pending();
+ report(display_path(it->path()), path_error ? path_error.message() : "path escapes model directory");
+ continue;
+ }
+ if (!it->is_regular_file() || !supported_model_file(it->path(), category) ||
+ !seen_files.insert(model_path_key(canonical)).second)
+ continue;
+ const auto relative = it->path().lexically_relative(root).generic_u8string();
+ const auto id = source.primary ? relative : source.id + "/" + relative;
+ files[id] = {category, it->path().lexically_relative(directory).generic_u8string(), relative, source};
+ }
+ }
+ } catch (const std::exception& e) {
+ report(source.id + "/" + category, e.what());
+ }
+ }
+ }
+ std::map<std::pair<std::string, std::string>, size_t> names;
+ for (const auto& entry : files)
+ ++names[{entry.second.category, entry.second.name}];
+ for (auto& entry : files) {
+ auto& file = entry.second;
+ if (names[{file.category, file.name}] > 1)
+ file.name = (file.root.path / fs::u8path(file.relative_path)).generic_u8string();
+ }
+ models.recipes = std::move(recipes);
+ models.files = std::move(files);
+ models.catalog_errors = std::move(errors);
+}
+
+json model_status(ServerRuntime& runtime) {
+ std::lock_guard<std::mutex> lock(runtime.models->status_mutex);
+ auto& models = *runtime.models;
+ json status = models.status;
+ if (status["state"] == "loading" && !status["load_elapsed_seconds"].is_null()) {
+ status["load_elapsed_seconds"] = std::chrono::duration<double>(std::chrono::steady_clock::now() - models.load_started_at).count();
+ }
+ return status;
+}
+
+void initialize_server_models(ServerRuntime& runtime) {
+ auto& models = *runtime.models;
+ models.base_context = *runtime.ctx_params;
+ clear_model_paths(models.base_context);
+ models.base_defaults = *runtime.default_gen_params;
+ if (runtime.sd_ctx) {
+ models.status["state"] = "ready";
+ models.status["origin"] = "command_line";
+ json features = json::array();
+ for (const auto& feature : k_recipe_features)
+ features.push_back(feature.first);
+ models.active_recipe = {
+ {"id", "@command-line"},
+ {"name", "Command-line model (all features)"},
+ {"source", "command_line"},
+ {"description", "Uses the loaded command-line model and its generation defaults."},
+ {"slots", json::array()},
+ {"defaults", json::object()},
+ {"features", features},
+ };
+ models.active_recipe["effective_features"] = resolve_recipe_features(models.active_recipe);
+ models.status["runtime_recipe"] = models.active_recipe;
+ }
+ refresh_model_catalog(runtime);
+}
+
+void prepare_model_load(ServerRuntime& runtime, const json& body, SDContextParams& context, SDGenerationParams& defaults, json* loaded_recipe) {
+ check_keys(body, {"recipe", "models"});
+ const auto recipe_id = body.at("recipe").get<std::string>();
+ auto selection = body.at("models");
+ if (!selection.is_object())
+ throw std::runtime_error("models must be an object");
+ auto& models = *runtime.models;
+ std::lock_guard<std::mutex> lock(models.catalog_mutex);
+ const auto found = models.recipes.find(recipe_id);
+ if (found == models.recipes.end())
+ throw std::runtime_error("unknown recipe");
+ const auto& recipe = found->second;
+ if (loaded_recipe)
+ *loaded_recipe = recipe;
+ context = models.base_context;
+ defaults = models.base_defaults;
+ apply_recipe_context(recipe.value("context", json::object()), context);
+ apply_recipe_defaults(recipe.value("defaults", json::object()), defaults);
+ std::set<std::string> allowed;
+ for (const auto& slot : recipe.at("slots")) {
+ const auto id = slot.at("id").get<std::string>();
+ allowed.insert(id);
+ if (id == "tae" || id == "taesd") {
+ const std::string alias = id == "tae" ? "taesd" : "tae";
+ allowed.insert(alias);
+ if (selection.contains(alias)) {
+ if (selection.contains(id))
+ throw std::runtime_error("specify only one of tae and taesd");
+ selection[id] = selection.at(alias);
+ }
+ }
+ const std::string file_id = selection.value(id, std::string());
+ if (file_id.empty()) {
+ if (slot.at("required").get<bool>())
+ throw std::runtime_error("missing required model: " + id);
+ continue;
+ }
+ const auto file = models.files.find(file_id);
+ if (file == models.files.end() || !matches_model_category(*find_model_slot(id), file->second.category)) {
+ throw std::runtime_error("unknown model or incorrect category: " + id);
+ }
+ const auto& root = file->second.root.path;
+ const fs::path path = fs::canonical(root / fs::u8path(file->second.relative_path));
+ if (!model_path_inside(root, path) || !fs::is_regular_file(path)) {
+ throw std::runtime_error("model file must remain inside the model directory: " + id);
+ }
+ context.*find_model_slot(id)->field = path.u8string();
+ }
+ for (auto it = selection.begin(); it != selection.end(); ++it) {
+ if (!allowed.count(it.key()))
+ throw std::runtime_error("unknown model slot: " + it.key());
+ }
+ for (const auto& group : recipe.value("required_any", json::array())) {
+ if (std::none_of(group.begin(), group.end(), [&](const json& value) {
+ return !selection.value(value.get<std::string>(), std::string()).empty();
+ })) {
+ throw std::runtime_error("select at least one model from: " + group.dump());
+ }
+ }
+ if (!context.resolve_and_validate(IMG_GEN))
+ throw std::runtime_error("invalid model context parameters");
+}
diff --git a/examples/server/model_catalog.h b/examples/server/model_catalog.h
new file mode 100644
index 0000000..7d49a19
--- /dev/null
+++ b/examples/server/model_catalog.h
@@ -0,0 +1,56 @@
+#ifndef __SD_EXAMPLES_SERVER_MODEL_CATALOG_H__
+#define __SD_EXAMPLES_SERVER_MODEL_CATALOG_H__
+
+#include <chrono>
+#include <map>
+#include <mutex>
+#include <shared_mutex>
+#include <string>
+
+#include "runtime.h"
+
+struct ModelRoot {
+ std::filesystem::path path;
+ std::string id;
+ std::string label;
+ bool primary = false;
+};
+
+struct ModelFile {
+ std::string category;
+ std::string name;
+ std::string relative_path;
+ ModelRoot root;
+};
+
+std::string model_path_key(const std::filesystem::path& path);
+bool model_path_inside(const std::filesystem::path& root, const std::filesystem::path& path);
+ModelRoot make_model_root(const std::filesystem::path& path, bool primary);
+std::vector<ModelRoot> get_model_roots(const SDSvrParams& params);
+std::vector<std::filesystem::path> model_category_directories(const ModelRoot& root, const std::string& category);
+
+struct ServerModels {
+ std::shared_mutex runtime_mutex;
+ std::mutex catalog_mutex;
+ std::mutex status_mutex;
+ SDCtxPtr context;
+ SDContextParams base_context;
+ SDGenerationParams base_defaults;
+ std::map<std::string, json> recipes;
+ std::map<std::string, ModelFile> files;
+ std::chrono::steady_clock::time_point load_started_at;
+ json active_recipe = nullptr;
+ json catalog_errors = json::array();
+ json status = {{"state", "empty"}, {"recipe", nullptr}, {"models", json::object()}, {"error", nullptr}, {"load_elapsed_seconds", nullptr}};
+};
+
+void refresh_model_catalog(ServerRuntime& runtime);
+json model_status(ServerRuntime& runtime);
+void initialize_server_models(ServerRuntime& runtime);
+
+json resolve_recipe_features(const json& recipe);
+json model_generation_features(const ServerRuntime& runtime);
+
+void prepare_model_load(ServerRuntime& runtime, const json& body, SDContextParams& context, SDGenerationParams& defaults, json* loaded_recipe = nullptr);
+
+#endif
diff --git a/examples/server/recipes/README.md b/examples/server/recipes/README.md
new file mode 100644
index 0000000..5043f6d
--- /dev/null
+++ b/examples/server/recipes/README.md
@@ -0,0 +1,79 @@
+# Built-in recipe policy
+
+Read these rules before adding or changing JSON files in this directory. A recipe
+represents a supported model-loading and capability configuration, not a saved
+prompt, a style preset, or a collection of sampling settings.
+
+## Required rules
+
+1. **Community editing adapters get a separate recipe.** When the base model
+ does not natively support instruction/reference-image editing and needs a
+ community LoRA to gain it, provide a dedicated editing recipe. Keep editing
+ inputs disabled in the base recipe. Examples: `anima` / `anima-edit` and
+ `krea2-raw` / `krea2-raw-edit`.
+2. **Split different task weights; share native multi-task weights.** If editing
+ and text-to-image use different generation checkpoints, use separate recipes.
+ If the same checkpoint natively supports both, use one recipe. Examples:
+ `boogu-image` / `boogu-image-edit` are separate; `flux2-dev` and
+ `qwen-image-21` each cover both generation and editing. Apply the same rule
+ to video: separate HunyuanVideo 1.5 T2V/I2V and Wan 2.2 A14B T2V/I2V weights,
+ but share one `wan22-ti2v-5b` recipe for its native T2V/I2V checkpoint.
+3. **Capability-extending weights get a separate recipe.** ControlNet,
+ IP-Adapter, PuLID, PhotoMaker, motion modules, and similar adapters that add
+ a distinct conditioning or generation capability belong in dedicated
+ recipes. Make the extension's model slots required when the schema provides
+ them. Do not add these as optional slots on the ordinary base recipe.
+4. **A LoRA alone is not a reason to split.** If adding a LoRA does not change
+ supported functionality, reuse the base recipe. Style, character, quality,
+ acceleration and distillation LoRAs do not get dedicated built-in recipes.
+ Users select the LoRA and change steps, guidance or other generation settings
+ separately. Standalone base/turbo/distilled checkpoints still follow the task,
+ component and layer-count rules; a different filename alone is not a reason
+ for another recipe.
+5. **Keep native components in the model's recipe.** Native vision encoders,
+ projectors, connectors and other components required by the model belong in
+ its existing recipe. Splitting a native component into another file does not
+ make it a capability-extending adapter under rule 3. For example, Qwen vision
+ projectors and LLaDA/LTX connectors do not get separate recipes. A slot may be
+ optional when its weights can be bundled in another selected file.
+6. **Layer-count variants share the family recipe.** Adding, repeating, removing
+ or pruning model layers does not justify a separate recipe when the runtime
+ supports the variant with the same components and capabilities. This also
+ applies to distilled checkpoints whose relevant difference is reduced depth.
+ Use `flux1-dev` for FLUX.1 Lite, `sdxl-checkpoint` for SSD-1B and Segmind Vega,
+ and the corresponding SD 1/2 checkpoint recipes for Tiny SD/Tiny SD2. Only an
+ independent difference in task, required component compatibility or latent
+ format can justify splitting such a variant; document that difference.
+
+## Interpretation and review
+
+- Instruction/reference-image editing is distinct from generic image-to-image
+ denoising. The shared `img2img` capability does not imply native editing.
+- Native components and external capability adapters are distinguished by their
+ role, not by whether their weights are stored in a separate file. LLaDA's
+ SigVQ-containing connectors remain in its shared generation/editing recipe.
+- Do not merge models solely because they descend from the same family. Flex.2
+ has different native inpainting/control conditioning, beyond its layer count
+ ([model card](https://huggingface.co/ostris/Flex.2-preview)). SDXS uses an
+ adjusted AutoEncoderTiny and a one-step model, rather than only a shallower
+ ordinary SD checkpoint ([runtime guide](../../../docs/distilled_sd.md#sdxs-512-dreamshaper)).
+ Their native components stay in their own model recipe.
+- Quantization, file format, prompt wording, seeds, step counts and CFG values
+ are not grounds for another recipe. Differences in required components,
+ latent formats or supported capabilities must be documented.
+- Evaluate what an adapter actually changes. Do not infer new functionality just
+ from a LoRA's name or from the presence of another weight file. In particular,
+ do not create an Omni-Kontext recipe solely because a LoRA is selected.
+- A recipe does not load generation LoRAs automatically. Community-edit recipes
+ must explain that the matching LoRA is required and must be selected separately.
+- Only advertise features implemented by the current runtime. Check the relevant
+ model documentation and code, and match each task's slots and feature flags to
+ its weights. Record any unverified inference paths in the change description.
+- Before submitting, validate all recipe JSON, inspect the embedded recipe API,
+ and check that task-specific inputs appear only on the appropriate recipes.
+ A catalog check is not a substitute for model-generation verification.
+
+These are built-in catalog rules. Users may keep personal presets in
+`<model-dir>/recipes/`; they need not add those presets to the built-in list.
+See [the recipe schema and feature reference](../api.md#user-recipe-format)
+for supported fields.
diff --git a/examples/server/recipes/anima-edit.json b/examples/server/recipes/anima-edit.json
new file mode 100644
index 0000000..f22c1b6
--- /dev/null
+++ b/examples/server/recipes/anima-edit.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "id": "anima-edit",
+ "name": "Anima Edit (LoRA)",
+ "description": "Requires a matching community editing LoRA. Apply it after loading and supply reference images; uses the cosmos_reference preset.",
+ "model_url": "https://huggingface.co/circlestone-labs/Anima",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 0.6B Base Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "scheduler": "discrete",
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/anima.json b/examples/server/recipes/anima.json
new file mode 100644
index 0000000..4809bd4
--- /dev/null
+++ b/examples/server/recipes/anima.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "anima",
+ "name": "Anima",
+ "description": "Supports all Anima variants supported by sd.cpp. Use a compatible Qwen Image VAE or TAE; adjust generation settings for distilled variants.",
+ "model_url": "https://huggingface.co/circlestone-labs/Anima",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 0.6B Base Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/animatediff-sd15.json b/examples/server/recipes/animatediff-sd15.json
new file mode 100644
index 0000000..a7c8338
--- /dev/null
+++ b/examples/server/recipes/animatediff-sd15.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "id": "animatediff-sd15",
+ "name": "AnimateDiff SD 1.5",
+ "description": "Use Video Generation with an SD 1.5 checkpoint and compatible motion module.",
+ "model_url": "https://huggingface.co/guoyww/animatediff",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "motion_module",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 8,
+ "fps": 8,
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "init_image"
+ ],
+ "disabled_features": [
+ "cache_dit",
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/boogu-image-edit.json b/examples/server/recipes/boogu-image-edit.json
new file mode 100644
index 0000000..ff1aaee
--- /dev/null
+++ b/examples/server/recipes/boogu-image-edit.json
@@ -0,0 +1,50 @@
+{
+ "schema_version": 1,
+ "id": "boogu-image-edit",
+ "name": "Boogu Image Edit",
+ "description": "Supply reference images. The separate vision projector is needed only when it is not bundled in the text encoder.",
+ "model_url": "https://huggingface.co/Boogu/Boogu-Image-0.1-Edit",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 8B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "img_cfg_scale": 1,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/boogu-image.json b/examples/server/recipes/boogu-image.json
new file mode 100644
index 0000000..24c6a42
--- /dev/null
+++ b/examples/server/recipes/boogu-image.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "boogu-image",
+ "name": "Boogu Image Base",
+ "model_url": "https://huggingface.co/Boogu/Boogu-Image-0.1-Base",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 8B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/chroma-radiance.json b/examples/server/recipes/chroma-radiance.json
new file mode 100644
index 0000000..6a0be8a
--- /dev/null
+++ b/examples/server/recipes/chroma-radiance.json
@@ -0,0 +1,32 @@
+{
+ "schema_version": 1,
+ "id": "chroma-radiance",
+ "name": "Chroma Radiance",
+ "description": "Pixel-space generation without a VAE. Also accepts the x0 and experimental patch-32 variants.",
+ "model_url": "https://huggingface.co/lodestones/Chroma1-Radiance",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 3.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ],
+ "disabled_features": [
+ "preview",
+ "vae_tiling"
+ ]
+}
diff --git a/examples/server/recipes/chroma.json b/examples/server/recipes/chroma.json
new file mode 100644
index 0000000..b8d1da0
--- /dev/null
+++ b/examples/server/recipes/chroma.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "chroma",
+ "name": "Chroma",
+ "model_url": "https://huggingface.co/lodestones/Chroma1-HD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/cosxl-edit.json b/examples/server/recipes/cosxl-edit.json
new file mode 100644
index 0000000..49407fc
--- /dev/null
+++ b/examples/server/recipes/cosxl-edit.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "cosxl-edit",
+ "name": "CosXL Edit",
+ "description": "Supply the image to edit as a reference image.",
+ "model_url": "https://huggingface.co/stabilityai/cosxl",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "img_cfg_scale": 1.5,
+ "sampling_method": "euler_a",
+ "scheduler": "karras"
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "img_cfg",
+ "ref_images",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/defaults.md b/examples/server/recipes/defaults.md
new file mode 100644
index 0000000..2be4dfb
--- /dev/null
+++ b/examples/server/recipes/defaults.md
@@ -0,0 +1,124 @@
+# Built-in generation defaults
+
+Reviewed against model-publisher cards and reference implementations on 2026-10-20.
+
+These are starting values, not limits on supported resolutions or generation settings.
+Built-in step defaults above 20 are reduced to 20; the cited sources may recommend more steps.
+Users can still choose a higher step count.
+When several aspect ratios or ranges are published, the selected example or square bucket is noted below.
+A dash means no recipe override: the available primary sources did not establish a fixed value, or the size depends on the input image.
+Ordinary DiT recipes explicitly use Euler; PiD retains its LCM sampling path.
+Guidance-distilled models use `cfg_scale: 1` to disable external CFG in sd.cpp. FLUX embedded guidance is stored separately as `distilled_guidance`.
+For community edit adapters and other optional extensions, these are the base-model settings unless the source specifies otherwise.
+
+| Recipe | CFG | Steps | Width × height | Source / notes |
+| --- | --- | --- | --- | --- |
+| `anima` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/circlestone-labs/Anima) The official ranges are 30–50 steps / CFG 4–5; CFG 4 selects the lower end. Edit uses the same base; tune the chosen editing LoRA separately. |
+| `anima-edit` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/circlestone-labs/Anima) The official ranges are 30–50 steps / CFG 4–5; CFG 4 selects the lower end. Edit uses the same base; tune the chosen editing LoRA separately. |
+| `animatediff-sd15` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/guoyww/AnimateDiff/main/configs/prompts/1_animate/1_3_animate_ToonYou.yaml) |
+| `boogu-image` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/Boogu/Boogu-Image-0.1-Base) |
+| `boogu-image-edit` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/Boogu/Boogu-Image-0.1-Edit) |
+| `chroma` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/lodestones/Chroma1-HD/raw/main/README.md) |
+| `chroma-radiance` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/lodestones/Chroma1-Radiance/raw/main/inference_reference_code.py) |
+| `cosxl-edit` | — | — | — | [Source](https://huggingface.co/stabilityai/cosxl) No numerical generation defaults in the author’s model card; previous test values were removed. |
+| `ernie-image` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/baidu/ERNIE-Image) |
+| `ernie-image-turbo` | 1 | 8 | 1024 × 1024 | [Source](https://huggingface.co/baidu/ERNIE-Image-Turbo) |
+| `flex2` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/ostris/Flex.2-preview) |
+| `flux1-canny` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev) |
+| `flux1-depth` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev) |
+| `flux1-dev` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-dev) |
+| `flux1-fill` | 1 | 20 | 1232 × 1632 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev) |
+| `flux1-kontext` | 1 | 20 | — | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/flux/pipeline_flux_kontext.py) Output size normally follows the reference image; no fixed size is imposed by this recipe. |
+| `flux1-pulid` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-dev) FLUX.1-dev defaults; identity conditioning is provided by PuLID. |
+| `flux1-schnell` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-schnell) |
+| `flux2-dev` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-dev) |
+| `flux2-klein-4b` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) |
+| `flux2-klein-9b` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-9B) |
+| `flux2-klein-base-4b` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B) |
+| `flux2-klein-base-9b` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B) |
+| `flux3-klein` | 1 | 16 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/black-forest-labs/flux-3-image-klein/main/README.md) |
+| `flux3-klein-base` | 4 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/black-forest-labs/flux-3-image-klein/main/src/flux3/pipeline.py) |
+| `hidream-o1-dev` | 1 | 20 | 2048 × 2048 | [Source](https://huggingface.co/HiDream-ai/HiDream-O1-Image-Dev) |
+| `hunyuan-video-15-i2v` | 6 | 20 | 1280 × 720 | [Source](https://huggingface.co/tencent/HunyuanVideo-1.5) 720p base I2V defaults; CFG/step-distilled and 480p checkpoints use different settings. |
+| `hunyuan-video-15-t2v` | 6 | 20 | 1280 × 720 | [Source](https://huggingface.co/tencent/HunyuanVideo-1.5) 720p base T2V defaults; CFG-distilled / 480p checkpoints need their published settings. |
+| `ideogram4` | 7 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/ideogram-oss/ideogram4/main/docs/inference.md) Constant-guidance pipeline defaults; this recipe does not reproduce the CLI’s per-step guidance schedule. |
+| `instruct-pix2pix` | — | 10 | — | [Source](https://huggingface.co/timbrooks/instruct-pix2pix) Official quick-start example; output size follows the input image. |
+| `iris-3b` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/speridlabs/iris-3b) |
+| `krea2-raw` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/krea/Krea-2-Raw) Base model defaults; edit requires a matching community LoRA with its own settings. |
+| `krea2-raw-edit` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/krea/Krea-2-Raw) Base model defaults; edit requires a matching community LoRA with its own settings. |
+| `krea2-turbo` | 1 | 8 | 2048 × 2048 | [Source](https://huggingface.co/krea/Krea-2-Turbo) Official CLI example; edit requires a matching community LoRA. |
+| `krea2-turbo-edit` | 1 | 8 | 2048 × 2048 | [Source](https://huggingface.co/krea/Krea-2-Turbo) Official CLI example; edit requires a matching community LoRA. |
+| `lens` | 5 | 20 | 1440 × 1440 | [Source](https://huggingface.co/microsoft/Lens) Official model card: RL model, default base_resolution 1440. |
+| `lens-turbo` | 1 | 4 | 1440 × 1440 | [Source](https://huggingface.co/microsoft/Lens-Turbo) Official model card: 4-step distilled model, default base_resolution 1440. |
+| `lingbot-video-dense` | 3 | 20 | 832 × 480 | [Source](https://huggingface.co/robbyant/lingbot-video-dense-1.3b) Official first-stage inference example; optional refiner is separate. |
+| `lingbot-video-moe` | 3 | 20 | 832 × 480 | [Source](https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b) Official inference example. |
+| `llada-image` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/inclusionAI/LLaDA-Image) |
+| `llada-image-turbo` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/inclusionAI/LLaDA-Image-Turbo) |
+| `longcat-image` | 4 | 20 | 1344 × 768 | [Source](https://huggingface.co/meituan-longcat/LongCat-Image) |
+| `longcat-image-edit` | 4.5 | 20 | — | [Source](https://huggingface.co/meituan-longcat/LongCat-Image-Edit) Reference-image-dependent output size; no fixed size is imposed. |
+| `ltx23-dev` | 3 | 20 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Official single-stage parameters, before optional latent upscaling. |
+| `ltx23-distilled` | 1 | 8 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Model card specifies 8 steps / CFG 1; size uses official pipeline’s first stage. |
+| `ltx25-dev` | 3 | 20 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Official non-distilled pipeline parameters for model versions >= 2.4, before optional upscaling. |
+| `mage-flow` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) |
+| `mage-flow-edit` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) |
+| `mage-flow-turbo` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) |
+| `ming-image` | 1 | 12 | 2048 × 2048 | [Source](https://huggingface.co/inclusionAI/Ming-Image-0.1-Design) |
+| `minimax-h3-fl2va` | 1 | — | 1344 × 768 | [Source](https://huggingface.co/docs/diffusers/main/en/api/pipelines/minimax_h3) Guidance-distilled; official 768p canvas. No fixed step count is prescribed here; the upstream API counts sigma grid points rather than sd.cpp denoising steps. |
+| `minimax-h3-ref2va` | 1 | — | 1344 × 768 | [Source](https://huggingface.co/docs/diffusers/main/en/api/pipelines/minimax_h3) Guidance-distilled; official 768p canvas. No fixed step count is prescribed here; the upstream API counts sigma grid points rather than sd.cpp denoising steps. |
+| `minit2i` | 2.5 | 20 | 512 × 512 | [Source](https://huggingface.co/MiniT2I/MiniT2I/raw/main/pipeline.py) B/16 example and native model size; L/16 uses CFG 6. |
+| `moss-image-base` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base) |
+| `moss-image-sft` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT) |
+| `nitrosd-realism` | 1 | 1 | 1024 × 1024 | [Source](https://huggingface.co/ChenDY/NitroFusion) |
+| `ovis-image` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/ATH-MaaS/Ovis-Image-7B) |
+| `pid` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-15` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-15-flux2` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-15-qwen-image` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-15-sd3` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-flux2` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-qwen-image` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pid-sd3` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
+| `pixart-alpha-1024` | 4.5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_alpha.py) |
+| `pixart-alpha-512` | 4.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_alpha.py) |
+| `pixart-sigma` | 4.5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_sigma.py) |
+| `qwen-image` | 4 | 20 | 1328 × 1328 | [Source](https://huggingface.co/Qwen/Qwen-Image) Official square resolution bucket; true_cfg_scale maps to cfg_scale. |
+| `qwen-image-21` | 1 | 20 | 2048 × 2048 | [Source](https://raw.githubusercontent.com/QwenLM/Qwen-Image-2.1/main/README.md) Original checkpoint defaults; select the matching TAEQI2.1 to replace its VAE. |
+| `qwen-image-21-turbo` | 1 | 8 | 2048 × 2048 | [Model card](https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo), [fixed sigmas](https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo/blob/main/model_index.json). Standalone Turbo weights; the fixed 8-step schedule is applied on model load. |
+| `qwen-image-edit` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit) Official editing example; output dimensions follow reference-image aspect ratio. |
+| `qwen-image-edit-2509` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit-2509) Official editing example; output dimensions follow reference-image aspect ratio. |
+| `qwen-image-edit-2511` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) Official editing example; output dimensions follow reference-image aspect ratio. |
+| `qwen-image-layered` | 4 | 20 | 640 × 640 | [Source](https://huggingface.co/Qwen/Qwen-Image-Layered) Official recommended 640 resolution bucket; layered RGBA VAE is required. |
+| `sd-turbo` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/stabilityai/sd-turbo) |
+| `sd1-checkpoint` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) |
+| `sd1-controlnet` | 7.5 | 20 | 512 × 512 | [Source](https://huggingface.co/lllyasviel/control_v11p_sd15_canny) |
+| `sd1-inpainting` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) |
+| `sd1-ip-adapter` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) SD 1.x base pipeline defaults; adapter strength is configured separately. |
+| `sd2-512-checkpoint` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) |
+| `sd2-768-checkpoint` | 7.5 | 20 | 768 × 768 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) |
+| `sd2-inpainting` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) |
+| `sd3-medium` | 7 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3-medium-diffusers) |
+| `sd35-large` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3.5-large) |
+| `sd35-medium` | 4.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3.5-medium) |
+| `sdxl-checkpoint` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) |
+| `sdxl-controlnet` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) SDXL pipeline defaults; compatible ControlNet weights are required. |
+| `sdxl-inpainting` | 8 | 20 | 1024 × 1024 | [Source](https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1/raw/main/README.md) |
+| `sdxl-ip-adapter` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) SDXL base pipeline defaults; adapter strength is configured separately. |
+| `sdxl-photomaker` | — | — | 1024 × 1024 | [Source](https://huggingface.co/TencentARC/PhotoMaker) Use settings for the selected compatible SDXL checkpoint; no unique CFG/steps are prescribed by the model card. |
+| `sdxl-turbo` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/stabilityai/sdxl-turbo) |
+| `sdxs` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/IDKiro/sdxs-512-dreamshaper) |
+| `sefi-image` | 4 | 20 | — | [Source](https://huggingface.co/SeFi-Image/SeFi-Image-5B-Base) Base/RL family defaults; Turbo checkpoints require 4 steps / CFG 1. |
+| `sensenova-u15` | 4 | 20 | 2048 × 2048 | [Source](https://raw.githubusercontent.com/OpenSenseNova/SenseNova-U1/main/examples/README.md) |
+| `wan21-flf2v` | 5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) |
+| `wan21-i2v` | 5 | 20 | 832 × 480 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) 480p base defaults; also accepts compatible SkyReels V2 and single Wan 2.2 I2V experts. |
+| `wan21-t2v` | 5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) 14B base defaults; single Wan 2.2 experts and acceleration LoRAs need matching settings. |
+| `wan21-vace` | 5 | 20 | 832 × 480 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) |
+| `wan22-i2v` | 3.5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_i2v_A14B.py) |
+| `wan22-s2v` | 4.5 | 20 | — | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_s2v_14B.py) The official S2V pipeline derives output dimensions from the input. |
+| `wan22-t2v` | 3 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_t2v_A14B.py) |
+| `wan22-ti2v-5b` | 5 | 20 | 1280 × 704 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_ti2v_5B.py) |
+| `z-image` | 4 | 20 | 720 × 1280 | [Source](https://huggingface.co/Tongyi-MAI/Z-Image) |
+| `z-image-l2p` | — | — | — | [Source](https://huggingface.co/zhen-nan/L2P) No numerical inference recommendations in the author’s model card; previous test values were removed. |
+| `z-image-turbo` | 1 | 9 | 1024 × 1024 | [Source](https://huggingface.co/Tongyi-MAI/Z-Image-Turbo) |
+
+TAE alternatives use the model-compatible weights listed by [TAESD](https://github.com/madebyollin/taesd) and [TAEHV](https://github.com/madebyollin/taehv).
+TAE slots are omitted for pixel-space models, PiD, layered RGBA decoding and Mage-VAE paths that the current runtime does not map to a compatible TAE.
diff --git a/examples/server/recipes/ernie-image-turbo.json b/examples/server/recipes/ernie-image-turbo.json
new file mode 100644
index 0000000..5702f5e
--- /dev/null
+++ b/examples/server/recipes/ernie-image-turbo.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "ernie-image-turbo",
+ "name": "ERNIE Image Turbo",
+ "model_url": "https://huggingface.co/baidu/ERNIE-Image-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Ministral 3 3B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 8,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/ernie-image.json b/examples/server/recipes/ernie-image.json
new file mode 100644
index 0000000..a610000
--- /dev/null
+++ b/examples/server/recipes/ernie-image.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "ernie-image",
+ "name": "ERNIE Image",
+ "model_url": "https://huggingface.co/baidu/ERNIE-Image",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Ministral 3 3B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/flex2.json b/examples/server/recipes/flex2.json
new file mode 100644
index 0000000..bbd900c
--- /dev/null
+++ b/examples/server/recipes/flex2.json
@@ -0,0 +1,48 @@
+{
+ "schema_version": 1,
+ "id": "flex2",
+ "name": "Flex.2 Preview",
+ "model_url": "https://huggingface.co/ostris/Flex.2-preview",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "distilled_guidance": 3.5
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux1-canny.json b/examples/server/recipes/flux1-canny.json
new file mode 100644
index 0000000..de9bb60
--- /dev/null
+++ b/examples/server/recipes/flux1-canny.json
@@ -0,0 +1,53 @@
+{
+ "schema_version": 1,
+ "id": "flux1-canny",
+ "name": "FLUX.1 Canny dev",
+ "description": "Select Canny weights and supply a precomputed edge map as Control Image. Edge detection is not performed by this recipe.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "distilled_guidance": 30
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg",
+ "control_image"
+ ],
+ "disabled_features": [
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/flux1-depth.json b/examples/server/recipes/flux1-depth.json
new file mode 100644
index 0000000..0efefd2
--- /dev/null
+++ b/examples/server/recipes/flux1-depth.json
@@ -0,0 +1,54 @@
+{
+ "schema_version": 1,
+ "id": "flux1-depth",
+ "name": "FLUX.1 Depth dev",
+ "description": "Select Depth weights and supply a precomputed depth map as Control Image. Depth estimation is not performed by this recipe.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "distilled_guidance": 10,
+ "scheduler": "flux",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg",
+ "control_image"
+ ],
+ "disabled_features": [
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/flux1-dev.json b/examples/server/recipes/flux1-dev.json
new file mode 100644
index 0000000..9356195
--- /dev/null
+++ b/examples/server/recipes/flux1-dev.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "flux1-dev",
+ "name": "FLUX.1 dev",
+ "description": "Supports FLUX.1 dev and compatible layer-count variants such as FLUX.1 Lite. Select the transformer with matching CLIP-L, T5XXL and FLUX.1 VAE.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "distilled_guidance": 3.5
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux1-fill.json b/examples/server/recipes/flux1-fill.json
new file mode 100644
index 0000000..5e6485a
--- /dev/null
+++ b/examples/server/recipes/flux1-fill.json
@@ -0,0 +1,52 @@
+{
+ "schema_version": 1,
+ "id": "flux1-fill",
+ "name": "FLUX.1 Fill dev",
+ "description": "Select Fill weights and supply Init Image plus Mask Image for inpainting. Denoising strength defaults to 1 for a full denoising schedule.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "distilled_guidance": 30,
+ "strength": 1.0,
+ "scheduler": "flux",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1232,
+ "height": 1632
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg",
+ "img_cfg"
+ ]
+}
diff --git a/examples/server/recipes/flux1-kontext.json b/examples/server/recipes/flux1-kontext.json
new file mode 100644
index 0000000..d66e41a
--- /dev/null
+++ b/examples/server/recipes/flux1-kontext.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "flux1-kontext",
+ "name": "FLUX.1 Kontext dev",
+ "description": "Select FLUX.1 Kontext weights. Supports text-to-image and native reference-image editing; an editing LoRA is not required.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "distilled_guidance": 2.5
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux1-pulid.json b/examples/server/recipes/flux1-pulid.json
new file mode 100644
index 0000000..8203dd1
--- /dev/null
+++ b/examples/server/recipes/flux1-pulid.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": 1,
+ "id": "flux1-pulid",
+ "name": "FLUX.1 dev PuLID",
+ "description": "Supply a precomputed PuLID identity embedding with the generation request.",
+ "model_url": "https://huggingface.co/guozinan/PuLID",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "pulid_weights",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "distilled_guidance": 3.5
+ },
+ "features": [
+ "clip_skip",
+ "distilled_guidance",
+ "flow_shift",
+ "slg"
+ ],
+ "disabled_features": [
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux1-schnell.json b/examples/server/recipes/flux1-schnell.json
new file mode 100644
index 0000000..106fef5
--- /dev/null
+++ b/examples/server/recipes/flux1-schnell.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "flux1-schnell",
+ "name": "FLUX.1 schnell",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "clip_skip",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux2-dev.json b/examples/server/recipes/flux2-dev.json
new file mode 100644
index 0000000..4d27ea1
--- /dev/null
+++ b/examples/server/recipes/flux2-dev.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "flux2-dev",
+ "name": "FLUX.2 dev",
+ "description": "Supports text-to-image and reference-image editing.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-dev",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Mistral Small 3.2 24B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "distilled_guidance": 4,
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "distilled_guidance",
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux2-klein-4b.json b/examples/server/recipes/flux2-klein-4b.json
new file mode 100644
index 0000000..f09360c
--- /dev/null
+++ b/examples/server/recipes/flux2-klein-4b.json
@@ -0,0 +1,45 @@
+{
+ "schema_version": 1,
+ "id": "flux2-klein-4b",
+ "name": "FLUX.2 Klein 4B",
+ "description": "Supports reference-image editing. The FLUX.2 small decoder is an alternative VAE.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-4B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux2-klein-9b.json b/examples/server/recipes/flux2-klein-9b.json
new file mode 100644
index 0000000..ad10d6e
--- /dev/null
+++ b/examples/server/recipes/flux2-klein-9b.json
@@ -0,0 +1,45 @@
+{
+ "schema_version": 1,
+ "id": "flux2-klein-9b",
+ "name": "FLUX.2 Klein 9B",
+ "description": "Supports text-to-image and reference-image editing.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-9B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 8B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux2-klein-base-4b.json b/examples/server/recipes/flux2-klein-base-4b.json
new file mode 100644
index 0000000..56c69f9
--- /dev/null
+++ b/examples/server/recipes/flux2-klein-base-4b.json
@@ -0,0 +1,45 @@
+{
+ "schema_version": 1,
+ "id": "flux2-klein-base-4b",
+ "name": "FLUX.2 Klein Base 4B",
+ "description": "Supports text-to-image and reference-image editing.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux2-klein-base-9b.json b/examples/server/recipes/flux2-klein-base-9b.json
new file mode 100644
index 0000000..e9bd53c
--- /dev/null
+++ b/examples/server/recipes/flux2-klein-base-9b.json
@@ -0,0 +1,45 @@
+{
+ "schema_version": 1,
+ "id": "flux2-klein-base-9b",
+ "name": "FLUX.2 Klein Base 9B",
+ "description": "Use the 9B base transformer with Qwen 3 8B. Supports reference-image editing.",
+ "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 8B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/flux3-klein-base.json b/examples/server/recipes/flux3-klein-base.json
new file mode 100644
index 0000000..29f6f68
--- /dev/null
+++ b/examples/server/recipes/flux3-klein-base.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "flux3-klein-base",
+ "name": "FLUX.3 Image Klein Base",
+ "model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein-base",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/flux3-klein.json b/examples/server/recipes/flux3-klein.json
new file mode 100644
index 0000000..164bce1
--- /dev/null
+++ b/examples/server/recipes/flux3-klein.json
@@ -0,0 +1,43 @@
+{
+ "schema_version": 1,
+ "id": "flux3-klein",
+ "name": "FLUX.3 Image Klein",
+ "description": "Supports text-to-image and reference-image editing.",
+ "model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 16,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/hidream-o1-dev.json b/examples/server/recipes/hidream-o1-dev.json
new file mode 100644
index 0000000..8e95f5c
--- /dev/null
+++ b/examples/server/recipes/hidream-o1-dev.json
@@ -0,0 +1,24 @@
+{
+ "schema_version": 1,
+ "id": "hidream-o1-dev",
+ "name": "HiDream O1 Image dev",
+ "description": "Bundled checkpoint; also supports reference-image editing.",
+ "model_url": "https://huggingface.co/HiDream-ai/HiDream-O1-Image-Dev",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 2048,
+ "height": 2048,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/hunyuan-video-15-i2v.json b/examples/server/recipes/hunyuan-video-15-i2v.json
new file mode 100644
index 0000000..a4d672f
--- /dev/null
+++ b/examples/server/recipes/hunyuan-video-15-i2v.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": 1,
+ "id": "hunyuan-video-15-i2v",
+ "name": "HunyuanVideo 1.5 I2V",
+ "description": "Select HunyuanVideo 1.5 I2V weights and supply a start image in Video Generation.",
+ "model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "ByT5 Small GlyphXL"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "HunyuanVideo 1.5 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false,
+ "label": "TAEHV 1.5"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "cfg_scale": 6,
+ "steps": 20,
+ "width": 1280,
+ "height": 720,
+ "flow_shift": 7
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/hunyuan-video-15-t2v.json b/examples/server/recipes/hunyuan-video-15-t2v.json
new file mode 100644
index 0000000..d477b80
--- /dev/null
+++ b/examples/server/recipes/hunyuan-video-15-t2v.json
@@ -0,0 +1,55 @@
+{
+ "schema_version": 1,
+ "id": "hunyuan-video-15-t2v",
+ "name": "HunyuanVideo 1.5 T2V",
+ "description": "Select HunyuanVideo 1.5 T2V weights and use Video Generation. Use the separate I2V recipe for I2V weights.",
+ "model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "ByT5 Small GlyphXL"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "HunyuanVideo 1.5 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false,
+ "label": "TAEHV 1.5"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "cfg_scale": 6,
+ "steps": 20,
+ "width": 1280,
+ "height": 720,
+ "flow_shift": 9
+ },
+ "features": [
+ "flow_shift"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/ideogram4.json b/examples/server/recipes/ideogram4.json
new file mode 100644
index 0000000..2d52300
--- /dev/null
+++ b/examples/server/recipes/ideogram4.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "ideogram4",
+ "name": "Ideogram 4",
+ "description": "Select the matching unconditional transformer for classifier-free guidance.",
+ "model_url": "https://huggingface.co/ideogram-ai/ideogram-4-fp8",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "uncond_diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 8B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 7,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/instruct-pix2pix.json b/examples/server/recipes/instruct-pix2pix.json
new file mode 100644
index 0000000..e819509
--- /dev/null
+++ b/examples/server/recipes/instruct-pix2pix.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "instruct-pix2pix",
+ "name": "Instruct-Pix2Pix",
+ "description": "Supply the image to edit as a reference image.",
+ "model_url": "https://huggingface.co/timbrooks/instruct-pix2pix",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "img_cfg_scale": 1,
+ "sampling_method": "euler_a",
+ "scheduler": "karras",
+ "steps": 10
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "img_cfg",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/iris-3b.json b/examples/server/recipes/iris-3b.json
new file mode 100644
index 0000000..c1b5d84
--- /dev/null
+++ b/examples/server/recipes/iris-3b.json
@@ -0,0 +1,32 @@
+{
+ "schema_version": 1,
+ "id": "iris-3b",
+ "name": "Iris 3B",
+ "description": "Pixel-space model; no VAE is required.",
+ "model_url": "https://huggingface.co/speridlabs/iris-3b",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ],
+ "disabled_features": [
+ "preview",
+ "vae_tiling"
+ ]
+}
diff --git a/examples/server/recipes/krea2-raw-edit.json b/examples/server/recipes/krea2-raw-edit.json
new file mode 100644
index 0000000..8d3eb9c
--- /dev/null
+++ b/examples/server/recipes/krea2-raw-edit.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "krea2-raw-edit",
+ "name": "Krea 2 Raw Edit (LoRA)",
+ "description": "Requires a matching Ostris-compatible community editing LoRA. Apply it after loading and supply reference images; uses the krea2_ostris_edit preset. Select a separate vision projector when it is not bundled in the text encoder.",
+ "model_url": "https://huggingface.co/krea/Krea-2-Raw",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler",
+ "cfg_scale": 3.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/krea2-raw.json b/examples/server/recipes/krea2-raw.json
new file mode 100644
index 0000000..5878291
--- /dev/null
+++ b/examples/server/recipes/krea2-raw.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "krea2-raw",
+ "name": "Krea 2 Raw",
+ "description": "For reference-image editing with a community LoRA, use the corresponding Krea 2 Edit recipe.",
+ "model_url": "https://huggingface.co/krea/Krea-2-Raw",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 3.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/krea2-turbo-edit.json b/examples/server/recipes/krea2-turbo-edit.json
new file mode 100644
index 0000000..fa248c0
--- /dev/null
+++ b/examples/server/recipes/krea2-turbo-edit.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "krea2-turbo-edit",
+ "name": "Krea 2 Turbo Edit (LoRA)",
+ "description": "Requires a matching Ostris-compatible community editing LoRA. Apply it after loading and supply reference images; uses the krea2_ostris_edit preset. Select a separate vision projector when it is not bundled in the text encoder.",
+ "model_url": "https://huggingface.co/krea/Krea-2-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 8,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/krea2-turbo.json b/examples/server/recipes/krea2-turbo.json
new file mode 100644
index 0000000..23dad12
--- /dev/null
+++ b/examples/server/recipes/krea2-turbo.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "krea2-turbo",
+ "name": "Krea 2 Turbo",
+ "description": "For reference-image editing with a community LoRA, use the corresponding Krea 2 Edit recipe.",
+ "model_url": "https://huggingface.co/krea/Krea-2-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 8,
+ "width": 2048,
+ "height": 2048,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/lens-turbo.json b/examples/server/recipes/lens-turbo.json
new file mode 100644
index 0000000..e9282ea
--- /dev/null
+++ b/examples/server/recipes/lens-turbo.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "lens-turbo",
+ "name": "LENS Turbo",
+ "model_url": "https://huggingface.co/microsoft/Lens-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "GPT-OSS 20B Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "GPT-OSS tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1440,
+ "height": 1440,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/lens.json b/examples/server/recipes/lens.json
new file mode 100644
index 0000000..0a89542
--- /dev/null
+++ b/examples/server/recipes/lens.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "lens",
+ "name": "LENS",
+ "model_url": "https://huggingface.co/microsoft/Lens",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "GPT-OSS 20B Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "GPT-OSS tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1440,
+ "height": 1440,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/lingbot-video-dense.json b/examples/server/recipes/lingbot-video-dense.json
new file mode 100644
index 0000000..5f313a0
--- /dev/null
+++ b/examples/server/recipes/lingbot-video-dense.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "lingbot-video-dense",
+ "name": "LingBot Video Dense 1.3B",
+ "description": "Use Video Generation.",
+ "model_url": "https://huggingface.co/robbyant/lingbot-video-dense-1.3b",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "video_frames": 33,
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 832,
+ "height": 480,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/lingbot-video-moe.json b/examples/server/recipes/lingbot-video-moe.json
new file mode 100644
index 0000000..cdbce61
--- /dev/null
+++ b/examples/server/recipes/lingbot-video-moe.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "lingbot-video-moe",
+ "name": "LingBot Video MoE 30B A3B",
+ "description": "Use Video Generation.",
+ "model_url": "https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "video_frames": 33,
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 832,
+ "height": 480,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/llada-image-turbo.json b/examples/server/recipes/llada-image-turbo.json
new file mode 100644
index 0000000..0589bb3
--- /dev/null
+++ b/examples/server/recipes/llada-image-turbo.json
@@ -0,0 +1,52 @@
+{
+ "schema_version": 1,
+ "id": "llada-image-turbo",
+ "name": "LLaDA Image Turbo",
+ "description": "Use matching turbo text encoder and connectors. Editing needs connectors that include the SigVQ encoder.",
+ "model_url": "https://huggingface.co/inclusionAI/LLaDA-Image-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "LLaDA-Image Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "LLaDA tokenizer.json"
+ },
+ {
+ "id": "embeddings_connectors",
+ "required": true,
+ "label": "LLaDA Turbo Connectors"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/llada-image.json b/examples/server/recipes/llada-image.json
new file mode 100644
index 0000000..27887c8
--- /dev/null
+++ b/examples/server/recipes/llada-image.json
@@ -0,0 +1,53 @@
+{
+ "schema_version": 1,
+ "id": "llada-image",
+ "name": "LLaDA Image (base)",
+ "description": "Use matching base transformer, text encoder and connectors; do not mix them with Turbo components. Editing needs connectors with SigVQ and 1024x1024 output.",
+ "model_url": "https://huggingface.co/inclusionAI/LLaDA-Image",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "LLaDA-Image Base Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "LLaDA tokenizer.json"
+ },
+ {
+ "id": "embeddings_connectors",
+ "required": true,
+ "label": "LLaDA Base Connectors"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "scheduler": "llada_image",
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/longcat-image-edit.json b/examples/server/recipes/longcat-image-edit.json
new file mode 100644
index 0000000..bc15ea2
--- /dev/null
+++ b/examples/server/recipes/longcat-image-edit.json
@@ -0,0 +1,43 @@
+{
+ "schema_version": 1,
+ "id": "longcat-image-edit",
+ "name": "LongCat Image Edit",
+ "description": "Supply reference images for editing.",
+ "model_url": "https://huggingface.co/meituan-longcat/LongCat-Image-Edit",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4.5,
+ "steps": 20,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/longcat-image.json b/examples/server/recipes/longcat-image.json
new file mode 100644
index 0000000..36e23e8
--- /dev/null
+++ b/examples/server/recipes/longcat-image.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "longcat-image",
+ "name": "LongCat Image",
+ "model_url": "https://huggingface.co/meituan-longcat/LongCat-Image",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1344,
+ "height": 768,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/ltx23-dev.json b/examples/server/recipes/ltx23-dev.json
new file mode 100644
index 0000000..83bb934
--- /dev/null
+++ b/examples/server/recipes/ltx23-dev.json
@@ -0,0 +1,60 @@
+{
+ "schema_version": 1,
+ "id": "ltx23-dev",
+ "name": "LTX 2.3 dev",
+ "description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings. Optional distillation LoRAs use this recipe; select the LoRA and adjust the sampling settings separately.",
+ "model_url": "https://huggingface.co/Lightricks/LTX-2.3",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 3 12B Text Encoder"
+ },
+ {
+ "id": "embeddings_connectors",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "LTX 2.3 Video VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "audio_vae",
+ "required": false,
+ "label": "LTX 2.3 Audio VAE"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "fps": 24,
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 768,
+ "height": 512
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/ltx23-distilled.json b/examples/server/recipes/ltx23-distilled.json
new file mode 100644
index 0000000..a2a6ac3
--- /dev/null
+++ b/examples/server/recipes/ltx23-distilled.json
@@ -0,0 +1,60 @@
+{
+ "schema_version": 1,
+ "id": "ltx23-distilled",
+ "name": "LTX 2.3 Distilled 1.1",
+ "description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings.",
+ "model_url": "https://huggingface.co/Lightricks/LTX-2.3",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 3 12B Text Encoder"
+ },
+ {
+ "id": "embeddings_connectors",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "LTX 2.3 Video VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "audio_vae",
+ "required": false,
+ "label": "LTX 2.3 Audio VAE"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 121,
+ "fps": 24,
+ "cfg_scale": 1,
+ "steps": 8,
+ "width": 768,
+ "height": 512
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/ltx25-dev.json b/examples/server/recipes/ltx25-dev.json
new file mode 100644
index 0000000..47efbc7
--- /dev/null
+++ b/examples/server/recipes/ltx25-dev.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": 1,
+ "id": "ltx25-dev",
+ "name": "LTX 2.5 dev",
+ "description": "Use the conv video VAE and LTX-specific Gemma 4 with bundled projection. Audio VAE is optional for silent video.",
+ "model_url": "https://huggingface.co/Lightricks/LTX-2.5",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "LTX-2.5 Gemma 4 12B with projection"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "LTX 2.5 Conv Video VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "audio_vae",
+ "required": false,
+ "label": "LTX 2.5 Audio VAE"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "fps": 24,
+ "video_frames": 33,
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 768,
+ "height": 512
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/mage-flow-edit.json b/examples/server/recipes/mage-flow-edit.json
new file mode 100644
index 0000000..82d66f6
--- /dev/null
+++ b/examples/server/recipes/mage-flow-edit.json
@@ -0,0 +1,39 @@
+{
+ "schema_version": 1,
+ "id": "mage-flow-edit",
+ "name": "Mage Flow Edit",
+ "description": "Supply reference images. Select a separate vision projector when it is not bundled in the text encoder.",
+ "model_url": "https://huggingface.co/microsoft/Mage-Flow-Edit",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": true
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/mage-flow-turbo.json b/examples/server/recipes/mage-flow-turbo.json
new file mode 100644
index 0000000..5cf9bb2
--- /dev/null
+++ b/examples/server/recipes/mage-flow-turbo.json
@@ -0,0 +1,31 @@
+{
+ "schema_version": 1,
+ "id": "mage-flow-turbo",
+ "name": "Mage Flow Turbo",
+ "model_url": "https://huggingface.co/microsoft/Mage-Flow-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/mage-flow.json b/examples/server/recipes/mage-flow.json
new file mode 100644
index 0000000..4cbd159
--- /dev/null
+++ b/examples/server/recipes/mage-flow.json
@@ -0,0 +1,31 @@
+{
+ "schema_version": 1,
+ "id": "mage-flow",
+ "name": "Mage Flow",
+ "model_url": "https://huggingface.co/microsoft/Mage-Flow",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/ming-image.json b/examples/server/recipes/ming-image.json
new file mode 100644
index 0000000..6c368e8
--- /dev/null
+++ b/examples/server/recipes/ming-image.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "ming-image",
+ "name": "Ming Image 0.1",
+ "model_url": "https://huggingface.co/inclusionAI/Ming-Image-0.1-Design",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Ling Mini 2.0 Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Ling tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 12,
+ "width": 2048,
+ "height": 2048,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/minimax-h3-fl2va.json b/examples/server/recipes/minimax-h3-fl2va.json
new file mode 100644
index 0000000..3971923
--- /dev/null
+++ b/examples/server/recipes/minimax-h3-fl2va.json
@@ -0,0 +1,55 @@
+{
+ "schema_version": 1,
+ "id": "minimax-h3-fl2va",
+ "name": "MiniMax H3 FL2VA",
+ "description": "Generate video and audio, optionally with start/end images. Supports full and pruned FL2VA weights. Audio VAE is optional for silent video.",
+ "model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "MiniMax-H3 Qwen 3 VL 32B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "MiniMax H3 Video VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "audio_vae",
+ "required": false,
+ "label": "MiniMax H3 Audio VAE"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "video_frames": 56,
+ "fps": 24,
+ "cfg_scale": 1,
+ "width": 1344,
+ "height": 768,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/minimax-h3-ref2va.json b/examples/server/recipes/minimax-h3-ref2va.json
new file mode 100644
index 0000000..085f5d5
--- /dev/null
+++ b/examples/server/recipes/minimax-h3-ref2va.json
@@ -0,0 +1,54 @@
+{
+ "schema_version": 1,
+ "id": "minimax-h3-ref2va",
+ "name": "MiniMax H3 Ref2VA",
+ "description": "Supply image, video or audio references with compatible Ref2VA weights. Audio VAE is optional for silent video.",
+ "model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "MiniMax-H3 Qwen 3 VL 32B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "MiniMax H3 Video VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "audio_vae",
+ "required": false,
+ "label": "MiniMax H3 Audio VAE"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "video_frames": 56,
+ "fps": 24,
+ "cfg_scale": 1,
+ "width": 1344,
+ "height": 768,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/minit2i.json b/examples/server/recipes/minit2i.json
new file mode 100644
index 0000000..2c7b94e
--- /dev/null
+++ b/examples/server/recipes/minit2i.json
@@ -0,0 +1,29 @@
+{
+ "schema_version": 1,
+ "id": "minit2i",
+ "name": "MiniT2I",
+ "description": "Pixel-space model; no VAE is required.",
+ "model_url": "https://huggingface.co/MiniT2I/MiniT2I",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "FLAN-T5 Large"
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 2.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512,
+ "sampling_method": "euler"
+ },
+ "disabled_features": [
+ "preview",
+ "vae_tiling"
+ ]
+}
diff --git a/examples/server/recipes/moss-image-base.json b/examples/server/recipes/moss-image-base.json
new file mode 100644
index 0000000..bc2cc7d
--- /dev/null
+++ b/examples/server/recipes/moss-image-base.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "moss-image-base",
+ "name": "MOSS Image Base",
+ "description": "Select the MOSS-Image-Base transformer with its matching text encoder and FLUX.2 VAE or TAE.",
+ "model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3.5 9B Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Qwen 3.5 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "flow_shift": 5,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/moss-image-sft.json b/examples/server/recipes/moss-image-sft.json
new file mode 100644
index 0000000..e0239b0
--- /dev/null
+++ b/examples/server/recipes/moss-image-sft.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "moss-image-sft",
+ "name": "MOSS Image SFT",
+ "description": "Select the MOSS-Image-SFT transformer with its matching text encoder and FLUX.2 VAE or TAE.",
+ "model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3.5 9B Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Qwen 3.5 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "flow_shift": 5,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/nitrosd-realism.json b/examples/server/recipes/nitrosd-realism.json
new file mode 100644
index 0000000..75f2d43
--- /dev/null
+++ b/examples/server/recipes/nitrosd-realism.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "nitrosd-realism",
+ "name": "NitroSD Realism",
+ "model_url": "https://huggingface.co/ChenDY/NitroFusion",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "scheduler": "sgm_uniform",
+ "shifted_timestep": 250,
+ "cfg_scale": 1,
+ "steps": 1,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/ovis-image.json b/examples/server/recipes/ovis-image.json
new file mode 100644
index 0000000..21e9d9f
--- /dev/null
+++ b/examples/server/recipes/ovis-image.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "ovis-image",
+ "name": "Ovis Image",
+ "model_url": "https://huggingface.co/ATH-MaaS/Ovis-Image-7B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Ovis 2.5 Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/pid-15-flux2.json b/examples/server/recipes/pid-15-flux2.json
new file mode 100644
index 0000000..4fb3db3
--- /dev/null
+++ b/examples/server/recipes/pid-15-flux2.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-15-flux2",
+ "name": "PiD 1.5 (Flux.2 VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "flux2"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-15-qwen-image.json b/examples/server/recipes/pid-15-qwen-image.json
new file mode 100644
index 0000000..9db9e74
--- /dev/null
+++ b/examples/server/recipes/pid-15-qwen-image.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-15-qwen-image",
+ "name": "PiD 1.5 (Qwen Image VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "wan"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-15-sd3.json b/examples/server/recipes/pid-15-sd3.json
new file mode 100644
index 0000000..3980b86
--- /dev/null
+++ b/examples/server/recipes/pid-15-sd3.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-15-sd3",
+ "name": "PiD 1.5 (SD3 VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "sd3"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-15.json b/examples/server/recipes/pid-15.json
new file mode 100644
index 0000000..f7913be
--- /dev/null
+++ b/examples/server/recipes/pid-15.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-15",
+ "name": "PiD 1.5 (Flux / Z-Image VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "flux"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-flux2.json b/examples/server/recipes/pid-flux2.json
new file mode 100644
index 0000000..9c802dc
--- /dev/null
+++ b/examples/server/recipes/pid-flux2.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-flux2",
+ "name": "PiD (Flux.2 VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "flux2"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-qwen-image.json b/examples/server/recipes/pid-qwen-image.json
new file mode 100644
index 0000000..4503b1c
--- /dev/null
+++ b/examples/server/recipes/pid-qwen-image.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-qwen-image",
+ "name": "PiD (Qwen Image VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "wan"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid-sd3.json b/examples/server/recipes/pid-sd3.json
new file mode 100644
index 0000000..6dad2ca
--- /dev/null
+++ b/examples/server/recipes/pid-sd3.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid-sd3",
+ "name": "PiD (SD3 VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "sd3"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pid.json b/examples/server/recipes/pid.json
new file mode 100644
index 0000000..2479449
--- /dev/null
+++ b/examples/server/recipes/pid.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "id": "pid",
+ "name": "PiD (Flux / Z-Image VAE)",
+ "description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.",
+ "model_url": "https://huggingface.co/nvidia/PiD",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Gemma 2 2B ELM Text Encoder"
+ },
+ {
+ "id": "tokenizer",
+ "required": true,
+ "label": "Gemma 2 tokenizer.json"
+ },
+ {
+ "id": "vae",
+ "required": true
+ }
+ ],
+ "context": {
+ "vae_format": "flux"
+ },
+ "defaults": {
+ "sampling_method": "lcm",
+ "scheduler": "lcm",
+ "cfg_scale": 1,
+ "steps": 4,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/pixart-alpha-1024.json b/examples/server/recipes/pixart-alpha-1024.json
new file mode 100644
index 0000000..1144f87
--- /dev/null
+++ b/examples/server/recipes/pixart-alpha-1024.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "pixart-alpha-1024",
+ "name": "PixArt Alpha XL 1024",
+ "description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.",
+ "model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "context": {
+ "model_args": "pixart_vae_scale_factor=0.18215"
+ },
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler",
+ "cfg_scale": 4.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ }
+}
diff --git a/examples/server/recipes/pixart-alpha-512.json b/examples/server/recipes/pixart-alpha-512.json
new file mode 100644
index 0000000..804a7a0
--- /dev/null
+++ b/examples/server/recipes/pixart-alpha-512.json
@@ -0,0 +1,42 @@
+{
+ "schema_version": 1,
+ "id": "pixart-alpha-512",
+ "name": "PixArt Alpha XL 512",
+ "description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.",
+ "model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-512x512",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "context": {
+ "model_args": "pixart_vae_scale_factor=0.18215,pixart_pos_embed_base_size=32,pixart_interpolation_scale=1"
+ },
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler",
+ "cfg_scale": 4.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ }
+}
diff --git a/examples/server/recipes/pixart-sigma.json b/examples/server/recipes/pixart-sigma.json
new file mode 100644
index 0000000..ed4d2f0
--- /dev/null
+++ b/examples/server/recipes/pixart-sigma.json
@@ -0,0 +1,37 @@
+{
+ "schema_version": 1,
+ "id": "pixart-sigma",
+ "name": "PixArt Sigma XL 1024",
+ "model_url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024,
+ "sampling_method": "euler"
+ }
+}
diff --git a/examples/server/recipes/qwen-image-21-turbo.json b/examples/server/recipes/qwen-image-21-turbo.json
new file mode 100644
index 0000000..2ef9fa1
--- /dev/null
+++ b/examples/server/recipes/qwen-image-21-turbo.json
@@ -0,0 +1,60 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-21-turbo",
+ "name": "Qwen Image 2.1 Turbo",
+ "description": "Use the standalone Turbo diffusion weights for generation and editing with the fixed 8-step schedule. Select the Qwen Image 2.1 VAE or TAEQI2.1, and supply a vision projector when needed.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 8B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false,
+ "label": "TAEQI2.1"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 8,
+ "width": 2048,
+ "height": 2048,
+ "custom_sigmas": [
+ 1.0,
+ 0.978453,
+ 0.95418,
+ 0.926626,
+ 0.89508,
+ 0.845148,
+ 0.704534,
+ 0.414568,
+ 0.0
+ ]
+ },
+ "features": [
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image-21.json b/examples/server/recipes/qwen-image-21.json
new file mode 100644
index 0000000..4b6c67b
--- /dev/null
+++ b/examples/server/recipes/qwen-image-21.json
@@ -0,0 +1,50 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-21",
+ "name": "Qwen Image 2.1",
+ "description": "Supports generation and editing. Select the full Qwen Image 2.1 VAE or TAEQI2.1. Supply a separate vision projector when needed. Select acceleration LoRAs and adjust their settings separately.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 8B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false,
+ "label": "TAEQI2.1"
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 20,
+ "width": 2048,
+ "height": 2048
+ },
+ "features": [
+ "flow_shift",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image-edit-2509.json b/examples/server/recipes/qwen-image-edit-2509.json
new file mode 100644
index 0000000..4c647a0
--- /dev/null
+++ b/examples/server/recipes/qwen-image-edit-2509.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-edit-2509",
+ "name": "Qwen Image Edit 2509",
+ "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit-2509",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image-edit-2511.json b/examples/server/recipes/qwen-image-edit-2511.json
new file mode 100644
index 0000000..cdd16db
--- /dev/null
+++ b/examples/server/recipes/qwen-image-edit-2511.json
@@ -0,0 +1,52 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-edit-2511",
+ "name": "Qwen Image Edit 2511",
+ "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit-2511",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "context": {
+ "model_args": "qwen_image_zero_cond_t=true"
+ },
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image-edit.json b/examples/server/recipes/qwen-image-edit.json
new file mode 100644
index 0000000..5610a6c
--- /dev/null
+++ b/examples/server/recipes/qwen-image-edit.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-edit",
+ "name": "Qwen Image Edit",
+ "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "llm_vision",
+ "required": false,
+ "label": "Vision projector (required for separate GGUF text encoders)"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image-layered.json b/examples/server/recipes/qwen-image-layered.json
new file mode 100644
index 0000000..96863be
--- /dev/null
+++ b/examples/server/recipes/qwen-image-layered.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image-layered",
+ "name": "Qwen Image Layered",
+ "description": "Supply a reference image and use the dedicated layered VAE.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image-Layered",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": true,
+ "label": "Qwen Image Layered VAE"
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 640,
+ "height": 640
+ },
+ "features": [
+ "flow_shift",
+ "img_cfg",
+ "ref_images"
+ ]
+}
diff --git a/examples/server/recipes/qwen-image.json b/examples/server/recipes/qwen-image.json
new file mode 100644
index 0000000..417a621
--- /dev/null
+++ b/examples/server/recipes/qwen-image.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "id": "qwen-image",
+ "name": "Qwen Image",
+ "description": "Supports text-to-image and image-to-image; pruned transformer variants use the same components.",
+ "model_url": "https://huggingface.co/Qwen/Qwen-Image",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 2.5 VL 7B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Qwen Image VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 1328,
+ "height": 1328
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/sd-turbo.json b/examples/server/recipes/sd-turbo.json
new file mode 100644
index 0000000..7723c81
--- /dev/null
+++ b/examples/server/recipes/sd-turbo.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "sd-turbo",
+ "name": "SD Turbo",
+ "description": "Select a single-file SD-Turbo checkpoint. Uses one step with CFG disabled (scale 1 in sd.cpp); use more steps for partial image-to-image denoising.",
+ "model_url": "https://huggingface.co/stabilityai/sd-turbo",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "scheduler": "discrete",
+ "cfg_scale": 1,
+ "steps": 1,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd1-checkpoint.json b/examples/server/recipes/sd1-checkpoint.json
new file mode 100644
index 0000000..38d2386
--- /dev/null
+++ b/examples/server/recipes/sd1-checkpoint.json
@@ -0,0 +1,35 @@
+{
+ "schema_version": 1,
+ "id": "sd1-checkpoint",
+ "name": "SD 1.x",
+ "description": "Supports SD 1.x, compatible LCM checkpoints and layer-count variants. For LCM weights, select LCM sampling and adjust steps/CFG. Tiny SD Diffusers directories must first be converted into a single-file checkpoint.",
+ "model_url": "https://huggingface.co/CompVis/stable-diffusion-v1-4",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "VAE override",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd1-controlnet.json b/examples/server/recipes/sd1-controlnet.json
new file mode 100644
index 0000000..eeaee22
--- /dev/null
+++ b/examples/server/recipes/sd1-controlnet.json
@@ -0,0 +1,38 @@
+{
+ "schema_version": 1,
+ "id": "sd1-controlnet",
+ "name": "SD 1.5 ControlNet",
+ "description": "Supply a control image; input image and mask are optional for image-to-image or inpainting.",
+ "model_url": "https://huggingface.co/lllyasviel/control_v11p_sd15_canny",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "control_net",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd1-inpainting.json b/examples/server/recipes/sd1-inpainting.json
new file mode 100644
index 0000000..f9f3064
--- /dev/null
+++ b/examples/server/recipes/sd1-inpainting.json
@@ -0,0 +1,34 @@
+{
+ "schema_version": 1,
+ "id": "sd1-inpainting",
+ "name": "SD 1.5 Inpainting",
+ "description": "Select an inpainting checkpoint, then supply an input image and mask.",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "img_cfg"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd1-ip-adapter.json b/examples/server/recipes/sd1-ip-adapter.json
new file mode 100644
index 0000000..6c0ab1a
--- /dev/null
+++ b/examples/server/recipes/sd1-ip-adapter.json
@@ -0,0 +1,43 @@
+{
+ "schema_version": 1,
+ "id": "sd1-ip-adapter",
+ "name": "SD 1.5 IP-Adapter / Plus",
+ "description": "Select matching SD 1.5 IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.",
+ "model_url": "https://huggingface.co/h94/IP-Adapter",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "clip_vision",
+ "required": true,
+ "label": "CLIP Vision H"
+ },
+ {
+ "id": "ip_adapter",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd2-512-checkpoint.json b/examples/server/recipes/sd2-512-checkpoint.json
new file mode 100644
index 0000000..77838c2
--- /dev/null
+++ b/examples/server/recipes/sd2-512-checkpoint.json
@@ -0,0 +1,33 @@
+{
+ "schema_version": 1,
+ "id": "sd2-512-checkpoint",
+ "name": "SD 2.1 512",
+ "description": "Supports SD 2.x 512 and compatible layer-count variants. Tiny SD2 Diffusers directories must first be converted into a single-file checkpoint.",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd2-768-checkpoint.json b/examples/server/recipes/sd2-768-checkpoint.json
new file mode 100644
index 0000000..808b187
--- /dev/null
+++ b/examples/server/recipes/sd2-768-checkpoint.json
@@ -0,0 +1,32 @@
+{
+ "schema_version": 1,
+ "id": "sd2-768-checkpoint",
+ "name": "SD 2.1 768",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 768,
+ "height": 768
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd2-inpainting.json b/examples/server/recipes/sd2-inpainting.json
new file mode 100644
index 0000000..4980f7f
--- /dev/null
+++ b/examples/server/recipes/sd2-inpainting.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "sd2-inpainting",
+ "name": "SD 2 Inpainting",
+ "description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler_a",
+ "cfg_scale": 7.5,
+ "steps": 20,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "img_cfg"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sd3-medium.json b/examples/server/recipes/sd3-medium.json
new file mode 100644
index 0000000..7c0e8fb
--- /dev/null
+++ b/examples/server/recipes/sd3-medium.json
@@ -0,0 +1,33 @@
+{
+ "schema_version": 1,
+ "id": "sd3-medium",
+ "name": "SD 3 Medium",
+ "description": "Checkpoint with CLIP-L, CLIP-G and T5 included.",
+ "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3-medium-diffusers",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 7,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "clip_skip",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/sd35-large.json b/examples/server/recipes/sd35-large.json
new file mode 100644
index 0000000..a231db9
--- /dev/null
+++ b/examples/server/recipes/sd35-large.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "id": "sd35-large",
+ "name": "SD 3.5 Large",
+ "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-large",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "clip_g",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 3.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "clip_skip",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/sd35-medium.json b/examples/server/recipes/sd35-medium.json
new file mode 100644
index 0000000..7508025
--- /dev/null
+++ b/examples/server/recipes/sd35-medium.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "id": "sd35-medium",
+ "name": "SD 3.5 Medium",
+ "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-medium",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "clip_l",
+ "required": true
+ },
+ {
+ "id": "clip_g",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 4.5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "clip_skip",
+ "flow_shift",
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-checkpoint.json b/examples/server/recipes/sdxl-checkpoint.json
new file mode 100644
index 0000000..372d5a6
--- /dev/null
+++ b/examples/server/recipes/sdxl-checkpoint.json
@@ -0,0 +1,36 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-checkpoint",
+ "name": "SDXL",
+ "description": "Supports SDXL and compatible reduced-depth checkpoints such as SSD-1B and Segmind Vega.",
+ "model_url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "VAE override",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-controlnet.json b/examples/server/recipes/sdxl-controlnet.json
new file mode 100644
index 0000000..c50efbd
--- /dev/null
+++ b/examples/server/recipes/sdxl-controlnet.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-controlnet",
+ "name": "SDXL ControlNet",
+ "description": "Select a compatible SDXL ControlNet and supply a preprocessed control image. Input image and mask are optional for image-to-image or inpainting.",
+ "model_url": "https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "control_net",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler_a",
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-inpainting.json b/examples/server/recipes/sdxl-inpainting.json
new file mode 100644
index 0000000..574423d
--- /dev/null
+++ b/examples/server/recipes/sdxl-inpainting.json
@@ -0,0 +1,39 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-inpainting",
+ "name": "SDXL Inpainting",
+ "description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.",
+ "model_url": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "VAE override",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "euler_a",
+ "cfg_scale": 8,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep",
+ "img_cfg"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-ip-adapter.json b/examples/server/recipes/sdxl-ip-adapter.json
new file mode 100644
index 0000000..b649fb1
--- /dev/null
+++ b/examples/server/recipes/sdxl-ip-adapter.json
@@ -0,0 +1,46 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-ip-adapter",
+ "name": "SDXL IP-Adapter / Plus",
+ "description": "Select matching SDXL ViT-H IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.",
+ "model_url": "https://huggingface.co/h94/IP-Adapter",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "clip_vision",
+ "required": true,
+ "label": "CLIP Vision H"
+ },
+ {
+ "id": "ip_adapter",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "scheduler": "discrete",
+ "sampling_method": "dpm++2m",
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-photomaker.json b/examples/server/recipes/sdxl-photomaker.json
new file mode 100644
index 0000000..025fb76
--- /dev/null
+++ b/examples/server/recipes/sdxl-photomaker.json
@@ -0,0 +1,37 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-photomaker",
+ "name": "SDXL PhotoMaker",
+ "description": "Select PhotoMaker v1 and supply identity images. Include the img trigger in the prompt.",
+ "model_url": "https://huggingface.co/TencentARC/PhotoMaker",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "photo_maker",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxl-turbo.json b/examples/server/recipes/sdxl-turbo.json
new file mode 100644
index 0000000..8650e6c
--- /dev/null
+++ b/examples/server/recipes/sdxl-turbo.json
@@ -0,0 +1,34 @@
+{
+ "schema_version": 1,
+ "id": "sdxl-turbo",
+ "name": "SDXL Turbo",
+ "model_url": "https://huggingface.co/stabilityai/sdxl-turbo",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 1,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip",
+ "shifted_timestep"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sdxs.json b/examples/server/recipes/sdxs.json
new file mode 100644
index 0000000..19e218a
--- /dev/null
+++ b/examples/server/recipes/sdxs.json
@@ -0,0 +1,29 @@
+{
+ "schema_version": 1,
+ "id": "sdxs",
+ "name": "SDXS",
+ "model_url": "https://huggingface.co/IDKiro/sdxs-512-dreamshaper",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "required": false
+ }
+ ],
+ "defaults": {
+ "cfg_scale": 1,
+ "steps": 1,
+ "width": 512,
+ "height": 512
+ },
+ "features": [
+ "cache_unet",
+ "clip_skip"
+ ],
+ "disabled_features": [
+ "cache_dit"
+ ]
+}
diff --git a/examples/server/recipes/sefi-image.json b/examples/server/recipes/sefi-image.json
new file mode 100644
index 0000000..35e9333
--- /dev/null
+++ b/examples/server/recipes/sefi-image.json
@@ -0,0 +1,39 @@
+{
+ "schema_version": 1,
+ "id": "sefi-image",
+ "name": "SeFi Image 5B RL",
+ "model_url": "https://huggingface.co/SeFi-Image/SeFi-Image-5B-Base",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 VL 4B Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "slg"
+ ]
+}
diff --git a/examples/server/recipes/sensenova-u15.json b/examples/server/recipes/sensenova-u15.json
new file mode 100644
index 0000000..afd2046
--- /dev/null
+++ b/examples/server/recipes/sensenova-u15.json
@@ -0,0 +1,24 @@
+{
+ "schema_version": 1,
+ "id": "sensenova-u15",
+ "name": "SenseNova U1.5",
+ "description": "Bundled MoT checkpoint.",
+ "model_url": "https://huggingface.co/sensenova/SenseNova-U1.5-8B-MoT",
+ "slots": [
+ {
+ "id": "model",
+ "required": true
+ }
+ ],
+ "defaults": {
+ "flow_shift": 3.0,
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 2048,
+ "height": 2048,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/wan21-flf2v.json b/examples/server/recipes/wan21-flf2v.json
new file mode 100644
index 0000000..02f0e80
--- /dev/null
+++ b/examples/server/recipes/wan21-flf2v.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": 1,
+ "id": "wan21-flf2v",
+ "name": "Wan 2.1 FLF2V 14B",
+ "description": "Select FLF2V weights and supply both start and end images.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.1-FLF2V-14B-720P",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "UMT5-XXL"
+ },
+ {
+ "id": "clip_vision",
+ "required": true,
+ "label": "CLIP Vision H"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 16,
+ "video_frames": 21,
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1280,
+ "height": 720
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan21-i2v.json b/examples/server/recipes/wan21-i2v.json
new file mode 100644
index 0000000..d8d8d13
--- /dev/null
+++ b/examples/server/recipes/wan21-i2v.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": 1,
+ "id": "wan21-i2v",
+ "name": "Wan 2.1 I2V",
+ "description": "Image-to-video with Wan 2.1, compatible SkyReels V2 weights, or a single Wan 2.2 I2V expert. CLIP Vision is required by Wan 2.1 I2V and is unused by Wan 2.2. Defaults target Wan 2.1 480p.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.1-I2V-14B-480P",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "label": "UMT5-XXL",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "Wan 2.1 VAE",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ },
+ {
+ "id": "clip_vision",
+ "label": "CLIP Vision H (Wan 2.1 I2V)",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "fps": 16,
+ "flow_shift": 3,
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 832,
+ "height": 480
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan21-t2v.json b/examples/server/recipes/wan21-t2v.json
new file mode 100644
index 0000000..367611c
--- /dev/null
+++ b/examples/server/recipes/wan21-t2v.json
@@ -0,0 +1,50 @@
+{
+ "schema_version": 1,
+ "id": "wan21-t2v",
+ "name": "Wan 2.1 T2V",
+ "description": "Text-to-video with Wan 2.1 or a compatible single Wan 2.2 T2V expert. Defaults target Wan 2.1 14B; adjust settings for the selected weights and acceleration LoRAs.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-14B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "label": "UMT5-XXL",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "Wan 2.1 VAE",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "fps": 16,
+ "flow_shift": 5,
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1280,
+ "height": 720
+ },
+ "features": [
+ "flow_shift"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan21-vace.json b/examples/server/recipes/wan21-vace.json
new file mode 100644
index 0000000..7b7f7e4
--- /dev/null
+++ b/examples/server/recipes/wan21-vace.json
@@ -0,0 +1,52 @@
+{
+ "schema_version": 1,
+ "id": "wan21-vace",
+ "name": "Wan 2.1 VACE 1.3B / 14B",
+ "description": "Select matching VACE weights. Supports text, reference images and control video through Video Generation.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.1-VACE-14B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "UMT5-XXL"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 832,
+ "height": 480,
+ "flow_shift": 16
+ },
+ "features": [
+ "control_frames",
+ "flow_shift",
+ "init_image",
+ "vace"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan22-i2v.json b/examples/server/recipes/wan22-i2v.json
new file mode 100644
index 0000000..62afdfc
--- /dev/null
+++ b/examples/server/recipes/wan22-i2v.json
@@ -0,0 +1,62 @@
+{
+ "schema_version": 1,
+ "id": "wan22-i2v",
+ "name": "Wan 2.2 I2V A14B",
+ "description": "Image-to-video. Select a start image in Video Generation after loading. LightX2V acceleration LoRAs must match the I2V low- and high-noise models; configure LoRAs and sampling separately.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "label": "Low-noise diffusion model",
+ "required": true
+ },
+ {
+ "id": "high_noise_diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "label": "UMT5-XXL",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "Wan 2.1 VAE",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "fps": 16,
+ "flow_shift": 5,
+ "high_noise_steps": -1,
+ "high_noise_cfg_scale": 3.5,
+ "high_noise_sampling_method": "euler",
+ "high_noise_flow_shift": 5,
+ "cfg_scale": 3.5,
+ "steps": 20,
+ "width": 1280,
+ "height": 720,
+ "moe_boundary": 0.9
+ },
+ "features": [
+ "end_image",
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan22-s2v.json b/examples/server/recipes/wan22-s2v.json
new file mode 100644
index 0000000..ba1ee4f
--- /dev/null
+++ b/examples/server/recipes/wan22-s2v.json
@@ -0,0 +1,53 @@
+{
+ "schema_version": 1,
+ "id": "wan22-s2v",
+ "name": "Wan 2.2 S2V 14B",
+ "description": "Supply a start image and reference audio through Video Generation.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.2-S2V-14B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "UMT5-XXL"
+ },
+ {
+ "id": "audio_encoder",
+ "required": true,
+ "label": "Wav2Vec2 Large English"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.1 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "flow_shift": 3,
+ "video_frames": 33,
+ "cfg_scale": 4.5,
+ "steps": 20
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan22-t2v.json b/examples/server/recipes/wan22-t2v.json
new file mode 100644
index 0000000..905bcfa
--- /dev/null
+++ b/examples/server/recipes/wan22-t2v.json
@@ -0,0 +1,60 @@
+{
+ "schema_version": 1,
+ "id": "wan22-t2v",
+ "name": "Wan 2.2 T2V A14B",
+ "description": "Text-to-video. Use Video Generation after loading. LightX2V acceleration LoRAs must match the T2V low- and high-noise models; configure LoRAs and sampling separately.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "label": "Low-noise diffusion model",
+ "required": true
+ },
+ {
+ "id": "high_noise_diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "label": "UMT5-XXL",
+ "required": true
+ },
+ {
+ "id": "vae",
+ "label": "Wan 2.1 VAE",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "fps": 16,
+ "flow_shift": 12,
+ "high_noise_steps": -1,
+ "high_noise_cfg_scale": 4,
+ "high_noise_sampling_method": "euler",
+ "high_noise_flow_shift": 12,
+ "cfg_scale": 3,
+ "steps": 20,
+ "width": 1280,
+ "height": 720,
+ "moe_boundary": 0.875
+ },
+ "features": [
+ "flow_shift"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/wan22-ti2v-5b.json b/examples/server/recipes/wan22-ti2v-5b.json
new file mode 100644
index 0000000..3e2aa35
--- /dev/null
+++ b/examples/server/recipes/wan22-ti2v-5b.json
@@ -0,0 +1,50 @@
+{
+ "schema_version": 1,
+ "id": "wan22-ti2v-5b",
+ "name": "Wan 2.2 TI2V 5B",
+ "description": "Use Wan 2.2 VAE. Supports text, start-image and start/end-image conditioning.",
+ "model_url": "https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "t5xxl",
+ "required": true,
+ "label": "UMT5-XXL"
+ },
+ {
+ "id": "vae",
+ "required": false,
+ "label": "Wan 2.2 VAE"
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "video_frames": 33,
+ "cfg_scale": 5,
+ "steps": 20,
+ "width": 1280,
+ "height": 704,
+ "flow_shift": 5
+ },
+ "features": [
+ "flow_shift",
+ "init_image"
+ ],
+ "disabled_features": [
+ "img2img",
+ "mask_image"
+ ]
+}
diff --git a/examples/server/recipes/z-image-l2p.json b/examples/server/recipes/z-image-l2p.json
new file mode 100644
index 0000000..0375f13
--- /dev/null
+++ b/examples/server/recipes/z-image-l2p.json
@@ -0,0 +1,29 @@
+{
+ "schema_version": 1,
+ "id": "z-image-l2p",
+ "name": "Z-Image L2P",
+ "description": "Pixel-space L2P checkpoint with Qwen 3 4B; no VAE is needed. Use dimensions divisible by 16.",
+ "model_url": "https://huggingface.co/zhen-nan/L2P",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 4B Text Encoder"
+ }
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "scheduler": "discrete"
+ },
+ "features": [
+ "flow_shift"
+ ],
+ "disabled_features": [
+ "vae_tiling",
+ "preview"
+ ]
+}
diff --git a/examples/server/recipes/z-image-turbo.json b/examples/server/recipes/z-image-turbo.json
new file mode 100644
index 0000000..9973767
--- /dev/null
+++ b/examples/server/recipes/z-image-turbo.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "z-image-turbo",
+ "name": "Z-Image Turbo",
+ "model_url": "https://huggingface.co/Tongyi-MAI/Z-Image-Turbo",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "sampling_method": "euler",
+ "cfg_scale": 1,
+ "steps": 9,
+ "width": 1024,
+ "height": 1024
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes/z-image.json b/examples/server/recipes/z-image.json
new file mode 100644
index 0000000..bcf2da7
--- /dev/null
+++ b/examples/server/recipes/z-image.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "z-image",
+ "name": "Z-Image",
+ "model_url": "https://huggingface.co/Tongyi-MAI/Z-Image",
+ "slots": [
+ {
+ "id": "diffusion_model",
+ "required": true
+ },
+ {
+ "id": "llm",
+ "required": true,
+ "label": "Qwen 3 Text Encoder"
+ },
+ {
+ "id": "vae",
+ "required": false
+ },
+ {
+ "id": "tae",
+ "required": false
+ }
+ ],
+ "required_any": [
+ [
+ "vae",
+ "tae"
+ ]
+ ],
+ "defaults": {
+ "cfg_scale": 4,
+ "steps": 20,
+ "width": 720,
+ "height": 1280,
+ "sampling_method": "euler"
+ },
+ "features": [
+ "flow_shift"
+ ]
+}
diff --git a/examples/server/recipes_builtin.h.in b/examples/server/recipes_builtin.h.in
new file mode 100644
index 0000000..ff31b4d
--- /dev/null
+++ b/examples/server/recipes_builtin.h.in
@@ -0,0 +1,7 @@
+#ifndef __SD_EXAMPLES_SERVER_RECIPES_BUILTIN_H__
+#define __SD_EXAMPLES_SERVER_RECIPES_BUILTIN_H__
+
+static const char* const k_builtin_recipes[] = {
+@SD_BUILTIN_RECIPES@};
+
+#endif
diff --git a/examples/server/routes.h b/examples/server/routes.h
index 1a4efb5..f779391 100644
--- a/examples/server/routes.h
+++ b/examples/server/routes.h
@@ -9,3 +9,18 @@ void register_index_endpoints(httplib::Server& svr, const SDSvrParams& svr_param
void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt);
void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt);
void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt);
+void register_model_endpoints(httplib::Server& svr, ServerRuntime& rt);
+
+class RuntimeRoutes {
+ httplib::Server& server_;
+ ServerRuntime& runtime_;
+
+ httplib::Server::Handler protect(httplib::Server::Handler handler);
+
+public:
+ RuntimeRoutes(httplib::Server& server, ServerRuntime& runtime)
+ : server_(server), runtime_(runtime) {}
+
+ void get(const std::string& pattern, httplib::Server::Handler handler);
+ void post(const std::string& pattern, httplib::Server::Handler handler);
+};
diff --git a/examples/server/routes_models.cpp b/examples/server/routes_models.cpp
new file mode 100644
index 0000000..ea14eb8
--- /dev/null
+++ b/examples/server/routes_models.cpp
@@ -0,0 +1,150 @@
+#include "routes.h"
+
+#include "async_jobs.h"
+#include "model_catalog.h"
+
+static void model_error(httplib::Response& res, int status, const std::string& code, const std::string& message) {
+ res.status = status;
+ res.set_content(json({{"error", message}, {"code", code}}).dump(), "application/json");
+}
+
+httplib::Server::Handler RuntimeRoutes::protect(httplib::Server::Handler handler) {
+ ServerRuntime* runtime = &runtime_;
+ return [runtime, handler](const httplib::Request& req, httplib::Response& res) {
+ std::shared_lock<std::shared_mutex> lock(runtime->models->runtime_mutex, std::try_to_lock);
+ if (!lock.owns_lock()) {
+ model_error(res, 503, "model_loading", "Model is loading or unloading");
+ return;
+ }
+ const bool generation = req.path == "/sdcpp/v1/img_gen" || req.path == "/sdcpp/v1/vid_gen" ||
+ req.path == "/sdapi/v1/txt2img" || req.path == "/sdapi/v1/img2img" ||
+ req.path == "/v1/images/generations" || req.path == "/v1/images/edits";
+ if (generation && !runtime->sd_ctx) {
+ model_error(res, 503, "model_not_loaded", "No model is loaded");
+ return;
+ }
+ handler(req, res);
+ };
+}
+
+void RuntimeRoutes::get(const std::string& pattern, httplib::Server::Handler handler) {
+ // Job polling and cancellation only access the job manager, even during a model transition.
+ server_.Get(pattern, pattern.find("/sdcpp/v1/jobs/") != std::string::npos ? handler : protect(handler));
+}
+
+void RuntimeRoutes::post(const std::string& pattern, httplib::Server::Handler handler) {
+ server_.Post(pattern, pattern.find("/sdcpp/v1/jobs/") != std::string::npos ? handler : protect(handler));
+}
+
+static void set_model_status(ServerRuntime& runtime, const char* state, const json& recipe, const json& selection, const json& error = nullptr) {
+ std::lock_guard<std::mutex> lock(runtime.models->status_mutex);
+ auto& models = *runtime.models;
+ const uint64_t revision = models.status.value("revision", uint64_t(0)) + 1;
+ json elapsed = nullptr;
+ if (!recipe.is_null()) {
+ const auto now = std::chrono::steady_clock::now();
+ if (std::string(state) == "loading") {
+ models.load_started_at = now;
+ elapsed = 0.0;
+ } else {
+ elapsed = std::chrono::duration<double>(now - models.load_started_at).count();
+ }
+ }
+ models.status = {{"state", state}, {"recipe", recipe}, {"models", selection}, {"error", error}, {"revision", revision}, {"load_elapsed_seconds", elapsed}};
+}
+
+static void change_model(ServerRuntime& runtime, const httplib::Request& req, httplib::Response& res, bool unload) {
+ auto& models = *runtime.models;
+ std::unique_lock<std::shared_mutex> lock(models.runtime_mutex, std::try_to_lock);
+ if (!lock.owns_lock()) {
+ model_error(res, 409, "model_busy", "Model is in use or another model operation is running");
+ return;
+ }
+ {
+ // Admission holds runtime_mutex through enqueue. Jobs stay pending until
+ // generation and metadata encoding finish, so no worker can retain this context here.
+ std::lock_guard<std::mutex> jobs_lock(runtime.async_job_manager->mutex);
+ if (count_pending_jobs(*runtime.async_job_manager) != 0) {
+ model_error(res, 409, "model_busy", "Finish or cancel pending generation jobs before switching models");
+ return;
+ }
+ }
+
+ json body;
+ json loaded_recipe;
+ SDContextParams context;
+ SDGenerationParams defaults;
+ if (!unload) {
+ try {
+ body = json::parse(req.body);
+ prepare_model_load(runtime, body, context, defaults, &loaded_recipe);
+ } catch (const std::exception& e) {
+ model_error(res, 400, "invalid_model_config", e.what());
+ return;
+ }
+ }
+
+ const json recipe = unload ? json(nullptr) : body.at("recipe");
+ const json selection = unload ? json::object() : body.at("models");
+ set_model_status(runtime, "loading", recipe, selection);
+ try {
+ runtime.sd_ctx = nullptr;
+ models.context.reset();
+ models.active_recipe = nullptr;
+ *runtime.ctx_params = unload ? models.base_context : std::move(context);
+ *runtime.default_gen_params = unload ? models.base_defaults : std::move(defaults);
+ if (!unload) {
+ auto params = runtime.ctx_params->to_sd_ctx_params_t(false);
+ models.context.reset(new_sd_ctx(¶ms));
+ if (!models.context) {
+ throw std::runtime_error("Model initialization failed; check the server log for component compatibility and memory errors");
+ }
+ runtime.sd_ctx = models.context.get();
+ models.active_recipe = std::move(loaded_recipe);
+ }
+ set_model_status(runtime, unload ? "empty" : "ready", recipe, selection);
+ res.set_content(model_status(runtime).dump(), "application/json");
+ } catch (const std::exception& e) {
+ runtime.sd_ctx = nullptr;
+ models.context.reset();
+ models.active_recipe = nullptr;
+ *runtime.ctx_params = models.base_context;
+ *runtime.default_gen_params = models.base_defaults;
+ set_model_status(runtime, "error", recipe, selection, e.what());
+ model_error(res, 422, "model_load_failed", e.what());
+ }
+}
+
+void register_model_endpoints(httplib::Server& svr, ServerRuntime& runtime) {
+ svr.Get("/sdcpp/v1/recipes", [&runtime](const httplib::Request&, httplib::Response& res) {
+ auto& models = *runtime.models;
+ std::lock_guard<std::mutex> lock(models.catalog_mutex);
+ json items = json::array();
+ for (const auto& entry : models.recipes)
+ items.push_back(entry.second);
+ res.set_content(json({{"recipes", items}, {"errors", models.catalog_errors}}).dump(), "application/json");
+ });
+ svr.Get("/sdcpp/v1/model-files", [&runtime](const httplib::Request&, httplib::Response& res) {
+ auto& models = *runtime.models;
+ std::lock_guard<std::mutex> lock(models.catalog_mutex);
+ json items = json::array();
+ for (const auto& entry : models.files) {
+ items.push_back({{"id", entry.first}, {"name", entry.second.name}, {"category", entry.second.category}, {"root", entry.second.root.id}, {"relative_path", entry.second.relative_path}});
+ }
+ res.set_content(json({{"files", items}, {"errors", models.catalog_errors}}).dump(), "application/json");
+ });
+ svr.Post("/sdcpp/v1/models/refresh", [&runtime](const httplib::Request&, httplib::Response& res) {
+ refresh_model_catalog(runtime);
+ std::lock_guard<std::mutex> lock(runtime.models->catalog_mutex);
+ res.set_content(json({{"errors", runtime.models->catalog_errors}}).dump(), "application/json");
+ });
+ svr.Get("/sdcpp/v1/models/status", [&runtime](const httplib::Request&, httplib::Response& res) {
+ res.set_content(model_status(runtime).dump(), "application/json");
+ });
+ svr.Post("/sdcpp/v1/models/load", [&runtime](const httplib::Request& req, httplib::Response& res) {
+ change_model(runtime, req, res, false);
+ });
+ svr.Post("/sdcpp/v1/models/unload", [&runtime](const httplib::Request& req, httplib::Response& res) {
+ change_model(runtime, req, res, true);
+ });
+}
diff --git a/examples/server/routes_openai.cpp b/examples/server/routes_openai.cpp
index 097b042..29dfe31 100644
--- a/examples/server/routes_openai.cpp
+++ b/examples/server/routes_openai.cpp
@@ -37,7 +37,7 @@ static bool build_openai_generation_request(const httplib::Request& req,
std::string output_format = j.value("output_format", "png");
int output_compression = j.value("output_compression", 100);
int width = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->width : 512;
- int height = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->height : 512;
+ int height = runtime.default_gen_params->height > 0 ? runtime.default_gen_params->height : 512;
if (!size.empty()) {
auto pos = size.find('x');
if (pos != std::string::npos) {
@@ -281,15 +281,18 @@ static bool execute_sync_img_gen_request(ServerRuntime& runtime,
void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
ServerRuntime* runtime = &rt;
+ RuntimeRoutes routes(svr, rt);
- svr.Get("/v1/models", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/v1/models", [runtime](const httplib::Request&, httplib::Response& res) {
json r;
r["data"] = json::array();
- r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}});
+ if (runtime->sd_ctx) {
+ r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}});
+ }
res.set_content(r.dump(), "application/json");
});
- svr.Post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
res.status = 400;
@@ -305,7 +308,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
return;
}
- LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str());
+ LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
SDImageVec results;
if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) {
@@ -363,7 +366,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
});
- svr.Post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
res.status = 400;
@@ -379,7 +382,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
return;
}
- LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str());
+ LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
SDImageVec results;
if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) {
diff --git a/examples/server/routes_sdapi.cpp b/examples/server/routes_sdapi.cpp
index 158b2af..4b8e7a5 100644
--- a/examples/server/routes_sdapi.cpp
+++ b/examples/server/routes_sdapi.cpp
@@ -10,6 +10,7 @@
#include "common/common.h"
#include "common/media_io.h"
#include "common/resource_owners.hpp"
+#include "model_catalog.h"
namespace fs = std::filesystem;
@@ -115,8 +116,9 @@ static bool build_sdapi_img_gen_request(const json& j,
request.gen_params.sample_params.sample_steps = steps;
request.gen_params.batch_count = batch_size;
request.gen_params.sample_params.guidance.txt_cfg = cfg_scale;
- request.gen_params.width = j.value("width", -1);
- request.gen_params.height = j.value("height", -1);
+ const bool recipe_dimensions = !img2img && model_status(runtime)["recipe"].is_string();
+ request.gen_params.width = j.value("width", recipe_dimensions ? runtime.default_gen_params->width : -1);
+ request.gen_params.height = j.value("height", recipe_dimensions ? runtime.default_gen_params->height : -1);
if (!img2img && j.value("enable_hr", false)) {
request.gen_params.hires_enabled = true;
@@ -151,6 +153,7 @@ static bool build_sdapi_img_gen_request(const json& j,
}
if (j.contains("lora") && j["lora"].is_array()) {
+ refresh_lora_cache(runtime);
request.gen_params.lora_map.clear();
request.gen_params.high_noise_lora_map.clear();
@@ -373,6 +376,7 @@ struct SDAPIPreviewGuard {
void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
ServerRuntime* runtime = &rt;
+ RuntimeRoutes routes(svr, rt);
auto sdapi_any2img = [runtime](const httplib::Request& req, httplib::Response& res, bool img2img) {
try {
@@ -398,7 +402,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
return;
}
- LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str());
+ LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
sd_img_gen_params_t img_gen_params = request.to_sd_img_gen_params_t();
SDImageVec results;
@@ -473,15 +477,15 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
};
- svr.Post("/sdapi/v1/txt2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/sdapi/v1/txt2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
sdapi_any2img(req, res, false);
});
- svr.Post("/sdapi/v1/img2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/sdapi/v1/img2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
sdapi_any2img(req, res, true);
});
- svr.Get("/sdapi/v1/loras", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/loras", [runtime](const httplib::Request&, httplib::Response& res) {
refresh_lora_cache(*runtime);
json result = json::array();
@@ -489,8 +493,9 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
std::lock_guard<std::mutex> lock(*runtime->lora_mutex);
for (const auto& e : *runtime->lora_cache) {
json item;
- item["name"] = e.name;
- item["path"] = e.path;
+ item["name"] = e.name;
+ item["path"] = e.path;
+ item["display_path"] = e.display_path;
result.push_back(item);
}
}
@@ -498,7 +503,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(result.dump(), "application/json");
});
- svr.Get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) {
refresh_upscaler_cache(*runtime);
auto make_builtin = [](const char* name) {
@@ -532,7 +537,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(result.dump(), "application/json");
});
- svr.Get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) {
json result = json::array({
{{"name", "Latent"}},
{{"name", "Latent (nearest)"}},
@@ -544,7 +549,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(result.dump(), "application/json");
});
- svr.Get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) {
std::vector<std::string> sampler_names;
sampler_names.push_back("default");
for (int i = 0; i < SAMPLE_METHOD_COUNT; i++) {
@@ -561,7 +566,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(r.dump(), "application/json");
});
- svr.Get("/sdapi/v1/schedulers", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/schedulers", [runtime](const httplib::Request&, httplib::Response& res) {
std::vector<std::string> scheduler_names;
scheduler_names.push_back("default");
for (int i = 0; i < SCHEDULER_COUNT; i++) {
@@ -580,7 +585,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(r.dump(), "application/json");
});
- svr.Get("/sdapi/v1/sd-models", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/sd-models", [runtime](const httplib::Request&, httplib::Response& res) {
fs::path model_path = resolve_display_model_path(*runtime);
json entry;
entry["title"] = model_path.stem().u8string();
@@ -590,11 +595,13 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
entry["sha256"] = "8888888888888888888888888888888888888888888888888888888888888888";
entry["config"] = nullptr;
json r = json::array();
- r.push_back(entry);
+ if (runtime->sd_ctx) {
+ r.push_back(entry);
+ }
res.set_content(r.dump(), "application/json");
});
- svr.Get("/sdapi/v1/options", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdapi/v1/options", [runtime](const httplib::Request&, httplib::Response& res) {
fs::path model_path = resolve_display_model_path(*runtime);
json r;
r["samples_format"] = "png";
@@ -602,7 +609,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(r.dump(), "application/json");
});
- svr.Get("/sdapi/v1/progress", [](const httplib::Request& req, httplib::Response& res) {
+ routes.get("/sdapi/v1/progress", [](const httplib::Request& req, httplib::Response& res) {
bool skip_current_image = false;
bool has_id_param = req.has_param("id_task");
std::string req_id_task = has_id_param ? req.get_param_value("id_task") : "";
diff --git a/examples/server/routes_sdcpp.cpp b/examples/server/routes_sdcpp.cpp
index a6c8a1e..7e93e5e 100644
--- a/examples/server/routes_sdcpp.cpp
+++ b/examples/server/routes_sdcpp.cpp
@@ -9,6 +9,7 @@
#include "common/common.h"
#include "common/media_io.h"
#include "common/resource_owners.hpp"
+#include "model_catalog.h"
namespace fs = std::filesystem;
@@ -98,12 +99,13 @@ static fs::path resolve_display_model_path(const ServerRuntime& runtime) {
return {};
}
-static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector<int>& skip_layers) {
+static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector<int>& skip_layers, const std::vector<float>& custom_sigmas = {}) {
const auto& guidance = sample_params.guidance;
return {
{"scheduler", capability_scheduler_name(sample_params.scheduler)},
{"sample_method", capability_sample_method_name(sample_params.sample_method)},
{"sample_steps", sample_params.sample_steps},
+ {"custom_sigmas", custom_sigmas},
{"eta", finite_number_or_null(sample_params.eta)},
{"shifted_timestep", sample_params.shifted_timestep},
{"flow_shift", finite_number_or_null(sample_params.flow_shift)},
@@ -153,7 +155,7 @@ static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const
{"increase_ref_index", defaults.increase_ref_index},
{"control_strength", defaults.control_strength},
{"ip_adapter_strength", defaults.ip_adapter_strength},
- {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
+ {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers, defaults.custom_sigmas)},
{"hires", make_hires_json(defaults)},
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
{"cache_mode", defaults.cache_mode},
@@ -179,7 +181,7 @@ static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const
{"fps", defaults.fps},
{"moe_boundary", defaults.moe_boundary},
{"vace_strength", defaults.vace_strength},
- {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
+ {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers, defaults.custom_sigmas)},
{"high_noise_sample_params", make_sample_params_json(defaults.high_noise_sample_params, defaults.high_noise_skip_layers)},
{"hires", make_hires_json(defaults)},
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
@@ -266,6 +268,7 @@ static json make_capabilities_json(ServerRuntime& runtime) {
available_loras.push_back({
{"name", entry.name},
{"path", entry.path},
+ {"display_path", entry.display_path},
});
}
}
@@ -344,18 +347,34 @@ static json make_capabilities_json(ServerRuntime& runtime) {
default_vid_output_format = video_output_formats[0].get<std::string>();
}
+ const json generation_features = model_generation_features(runtime);
+ auto recipe_features = [&](json features, bool video) {
+ features["init_image"] = generation_features.at(video ? "init_image" : "img2img");
+ for (const char* key : {"mask_image", "ref_images", "end_image", "control_frames", "lora", "vae_tiling", "cache", "preview"}) {
+ if (features.contains(key) || std::string(key) == "ref_images")
+ features[key] = generation_features.at(key);
+ }
+ if (video)
+ features["high_noise_sample_params"] = generation_features.at("high_noise");
+ else {
+ features["mask_image"] = generation_features.at("mask_image").get<bool>() && generation_features.at("img2img").get<bool>();
+ features["control_image"] = generation_features.at("controlnet").get<bool>() || generation_features.at("control_image").get<bool>();
+ features["ip_adapter_image"] = generation_features.at("ip_adapter");
+ }
+ return features;
+ };
json defaults_by_mode = json::object();
json output_formats_by_mode = json::object();
json features_by_mode = json::object();
if (supports_img) {
defaults_by_mode["img_gen"] = make_img_gen_defaults_json(defaults, default_img_output_format);
output_formats_by_mode["img_gen"] = image_output_formats;
- features_by_mode["img_gen"] = make_img_gen_features_json();
+ features_by_mode["img_gen"] = recipe_features(make_img_gen_features_json(), false);
}
if (supports_vid) {
defaults_by_mode["vid_gen"] = make_vid_gen_defaults_json(defaults, default_vid_output_format);
output_formats_by_mode["vid_gen"] = video_output_formats;
- features_by_mode["vid_gen"] = make_vid_gen_features_json();
+ features_by_mode["vid_gen"] = recipe_features(make_vid_gen_features_json(), true);
}
json top_level_defaults = json::object();
@@ -383,19 +402,21 @@ static json make_capabilities_json(ServerRuntime& runtime) {
{"stem", model_path.stem().u8string()},
{"path", model_path.u8string()},
};
- result["current_mode"] = current_mode;
- result["supported_modes"] = supported_modes;
- result["defaults"] = top_level_defaults;
- result["defaults_by_mode"] = defaults_by_mode;
- result["limits"] = {
- {"min_width", 64},
- {"max_width", 4096},
- {"min_height", 64},
- {"max_height", 4096},
- {"max_batch_count", 8},
- {"max_queue_size", manager.max_pending_jobs},
- {"max_upscale_width", k_max_upscale_dimension},
- {"max_upscale_height", k_max_upscale_dimension},
+ result["generation_features"] = generation_features;
+ result["runtime"] = model_status(runtime);
+ result["current_mode"] = current_mode;
+ result["supported_modes"] = supported_modes;
+ result["defaults"] = top_level_defaults;
+ result["defaults_by_mode"] = defaults_by_mode;
+ result["limits"] = {
+ {"min_width", 64},
+ {"max_width", 4096},
+ {"min_height", 64},
+ {"max_height", 4096},
+ {"max_batch_count", 8},
+ {"max_queue_size", manager.max_pending_jobs},
+ {"max_upscale_width", k_max_upscale_dimension},
+ {"max_upscale_height", k_max_upscale_dimension},
};
result["samplers"] = samplers;
result["schedulers"] = schedulers;
@@ -470,13 +491,14 @@ static bool parse_vid_gen_request(const json& body,
void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
ServerRuntime* runtime = &rt;
+ RuntimeRoutes routes(svr, rt);
- svr.Get("/sdcpp/v1/capabilities", [runtime](const httplib::Request&, httplib::Response& res) {
+ routes.get("/sdcpp/v1/capabilities", [runtime](const httplib::Request&, httplib::Response& res) {
res.status = 200;
res.set_content(make_capabilities_json(*runtime).dump(), "application/json");
});
- svr.Post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (req.body.empty()) {
res.status = 400;
@@ -641,7 +663,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
});
- svr.Post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (req.body.empty()) {
res.status = 400;
@@ -667,7 +689,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
std::shared_ptr<AsyncGenerationJob> job = std::make_shared<AsyncGenerationJob>();
job->kind = AsyncJobKind::ImgGen;
job->status = AsyncJobStatus::Queued;
- job->created_at = unix_timestamp_now();
+ job->created_at = unix_timestamp_now_ms();
job->img_gen = std::move(request);
{
@@ -703,7 +725,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
});
- svr.Post("/sdcpp/v1/vid_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post("/sdcpp/v1/vid_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (req.body.empty()) {
res.status = 400;
@@ -729,7 +751,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
std::shared_ptr<AsyncGenerationJob> job = std::make_shared<AsyncGenerationJob>();
job->kind = AsyncJobKind::VidGen;
job->status = AsyncJobStatus::Queued;
- job->created_at = unix_timestamp_now();
+ job->created_at = unix_timestamp_now_ms();
job->vid_gen = std::move(request);
{
@@ -765,7 +787,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
});
- svr.Get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) {
AsyncJobManager& manager = *runtime->async_job_manager;
std::lock_guard<std::mutex> lock(manager.mutex);
purge_expired_jobs(manager);
@@ -787,7 +809,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(make_async_job_json(manager, *it->second).dump(), "application/json");
});
- svr.Post(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+)/cancel)", [runtime](const httplib::Request& req, httplib::Response& res) {
+ routes.post(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+)/cancel)", [runtime](const httplib::Request& req, httplib::Response& res) {
AsyncJobManager& manager = *runtime->async_job_manager;
std::lock_guard<std::mutex> lock(manager.mutex);
purge_expired_jobs(manager);
diff --git a/examples/server/runtime.cpp b/examples/server/runtime.cpp
index 76d89d3..34140ea 100644
--- a/examples/server/runtime.cpp
+++ b/examples/server/runtime.cpp
@@ -11,6 +11,10 @@
#include "common/common.h"
#include "common/log.h"
+#include "model_catalog.h"
+
+#include <map>
+#include <set>
namespace fs = std::filesystem;
@@ -210,6 +214,13 @@ ArgOptions SDSvrParams::get_options() {
options.manual_options = {
{"-h", "--help", "show this help message and exit", on_help_arg},
+ {"", "--model-dir", "model root; repeat to search multiple directories (default: models)",
+ [this](int argc, const char** argv, int index) {
+ if (++index >= argc || argv[index][0] == '\0')
+ return -1;
+ model_dirs.push_back(argv_to_utf8(index, argv));
+ return 1;
+ }},
};
add_log_options(options, log_level);
return options;
@@ -253,71 +264,79 @@ std::string SDSvrParams::to_string() const {
void refresh_lora_cache(ServerRuntime& rt) {
std::vector<LoraEntry> new_cache;
-
- if (rt.ctx_params->lora_model_dir.empty()) {
- std::lock_guard<std::mutex> lock(*rt.lora_mutex);
- rt.lora_cache->clear();
- return;
+ std::vector<ModelRoot> directories;
+ if (!rt.ctx_params->lora_model_dir.empty()) {
+ directories.push_back(make_model_root(fs::u8path(rt.ctx_params->lora_model_dir), true));
+ } else {
+ for (const auto& root : get_model_roots(*rt.svr_params)) {
+ try {
+ const auto paths = model_category_directories(root, "loras");
+ for (size_t i = 0; i < paths.size(); ++i) {
+ if (!model_path_inside(root.path, fs::canonical(paths[i]))) {
+ LOG_WARN("LoRA directory escapes model directory: %s", paths[i].u8string().c_str());
+ continue;
+ }
+ directories.push_back(make_model_root(paths[i], root.primary && lower_ascii(paths[i].filename().u8string()) == "loras"));
+ }
+ } catch (const std::exception& e) {
+ LOG_WARN("error while finding LoRA directories in %s: %s", root.label.c_str(), e.what());
+ }
+ }
}
-
- fs::path lora_dir = fs::u8path(rt.ctx_params->lora_model_dir);
- std::error_code ec;
- if (fs::exists(lora_dir, ec) && !ec && fs::is_directory(lora_dir, ec) && !ec) {
+ std::set<std::string> seen;
+ std::map<std::string, size_t> names;
+ for (const auto& directory : directories) {
try {
- auto it = fs::recursive_directory_iterator(
- lora_dir,
- fs::directory_options::skip_permission_denied,
- ec);
- auto end = fs::recursive_directory_iterator();
- while (!ec && it != end) {
- std::error_code entry_ec;
- bool is_reg = it->is_regular_file(entry_ec);
- if (entry_ec || !is_reg) {
- it.increment(ec);
+ if (!fs::exists(directory.path))
+ continue;
+ for (fs::recursive_directory_iterator it(directory.path), end; it != end; ++it) {
+ std::error_code ec;
+ const auto canonical = fs::canonical(it->path(), ec);
+ if (ec || !model_path_inside(directory.path, canonical)) {
+ it.disable_recursion_pending();
+ LOG_WARN("skipping inaccessible LoRA or path outside LoRA directory: %s", it->path().u8string().c_str());
continue;
}
- const fs::path& p = it->path();
- if (!is_supported_model_ext(p)) {
- it.increment(ec);
+ const auto ext = lower_ascii(it->path().extension().u8string());
+ if (!it->is_regular_file() || (!is_supported_model_ext(it->path()) && ext != ".ckpt" && ext != ".sft") ||
+ !seen.insert(model_path_key(canonical)).second)
continue;
- }
-
- LoraEntry lora_entry;
- lora_entry.name = p.stem().u8string();
- lora_entry.fullpath = p.u8string();
- std::string rel = p.lexically_relative(lora_dir).u8string();
- std::replace(rel.begin(), rel.end(), '\\', '/');
- lora_entry.path = rel;
-
- new_cache.push_back(std::move(lora_entry));
- it.increment(ec);
+ const auto relative = it->path().lexically_relative(directory.path).generic_u8string();
+ LoraEntry entry;
+ entry.name = it->path().stem().u8string();
+ entry.path = directory.primary ? relative : directory.id + "/" + relative;
+ entry.fullpath = it->path().u8string();
+ entry.display_path = relative;
+ entry.root = directory.path;
+ ++names[relative];
+ new_cache.push_back(std::move(entry));
}
} catch (const std::exception& e) {
- LOG_WARN("error while scanning lora directory '%s': %s", lora_dir.u8string().c_str(), e.what());
- return;
+ LOG_WARN("error while scanning LoRA directory '%s': %s", directory.path.u8string().c_str(), e.what());
}
}
-
- if (ec) {
- LOG_WARN("error while scanning lora directory '%s': %s", lora_dir.u8string().c_str(), ec.message().c_str());
- return;
+ for (auto& entry : new_cache) {
+ if (names[entry.display_path] > 1)
+ entry.display_path = fs::u8path(entry.fullpath).generic_u8string();
}
-
std::sort(new_cache.begin(), new_cache.end(), [](const LoraEntry& a, const LoraEntry& b) {
return a.path < b.path;
});
-
- {
- std::lock_guard<std::mutex> lock(*rt.lora_mutex);
- *rt.lora_cache = std::move(new_cache);
- }
+ std::lock_guard<std::mutex> lock(*rt.lora_mutex);
+ *rt.lora_cache = std::move(new_cache);
}
std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) {
std::lock_guard<std::mutex> lock(*rt.lora_mutex);
auto it = std::find_if(rt.lora_cache->begin(), rt.lora_cache->end(),
[&](const LoraEntry& entry) { return entry.path == path; });
- return it != rt.lora_cache->end() ? it->fullpath : "";
+ if (it == rt.lora_cache->end())
+ return "";
+ std::error_code ec;
+ const auto canonical = fs::canonical(fs::u8path(it->fullpath), ec);
+ if (ec || !model_path_inside(it->root, canonical) || !fs::is_regular_file(canonical, ec) || ec)
+ return "";
+ return canonical.u8string();
}
void refresh_upscaler_cache(ServerRuntime& rt) {
@@ -404,8 +423,8 @@ void refresh_upscaler_cache(ServerRuntime& rt) {
}
}
-int64_t unix_timestamp_now() {
- return std::chrono::duration_cast<std::chrono::seconds>(
+int64_t unix_timestamp_now_ms() {
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
diff --git a/examples/server/runtime.h b/examples/server/runtime.h
index 5baa633..d9272f6 100644
--- a/examples/server/runtime.h
+++ b/examples/server/runtime.h
@@ -17,14 +17,16 @@ using json = nlohmann::json;
struct ArgOptions;
struct SDContextParams;
struct AsyncJobManager;
+struct ServerModels;
struct SDSvrParams {
std::string listen_ip = "127.0.0.1";
int listen_port = 1234;
std::string serve_html_path;
- bool normal_exit = false;
+ std::vector<std::string> model_dirs;
+ bool normal_exit = false;
sd_log_level_t log_level = SD_LOG_INFO;
- bool color = false;
+ bool color = false;
ArgOptions get_options();
bool validate();
@@ -36,6 +38,8 @@ struct LoraEntry {
std::string name;
std::string path;
std::string fullpath;
+ std::string display_path;
+ std::filesystem::path root;
};
struct UpscalerEntry {
@@ -43,7 +47,7 @@ struct UpscalerEntry {
std::string path;
std::string fullpath;
std::string model_name;
- int scale = 4;
+ int scale = 4;
int image_upscale_factor = 0;
uintmax_t file_size = 0;
std::filesystem::file_time_type last_modified;
@@ -53,13 +57,14 @@ struct ServerRuntime {
sd_ctx_t* sd_ctx;
std::mutex* sd_ctx_mutex;
const SDSvrParams* svr_params;
- const SDContextParams* ctx_params;
- const SDGenerationParams* default_gen_params;
+ SDContextParams* ctx_params;
+ SDGenerationParams* default_gen_params;
std::vector<LoraEntry>* lora_cache;
std::mutex* lora_mutex;
std::vector<UpscalerEntry>* upscaler_cache;
std::mutex* upscaler_mutex;
AsyncJobManager* async_job_manager;
+ ServerModels* models;
};
struct ImgGenJobRequest {
@@ -105,4 +110,4 @@ std::string unsupported_generation_mode_error(SDMode mode);
void refresh_lora_cache(ServerRuntime& rt);
std::string get_lora_full_path(ServerRuntime& rt, const std::string& path);
void refresh_upscaler_cache(ServerRuntime& rt);
-int64_t unix_timestamp_now();
+int64_t unix_timestamp_now_ms();
diff --git a/start-server.bat b/start-server.bat
new file mode 100644
index 0000000..60ea9e1
--- /dev/null
+++ b/start-server.bat
@@ -0,0 +1,45 @@
+@echo off
+setlocal DisableDelayedExpansion
+
+set "SD_SERVER_PATH=%SD_SERVER_PATH%"
+set "MODEL_DIR=./models"
+set "COMMANDLINE_ARGS="
+
+if exist "%~dp0start-server.local.bat" (
+ call "%~dp0start-server.local.bat"
+ if errorlevel 1 exit /b 1
+)
+
+if defined SD_SERVER_PATH goto explicit
+
+for %%F in (
+ "%~dp0sd-server.exe"
+ "%~dp0bin\Release\sd-server.exe"
+ "%~dp0bin\sd-server.exe"
+ "%~dp0build\bin\Release\sd-server.exe"
+ "%~dp0build\bin\sd-server.exe"
+) do (
+ if exist "%%~F" (
+ set "SD_SERVER_PATH=%%~F"
+ goto run
+ )
+)
+
+>&2 echo sd-server.exe was not found. Build it first or set SD_SERVER_PATH to its executable path.
+exit /b 1
+
+:explicit
+if exist "%SD_SERVER_PATH%" goto run
+>&2 echo SD_SERVER_PATH does not exist: "%SD_SERVER_PATH%"
+exit /b 1
+
+:run
+if "%~1"=="--version" goto version
+echo Running: "%SD_SERVER_PATH%" --model-dir "%MODEL_DIR%" %COMMANDLINE_ARGS% %*
+"%SD_SERVER_PATH%" --model-dir "%MODEL_DIR%" %COMMANDLINE_ARGS% %*
+exit /b %errorlevel%
+
+:version
+echo Running: "%SD_SERVER_PATH%" %*
+"%SD_SERVER_PATH%" %*
+exit /b %errorlevel%
diff --git a/start-server.sh b/start-server.sh
new file mode 100644
index 0000000..b73dc27
--- /dev/null
+++ b/start-server.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -eu
+
+SD_SERVER_PATH="${SD_SERVER_PATH:-}"
+MODEL_DIR="./models"
+COMMANDLINE_ARGS=""
+
+script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+if [ -f "$script_dir/start-server.local.sh" ]; then
+ . "$script_dir/start-server.local.sh"
+fi
+
+if [ "${1:-}" != "--version" ]; then
+ # Split configured arguments on whitespace without expanding wildcards.
+ set -f
+ set -- --model-dir "$MODEL_DIR" $COMMANDLINE_ARGS "$@"
+fi
+
+run_server() {
+ printf 'Running:'
+ for arg do
+ printf " '%s'" "$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")"
+ done
+ printf '\n'
+ exec "$@"
+}
+
+if [ -n "$SD_SERVER_PATH" ]; then
+ case "$SD_SERVER_PATH" in
+ */*) ;;
+ *) SD_SERVER_PATH="./$SD_SERVER_PATH" ;;
+ esac
+ if [ ! -f "$SD_SERVER_PATH" ] || [ ! -x "$SD_SERVER_PATH" ]; then
+ printf 'SD_SERVER_PATH is not an executable file: %s\n' "$SD_SERVER_PATH" >&2
+ exit 1
+ fi
+ run_server "$SD_SERVER_PATH" "$@"
+fi
+
+for server in \
+ "$script_dir/sd-server" \
+ "$script_dir/bin/Release/sd-server" \
+ "$script_dir/bin/sd-server" \
+ "$script_dir/build/bin/Release/sd-server" \
+ "$script_dir/build/bin/sd-server"
+do
+ if [ -f "$server" ] && [ -x "$server" ]; then
+ run_server "$server" "$@"
+ fi
+done
+
+printf '%s\n' 'sd-server was not found. Build it first or set SD_SERVER_PATH to its executable path.' >&2
+exit 1