Skip to content

hal0 v1.7.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 17:45
· 78 commits to main since this release
5f52993

Highlights

  • Image models have a catalog of their own. hal0 comfyui models list|install|remove, the dashboard and /api/comfyui/models, /jobs and
    /files install catalog models with resumable, sha256-checked downloads,
    and the installer offers a starter model sized to the box's RAM
    (HAL0_IMAGE_MODELS answers it unattended). Image models leave the LLM
    registry, and /v1/images/generations renders every installed catalog
    model the API can render plus your own SDXL and SD 1.5 checkpoints.
    (#3032, #3037, #3054, #3061)
  • Open WebUI generates images through hal0's /v1. Its image engine
    moves to OpenAI, pointed at /v1/images/generations with the box's client
    key, so its images get the same model, defaults and GPU switch as any other
    client, and the four defects that broke Open WebUI images are gone. (#3062)
  • Image mode lasts only while an image renders. Every GPU LLM slot
    unloads for a render and reloads about 10 seconds after the last one, and
    [image].idle_restore_minutes is gone. A chat that arrives mid-render
    waits, a slot still loading is waited for and unloaded before ComfyUI runs,
    and a failed unload rolls the switch back instead of leaving an LLM beside
    ComfyUI. (#3062, #3080, #3082)
  • A fit check before you install or render. Every catalog model carries a
    memory fit against the GPU pool and RAM, judged at the size you pick, and a
    render that cannot fit is refused with 422 image.wont_fit before any slot
    unloads. Memory figures come from one owner (ADR-0039), which reads a
    capped LXC guest's RAM limit, shows other guests on a shared GPU and
    credits what image mode frees on bare-metal unified memory. (#3064, #3070,
    #3079)
  • A new Image Gen tab: Generate, Models and Engine. A GPU status line says
    what the GPU is doing and what a render costs chat, the Generate tab follows
    the switch phase by phase and the render step by step, fit meters sit on
    every model card, and Hold image mode pins the GPU behind a confirm.
    (#3078)
  • ComfyUI's GPU gate authenticates with a token of its own, never the
    admin key, so image generation keeps working with hal0 auth require on.
    Run hal0 slot restart img once after updating. (#3067, ADR-0040)

Breaking

  • hal0 comfyui orchestrate-models is gone: use hal0 comfyui models install <id>.... There is no alias, and the command exits with the CLI's unknown-command error. hal0 comfyui models list shows the image-model catalog with each model's install state, the GB it still has to download and the pick recommended for this box (--json for scripts); models install installs the models one at a time, following each download to the end, and exits 1 if any fails; models remove <id> and hal0 comfyui files remove a model and list the weight files on disk. --dry-run and --log-dir are gone with the command. The vendored get_*.sh download scripts under installer/comfyui/ are deleted: downloads run in hal0, with resume and a sha256 check.

  • gen.capabilities in a hal0-setup.yaml answer file is replaced by gen.models: a list of image model ids (for example models: [sdxl-lightning]; hal0 comfyui models list prints them all). A file that still has gen.capabilities fails validation with a message naming gen.models, and an unknown id is refused with the list of known ones. gen.models is installed when gen.mode is scaffold_and_download, one model after another with a progress line every few seconds, and hal0 setup --emit-answers writes it. The auto setup path installs no image models; the guided path and install.sh offer a starter set and download nothing unless you choose one. Pick any later with hal0 comfyui models install or from the dashboard.

  • POST /api/comfyui/models/fetch is gone: use POST /api/comfyui/models/{model_id}/install. The fetch route took a capability and family (or auto); an install names one catalog model, answers 202 with its job, and is followed on GET /api/comfyui/jobs. A request the old route would have started now gets 404 or 405.

  • COMFYUI_MODELS_DIR, COMFYUI_WORKFLOWS_DIR and COMFYUI_DATA_DIR no longer do anything. There is no fallback. ComfyUI's models, workflows and data live under one root that follows the configured model store (<model store>/comfyui); HAL0_COMFYUI_DATA_ROOT moves that root as a whole. A box that set one of the three variables to a path other than that root should set HAL0_COMFYUI_DATA_ROOT instead.

  • /v1/images/generations answers an image id outside the catalog with 404 image.model_unknown, and image.model_not_curated is gone. There is no alias: a client that matched the old code should match the new one. model is now an installed catalog image model the API can render (sdxl-turbo, sd-1.5-pruned-emaonly, sdxl-lightning, qwen-image-lightning or qwen-image), a ckpt/<path> checkpoint, or empty for the model selected for img. An empty model used to render sdxl-turbo whatever was installed; a client that relied on that now gets the img selection's model, or 422 image.model_required when nothing is selected, and should send model explicitly. The 404 body lists the ids that work in error.details.available. A catalog model that is not installed gets 409 image.model_not_installed (naming the install call), one the API cannot render (video, edit, upscale) gets 422 image.model_not_renderable, and an empty model with nothing selected gets 422 image.model_required. Flux-2-Klein-9B-GGUF used to render through a fallback to the SDXL Turbo graph and now gets 404. Install a catalog model with hal0 comfyui models install <id>.

  • [image].default_size now defaults to "", the model's native size, instead of "1024x1024". On an img slot whose TOML does not set default_size (or sets "", as the shipped seed does), a request with no size renders at the model's native size, where it used to get 1024x1024: Stable Diffusion 1.5 and SDXL Turbo now come out at 512x512. A slot that sets a value keeps it. Set default_size = "1024x1024" in the slot's [image] table to keep the old size for every model. A box that already has an img slot is unaffected: the v1.6.x installer seeded default_size = "1024x1024" into /etc/hal0/slots/img.toml, and hal0 update does not rewrite slot files, so every model keeps rendering at 1024x1024 there. To opt in to native sizes, set default_size = "" in the [image] table of that file, or clear the Default size field on the dashboard's Capabilities page; the change applies to the next request, with no restart.

  • Image models are no longer in the model registry: the curated image ids are gone from /api/models and hal0 model pull. sdxl-turbo, sd-1.5-pruned-emaonly, sdxl-lightning, esrgan-4x, SD-Turbo-GGUF and Flux-2-Klein-9B-GGUF are no longer curated registry entries, so they are neither listed nor pullable there, and there is no alias. A pull of an image catalog id, or of any model with the image capability, answers 422 model.image_asset before a row is seeded or a byte moves, and its message names hal0 comfyui models install <id>. The same 422 answers POST /api/models, PUT /api/models/{id} and POST /api/models/add-from-path (hal0 model add) when path is inside ComfyUI's models folder; a scan no longer registers a file there. Migration 014 deletes the registry rows whose path is in that folder (the default location; a row under a relocated tree stays until you remove it) or whose capabilities include image, with their bookkeeping rows: rows only, never files, and the image catalog reads each model's state from the files on disk. No operator step is needed. A script that pulled an image id should call hal0 comfyui models install <id>.

  • The GGUF image ids SD-Turbo-GGUF and Flux-2-Klein-9B-GGUF are dropped, with no converter. The image catalog has no GGUF model, so neither can be installed again. A request that names one gets 404 image.model_unknown, and so does a request with no model on a box whose img selection names one: such a box must install a catalog model first (hal0 comfyui models install <id>) and then pick it under AI Capabilities → Image or with hal0 capabilities set img img --model <id>: picking a model that is not downloaded yet is refused with 409 capability.model_not_downloaded. The bundles that named them now name catalog models (see Changed).

  • Open WebUI generates images through hal0's /v1/images/generations instead of calling ComfyUI. hal0 sets Open WebUI's image engine to OpenAI, points it at hal0's /v1 with the box client key, and sets the model to the one selected for img. Every render from Open WebUI therefore uses the same template, step count, size and GPU switch a /v1 client gets, including the img slot's [image].default_size and default_steps. hal0 leaves Open WebUI's image size empty so that hal0 picks it. Image generation is turned on in Open WebUI only while the img model is installed and the API can render it. Each render from Open WebUI switches the GPU to image mode for that render only. A chat request for a GPU-slot model that arrives while the image renders waits for the render and for hal0 to reload the chat model, so Open WebUI's reply in the same turn, right after the image, is served instead of being refused with 503 gpu.image_mode (see the next entry). Chat on NPU and CPU slots is not affected. COMFYUI_BASE_URL, COMFYUI_WORKFLOW and COMFYUI_WORKFLOW_NODES are no longer written to openwebui.env, and the next render (on hal0 update, an image model install or removal, or an img selection change) removes the copies hal0 wrote. A value you set by hand stays. Choose the model in hal0 (hal0 capabilities set img img --model <id>): Open WebUI's image settings list OpenAI's model names, and a change made there lasts only until Open WebUI restarts.

  • The GPU leaves image mode as soon as image generation stops, and [image].idle_restore_minutes is gone. Image mode used to hold until idle_restore_minutes (60 by default) passed with no image activity. Now it holds only while a render runs: a /v1/images/generations request (Open WebUI and the omni loop use it too), or a render in ComfyUI's queue from its editor. About ten seconds after the last render hal0 frees ComfyUI's models and reloads the GPU slots it unloaded. A request for one of those slots waits for the render to finish and for that reload, up to 120 seconds, instead of getting 503 gpu.image_mode; it gets the 503, with Retry-After: 15, only when image mode is pinned, ComfyUI's queue cannot be read, or the 120 seconds run out. A switch to generation without pin, such as the dashboard's Start button, also ends about ten seconds after the last render, or after the switch when nothing renders: pin image mode to hold it, and it holds until you unpin it or force a switch to inference. Pinning needs image mode: pass pin on the switch that enters it, because POST /api/comfyui/pin with {"pinned": true} while the GPU serves LLMs now answers 409 comfyui.not_image_mode instead of a pin that never held. A ComfyUI queue that never drains keeps image mode until you force a switch to inference. hal0 update and each hal0-api start remove idle_restore_minutes from slot files, the API refuses it, and the "Idle restore" row is gone from Settings. GET /api/comfyui/status drops arbiter.idle_restore_at, adds arbiter.switching and arbiter.image_jobs, and reports its top-level mode as generation only once a switch into image mode has finished (inference while hal0 switches or restores the GPU).

  • ComfyUI's GPU gate refuses a prompt that hal0 cannot hand the GPU to. After updating, run hal0 slot restart img once. ComfyUI's hal0_gpu_gate node, which every prompt passes (the editor's, and hal0's own /v1 renders), now waits while hal0 is still switching the GPU, asks again if an LLM request took the GPU back before the prompt was queued, and waits up to 180 seconds. When it lets a prompt through in image mode it first tells hal0 (POST /api/comfyui/prompt-handoff), which holds image mode for that prompt for about 3 seconds, until ComfyUI's queue shows it. It refuses the prompt and queues nothing, answering ComfyUI's /prompt with a 503 that carries the reason, when hal0 rejects the switch, stops answering mid-switch, still holds the GPU for the LLM slots after three switch requests, has not finished the switch within 180 seconds, has been unreachable (or silent about which mode holds the GPU) for 5 seconds, or answers 401 or 403 (refused at once, with a reason that says hal0 requires authentication). It used to let such a prompt through, and it could render next to the loaded LLM slots. The node reaches hal0-api on the port and address it binds (HAL0_PORT, HAL0_BIND_HOST) and reads a lightweight GET /api/comfyui/arbiter, so a box whose API is not on port 8080 keeps its image generation. To let prompts through while hal0-api is down or refuses the gate's credential, unguarded as in v1.6.5, set HAL0_COMFYUI_GATE_FAIL_OPEN=1 in /etc/hal0/api.env, restart hal0-api, then run hal0 slot restart img: hal0 writes the setting into the slot's unit when it loads the slot, so a bare systemctl restart does not pick it up. On a box with auth enforcement on (hal0 auth require on or HAL0_REQUIRE_AUTH=1), or one whose HAL0_BIND_HOST is a specific address with an admin key set, the gate authenticates with a token of its own (see Security); a container started before the update has none, so restarting img is what keeps image generation working there. After a rollback to 1.6.x the newer gate keeps working with the older hal0, reading its /api/comfyui/status, so no step is needed, except that it refuses image prompts on a box that enforces auth (1.6.x does not know the gate's token), and once img restarts under 1.6.x it refuses image prompts on a box whose hal0-api does not answer on 127.0.0.1:8080 (another port, or HAL0_BIND_HOST set to a specific address), and, unlike the 1.6.x gate, it still refuses when that hal0 is silent or slow (see docs/guides/update-and-rollback.mdx). A running img container keeps the gate it started with, and hal0 update does not restart slots, so restart it once after updating. Until then hal0 keeps that older gate waiting while it switches or restores the GPU, but the older gate still lets a prompt through when hal0-api is unreachable or after its own 120 seconds, and such a prompt can render next to the LLM slots.

  • GET /api/comfyui/status drops memory: read the memory block of GET /api/stats/hardware instead. memory was {gtt_used_gb, gtt_ceil_gb, ram_used_gb, ram_ceil_gb, pressure}, folded from ComfyUI's /system_stats against ceilings hal0 hard-coded (an 80 GB GPU pool, 96 GB of RAM) that your box may not have. There is no alias: the key is absent from the response, so a client that read body["memory"]["gtt_used_gb"] now fails on a missing key. GET /api/stats/hardware carries the one memory budget (ADR-0039), with this box's own GPU pool and RAM totals. The dashboard's old Image Gen pane and its GPU memory gauges are gone with the key: the tab is now Generate, Models and Engine.

Added

  • /api/comfyui/models, /api/comfyui/jobs and /api/comfyui/files list, install, cancel and remove image models, and hal0 comfyui models drives them. GET /api/comfyui/models lists the catalog with each model's install state and the size still to download and in total (missing_gb and approx_gb; a GB is 2^30 bytes here, as in the memory budget); POST /api/comfyui/models/{model_id}/install starts a download and answers 202 with its job; DELETE /api/comfyui/models/{model_id} removes the model's weights and workflow and keeps, and lists as kept with the models that hold it, any weight file another complete model loads (two models can load one file set, so each keeps the other complete) unless ?include_shared=true deletes it too and lists the models it uninstalled as affected, deleting their workflows as well; GET /api/comfyui/jobs and POST /api/comfyui/jobs/{model_id}/cancel follow and stop installs; GET /api/comfyui/files lists the weight files on disk with the catalog models that own each. An install that cannot work is refused before anything downloads: 409 comfyui.models_dir_not_writable for a destination directory the hal0 user cannot write, 409 comfyui.staging_cross_device, or 507 comfyui.insufficient_disk. A removal that hits a file it cannot delete stops with 409 comfyui.remove_failed and lists what had already gone. hal0 comfyui models list|install|remove and hal0 comfyui files are the CLI over them; install takes several ids and installs them one at a time, and remove --include-shared deletes the shared files too. An install of a model whose weights are all on disk but whose workflow is missing (a sibling model completed it) writes the workflow instead of answering 409 comfyui.model_installed, and the disk check counts a resumable partial download as already fetched.

  • The installer offers a starter image model, and HAL0_IMAGE_MODELS answers it unattended. On a box with a ROCm lane for ComfyUI, install.sh lists the image models the API can render with the GB each still has to download, and marks the one that suits the box's memory as recommended (Stable Diffusion 1.5 below 16 GB, SDXL Lightning from 16 GB, Qwen-Image Lightning from 96 GB). Enter downloads nothing, a bad answer is asked again, and a failed download warns while the install carries on. HAL0_IMAGE_MODELS skips the prompt: recommended, or catalog ids or menu numbers separated by commas or spaces; none or 0 skips the step even on a terminal, and an install with no terminal downloads nothing unless it is set. The guided hal0 setup asks the same question, after its question about scaffolding capability slots.

  • The first image model you install that the API can render becomes the img model. From the installer, hal0 setup or hal0 comfyui models install, hal0 selects it for the img capability when none is selected, which starts the img slot. A selection you made is left alone, and nothing is started.

  • hal0-api writes the ComfyUI workflow of hand-installed image models when it starts, and a removal is remembered. A model whose weights were copied into the models folder reads as installed, so no install ever wrote its workflow. A model removed while other models still hold its weights stays installed and is not given its workflow back; installing it again restores it.

  • GET /api/comfyui/models reports engine: {available, reason}, and an unreadable models tree is a 409 instead of a 500. available is false, with a reason, on a host with no ROCm lane; the catalog is listed either way. The model list, install and removal routes, and /v1/images/generations, answer 409 comfyui.models_dir_not_writable when the hal0 user cannot read the models tree.

  • DELETE /api/comfyui/files/{subdir}/{relpath} deletes a weight file no catalog model loads, and the gated MCP tool comfyui_file_delete calls it. subdir and relpath are the fields of a GET /api/comfyui/files row, so a file the listing shows without an owner can be deleted, and the answer is {"deleted": "<subdir>/<relpath>"}. It refuses a file a catalog model loads (409 comfyui.file_catalog_owned, with error.details.models naming the owners: remove the model instead), a symlink (400 comfyui.file_symlink), a subdir that is not a model directory or a relpath that is absolute, climbs out with .., has a hidden part or does not name a weight file such as a .yaml (400 comfyui.file_path_invalid), a missing file (404 comfyui.file_not_found) and a file the hal0 user may not delete (403 comfyui.file_permission). Deleting the checkpoint behind the img selection re-renders Open WebUI's image settings. Each GET /api/comfyui/files row now says whether its path is a symlink.

  • GET /api/comfyui/models rows carry a memory fit, and GET /api/comfyui/models/{model_id}/fit?size=WxH judges the size you choose. A row's fit is {verdict, reason, need, render_free_gb, ram_free_gb} at the model's native size: fits, tight (the need is above 0.9 of what is free) or wont_fit, judged separately against the GPU pool a render can use (render_free_gb, which does not subtract the image engine, so a back-to-back render is not refused for the weights the engine still holds) and this box's RAM (ram_free_gb, a container's limit included) and never summed, with reason naming the budget that decided it. A size the catalog measured is exact; another size of a measured model is an estimate that does not undershoot, and a model nobody measured needs its weight total, a lower bound (need.gpu_estimated, need.ram_estimated). fit is null, never wont_fit, when the box cannot say (no GPU lane or pool reading, RAM unreadable). The list also carries store: {free_gb, path}, the ComfyUI models folder and the free space under it (free_gb is null when it cannot be measured). The /fit route answers {"fit": {...} | null, "size": "WxH"}, where size is the size it judged: the one /v1 would render for the size you ask about (each side clamped to 64 to 2048 and floored to a multiple of 8, so 1030x1030 is judged as 1024x1024), or the native size when you leave it out. It answers 404 comfyui.model_unknown for an id outside the catalog and 422 image.invalid_size for a size that is not WIDTHxHEIGHT. Both read the memory budget (ADR-0039), so a verdict and the memory screens agree about what is free. The list also carries recommendation: {id, pool_gb, limited_by}, the figure the recommended model was chosen from (pool_gb, the smaller of the GPU pool and this box's RAM limit) and which of the two set it (limited_by is "ram" or "gpu"); it is null on a box that was never probed. The fields are in the new Image readiness API reference.

  • The arbiter block of GET /api/comfyui/status and GET /api/comfyui/arbiter says what the GPU is doing. phase is draining, unloading, starting_engine or reloading while a switch or a restore runs, and handoff, rendering or grace in image mode, else null (a pinned image mode with nothing rendering has no grace and reads null beside pinned: true). restore_at and handoff_until are epoch seconds, set only in grace and handoff; drain_in_flight counts the requests still running on the slots being drained; unload_set lists the slots image mode unloaded, or the GPU LLM slots a switch would unload now, and is null when hal0 has not read the slot configs yet (never [] for unknown); llm_waiters is {count, oldest_s} for LLM requests waiting for the GPU.

  • ComfyUI's GPU gate reports its version and the prompts it refuses. The hal0_gpu_gate node sends X-Hal0-Gate-Version on every request to hal0-api and, when it refuses a prompt, says why with POST /api/comfyui/gate-refusal (internal: only the gate's own token admits it, from the box itself, and it is not an MCP tool; an empty or whitespace-only reason is 422 comfyui.invalid_gate_refusal). GET /api/comfyui/status gains gate: {state, version_seen}, where state is current, stale (the engine runs an older gate: run hal0 slot restart img), unknown (the gate has not called since hal0-api started) or no_credential (hal0 requires the gate to authenticate and it has no token it can send, or HAL0_BIND_HOST is a host name), and gate_refusal: {at, reason}, the latest refusal or null: reason is the text the gate refused the prompt with or, for a switch request hal0 rejected, hal0's own sentence, cut to 500 characters. GET /api/comfyui/arbiter carries gate_refusal too. hal0 also records a refusal itself when a /v1/images/generations render or a workflow launch meets one, because a gate without a credential cannot report on a box that requires one.

  • GET /api/comfyui/status reports the step progress of the render hal0 submitted. progress is {model, step, total, node, elapsed_s, prompt_id}, or null when no such render is live: step and total are null until ComfyUI's first progress frame, node is the graph node it is executing, and with several renders queued it is the one that stepped most recently. hal0 follows the render on ComfyUI's websocket (the websockets package, now a declared dependency, >=13.0) while it polls for the result; a socket that cannot open or drops costs only the read-out, never the render. A render queued from ComfyUI's editor has no entry; read queue for it.

  • GET /api/config/urls says whether the ComfyUI link is one you declared. It gains comfyui_public, true only when HAL0_COMFYUI_PUBLIC_URL is set. ComfyUI listens on the box's loopback, so the http://<host>:8188 link hal0 derives from the request host answers on the box itself and nowhere else; the dashboard opens the editor only on the box, or when comfyui_public is true, and shows an ssh -L command otherwise.

  • The Image Gen tab is Generate, Models and Engine. Generate renders from the dashboard: a prompt, a model (the one selected for img first, then every installed catalog model the API renders, then your own checkpoints), a size (the model's own sizes or a custom one, each side 64 to 2048 and floored to a multiple of 8) and, under More options, a negative prompt, steps, CFG, one to four images and a seed. The cost to chat sits under the button, a run shows its steps with a progress bar and Cancel, and the last 12 results keep Open, Download and Reuse seed. Models lists the image-model catalog by task with Install, Cancel, Remove, Set as default, Generate and Open in ComfyUI, the filters All, Fits this box and Installed, and the free space under the model store; with no model installed it opens on Set up image generation, which installs the recommended model in one click, says why it was chosen (chosen from N GB of RAM (container limit)) and offers two lighter models. Engine has the engine card (state, container, GPU gate, Restart engine, Logs, Stop image mode), the editor card, the queue (n running · n waiting, with hal0's own render) and the GPU device's temperature and clock. The sub-tabs open at #slots/image/generate, #slots/image/models and #slots/image/engine, and the tab is reachable on a box with no slots, where the Slots page used to show only "No slots configured".

  • A GPU status line heads the Image Gen tab and says what a render costs chat. It names what the GPU is doing (GPU · chat mode, Switching to image mode, Image mode · hand-off, Rendering · <model> · step n/N, Image mode ends in N s, Reloading GPU slots · k of n ready, Image mode pinned), what the next image pauses (Pauses a, b and c (N GB). They reload about 10 s after the last image.), how many chat requests wait, and the GPU gate's state, and offers the buttons that fit: Hold image mode and Keep image mode (each asks first, because a pin refuses GPU chat until you unpin), Unpin, Stop image mode (it asks before it drops a running render), Cancel, Cancel render, Restart engine, Open editor and Dismiss. A render queued from ComfyUI's editor shows too. On the Inference tab, the card of a slot that image mode paused carries a paused · image mode chip, and the banner shown while the GPU is in image mode follows the GPU arbiter and links to the Engine tab. The line is hidden on a host with no GPU pool reading or no ROCm lane.

  • Fit meters show whether a model fits before you install or run it. Each model card on the Models tab, and the Generate tab for the model and size you picked, draw two bars (the GPU pool and RAM · this box) with the model's need against what is free, a chip (Fits · N GB to spare, Tight, Won't fit) and the server's reason. The verdict is always the server's; a box the server cannot judge shows no chip, and a model that won't fit offers one that does. The Generate tab says judged at WxH when the server clamped the size you asked for, and Fit is checked when you generate. for a checkpoint of your own, which has no catalog row.

  • Settings → AI Capabilities → Image has a Default size picker. Model native (the empty value, still the default), 512x512, 768x768, 1024x1024, 1328x1328 or Custom…; a custom size must be a lower-case WxH with each side 64 to 2048 and a multiple of 8. A size saved before stays and does not block saving the other fields: it opens as Custom with a note on the size the engine renders.

Changed

  • /api/comfyui/status inventory.loras now counts LoRAs in subdirectories, and a failed scan hides the counts. The old count read only the top level of loras/, so Wan's LoRAs, which live in a directory of their own, were missed; every category now comes from one recursive scan of the models tree. When that scan fails, inventory is null instead of a row of zeros.

  • The installer no longer creates the ComfyUI directories, and HAL0_SKIP_COMFYUI no longer does anything. The block hardcoded /mnt/ai-models/comfyui, ignored [models].store and HAL0_COMFYUI_DATA_ROOT, and made root-owned directories the hal0 user could not write. The ComfyUI provider now creates every directory under the configured store when the img slot starts, and the installer has 15 steps instead of 16.

  • /v1/images/generations renders every installed catalog image model the API can render and your own SDXL and SD 1.5 checkpoints, and takes its defaults from the model. A checkpoint you put in the ComfyUI checkpoints/ folder whose safetensors header reads as SDXL or SD 1.5 is addressed as ckpt/<path>, the file's path under checkpoints/ without .safetensors (ckpt/a for a.safetensors, ckpt/sub/a for sub/a.safetensors); a checkpoint a catalog model loads gets no ckpt/ id and is addressed by that model's id. An empty model uses the model selected for img. A request that sets no size gets the slot's default_size when it is not empty, otherwise the model's native size: 1024x1024 for SDXL, 512x512 for Stable Diffusion 1.5 and SDXL Turbo, 1328x1328 for Qwen-Image. steps follow extra_body, then the slot's default_steps when above 0 (0 is the workflow template's own count), then the template's. The _hal0 block of the response carries model and template. Video, edit and upscale models stay in the ComfyUI editor.

  • Bundles install image models through the image catalog. hal0-Pro names sdxl-lightning (7.1 GB) in place of SD-Turbo-GGUF (1.3 GB), and hal0-Max and LMX-Omni-52B-Halo name qwen-image-lightning (47.9 GB) in place of Flux-2-Klein-9B-GGUF (8.4 GB), about 39.5 GB more to download. These are the GB the API and hal0 comfyui models list show (a GB is 2^30 bytes, as in the memory budget). Applying a bundle, or an img selection, through /api/install/apply or /api/install/apply-selections (which hal0 setup calls when the API is up) no longer pulls an image model into the registry: it creates the img slot with no model when there is none (an existing slot is left alone) and starts the catalog install, the job POST /api/comfyui/models/{model_id}/install starts, which runs on /api/comfyui/jobs. Both responses carry an image_installs list with a {model_id, job_id} row per image model, or job_id: null and skipped set to the catalog's error code when it refuses one (for example comfyui.install_running), or to comfyui.install_start_failed when the start itself breaks (an I/O error from the models store, say); a refusal never fails the apply. A host with no ROCm lane for ComfyUI reports the slot as skipped with image_engine_unavailable and creates none, and an id the catalog does not know is skipped with image_model_unknown.

  • Models → Image lists ComfyUI's files by component type, and the img slot points at AI Capabilities → Image for its model. The Image / ComfyUI tab of the Models view is the file inventory (GET /api/comfyui/files): checkpoints, diffusion models, text encoders, VAE, LoRAs, upscalers, CLIP vision and latent upscalers, each file marked with the catalog models that load it or as unmanaged, and an unmanaged file that is not a symlink has a Delete button. Image models are not registry rows, so the img slot drawer's Model field and the slot card's swap popover no longer list /api/models: they show the current model and link to AI Capabilities → Image. That page's Image panel has a new Model row that picks the image model from the catalog models the API can render (marking those not downloaded yet) and your own ckpt/<path> checkpoints; on a host with no ComfyUI image engine it shows a read-only line with the reason instead.

  • Memory figures come from one backend owner (ADR-0039). GET /api/stats/hardware gains a memory block with two budgets that are never summed: the GPU pool (gpu: its kind, total, used and free, what this box's slots and the image engine hold, and the rest as other_guests_gb; image_engine_slot names the slot that hosts the engine and image_engine_estimated says when its figure is that slot's estimate, which it always is on a discrete card, where ComfyUI's reading covers the whole card) and this box's RAM (ram: limit, used and free, read live), plus render_free_gb, the pool a render can use once image mode has unloaded this box's GPU slots: the pool less other guests, slots still starting and a 2 GB margin, with the image engine's own memory counted as room, since a model's need is measured above an idle engine and the engine renders inside its own allocation. gpu and render_free_gb are null when the box has no GPU lane or the kernel counter is missing, and RAM reads as zeros when /proc/meminfo cannot be read; the budget uses no fixed ceiling. The Slots page's telemetry bar and memory ruler, the Overview page's health strip and Unified memory card, Settings → Overview and the Slots page's free figure all take their pool, RAM and per-owner figures from that block instead of combining their own counters, so one screen shows one free number. The pool is named "GPU pool · shared" on unified memory and "VRAM" on a discrete card. The block is documented in the Memory budget API reference, and the dashboard guide has a Memory figures section. cgroup_max_mb in the hardware probe follows the same cgroup walk, so the installer sizes models against a limit set on a systemd slice. The Image Gen pane has no gauge of its own; the image fit verdicts and the /v1 preflight read it (see Added and Changed); and POST /api/models/feasibility keeps its own fits, tight and exceeds verdicts.

  • /v1/images/generations refuses a render that cannot fit before any slot unloads. A render whose model needs more GPU pool or RAM than the box has free, judged at the size it will render (the request's size, else the slot's default_size, else the model's native size, clamped as the workflow clamps it), answers 422 image.wont_fit with the need, the two free figures, the model and the size in error.details, before the GPU arbiter unloads a slot: the LLM slots stay loaded and nothing about the GPU changes. Before, such a render unloaded the LLM slots first and could then run out of memory in the engine. Only wont_fit refuses; tight, fits, an unknown budget and a check that itself fails let the render go ahead. A ckpt/<path> checkpoint is judged from its file size. extra_body.hal0_skip_fit_check: true skips the check.

  • API image sizes are GB at 2^30 bytes. The download sizes of image models (approx_gb and missing_gb on GET /api/comfyui/models, the image rows' size_gb in the capability picker, missing_gb in 409 image.model_not_installed) and the figures hal0 comfyui models list, the installer's starter menu, hal0 setup and the install and disk messages print count a GB as 2^30 bytes, like the memory budget, where the catalog's decimal figure used to leak out: Qwen-Image Lightning reads 47.9 where it read 51.4, SDXL Lightning 7.1 where it read 7.6. The word stays "GB". The plan table of hal0 setup says "GB free" and "GB threshold" where it said "GiB", with the same values. Bundle manifests' size_gb still holds the decimal figure.

  • GET /api/comfyui/status replaces it_s, eta and step with progress. The three fields were always null. A client that read them should read progress.step, progress.total and progress.elapsed_s; hal0 reports no rate and no estimate of the time left.

  • A prompt the GPU gate refuses answers 503 image.gate_refused, and a refused workflow launch is a 503. Both routes took any non-200 from ComfyUI's /prompt for a failure of their own: POST /v1/images/generations answered 502 dispatch.upstream_failed, and POST /api/comfyui/workflows/{name}/launch answered 202 queued with a null prompt_id. A 503 in the gate's shape now answers 503 image.gate_refused on both, with the gate's reason in error.message, and is recorded as gate_refusal on GET /api/comfyui/status; any other non-200 answer is unchanged.

Removed

  • The Image Gen tab's old "render hero" pane is gone, with its GTT and system RAM gauges and placeholder values. The old pane drew a GTT and a system RAM gauge, a frame counter, a hard-coded workflow strip and model-count pills, and filled any figure the status did not carry with a placeholder. A figure the status does not carry is left out, not replaced. The tab's Generate, Models and Engine sub-tabs replace it (see Added); memory figures are the top bar ruler's and the fit meters'.

  • The Services page's ComfyUI Queue drawer is gone, and the ComfyUI link opens only from a page on the box itself. The drawer fetched ComfyUI's /queue and /history from your browser at :8188, which answers on the box only; the queue is the Engine tab's Queue card now, from GET /api/comfyui/status. ComfyUI listens on the box's loopback, so the http://<host>:8188 link the Services page, its board card and the Overview Services widget opened is dead from another machine. There the button is disabled, and the Services page shows Only reachable on the box itself and the command ssh -L 8188:127.0.0.1:8188 <user>@<host>. A public URL declared with HAL0_COMFYUI_PUBLIC_URL still opens from anywhere.

Security

  • ComfyUI's GPU gate authenticates with a token of its own, never the admin key (#3058). The hal0_gpu_gate node calls three ADMIN routes before every image prompt (and reports a prompt it refused to a fourth), so a box that enforces auth (hal0 auth require on), or one whose HAL0_BIND_HOST is a specific address with an admin key set, refused it, and every image prompt with it. hal0 now mints the gate a token in /var/lib/hal0/comfyui-gate/token (0600 in a 0700 directory, owned by the service user) when the img slot loads, and mounts that directory read-only into the ComfyUI container; the token is never in the unit file or the container's environment. hal0-api accepts it on exactly the gate's four requests (GET /api/comfyui/arbiter, POST /api/comfyui/prompt-handoff, POST /api/comfyui/switchover asking for image mode without force or pin, where anything else answers 403 auth.forbidden, and POST /api/comfyui/gate-refusal, the reason the gate refused a prompt), only from the box itself (loopback or the address hal0-api binds), and on no other route, WebSocket or stream. The gate reads the token on every request and sends it only to hal0-api by address: through no proxy, never to a host name, and never on to a redirect. A token whose file others could have read is replaced at the next img load. Run hal0 slot restart img once after updating so the container gets its token. To replace the token, delete the file and restart img.

  • An approved admin MCP call now acts on exactly the resource its approval card names. The gated tools that take a path argument (slot_delete, model_delete, upstream_delete, stack_apply and the rest) pasted that argument into the REST URL raw. After you approved, .. climbed out of the tool's route onto a sibling route, a ? or # cut the path short (junk?x deleted junk), and a %6D escape was decoded by the server (%6Dy-model acted on my-model), so a prompt-injected agent could get an approval for a card that did not name what the call changed. Path arguments are now sent literally: each one is percent-encoded as a single URL segment, so ?, #, %, spaces, : and non-ASCII reach the server as typed. A path argument must now be a string (or an integer below 2^53): null, booleans, floats, lists and objects are refused, as are an empty value, ., .., a value containing / (only an argument its route declares {name:path} may contain / between valid segments, each checked on its own: today that is comfyui_file_delete's relpath), and a value with a control or invisible formatting character (a zero-width space or a bidi override can make a card read as one name while the value is another). They are refused with mcp.invalid_arg before any approval card exists (and again if the call is run), instead of after you approved. A missing path argument on a gated tool now returns mcp.missing_arg the same way, with no card. (#3051)

  • ComfyUI model downloads no longer spawn hf with the API process environment (they use hal0's own HTTPS downloader).

Fixed

  • The image fit check no longer refuses a large model on a bare-metal unified-memory host because its LLM slots hold the RAM. On such a host the GTT the slots hold comes out of system RAM, so a 128 GB box with about 85 GB of LLM slots loaded refused qwen-image-lightning with 422 image.wont_fit, although image mode's unload frees that memory before the render. The memory budget gains render_ram_free_gb: ram.free_gb plus the GTT the slots hold, credited only on a shared pool whose RAM reading is the host's own, capped at the pool used outside the image engine and never above the RAM limit. The fit verdicts and the /v1/images/generations preflight judge the RAM need against it, so fit.ram_free_gb carries it, and a refusal that counted it says "free with the chat slots paused". Containers, LXC guests included, are judged as before: their RAM is not charged for GTT. ram.source also reads container when lxcfs is mounted over /proc/meminfo, and the Image Gen fit meter labels its RAM figure "free for a render" like the GPU one. (#3072)

  • An LLM slot that is still loading when image mode starts no longer finishes loading beside ComfyUI. The switch into image mode unloaded only the LLM slots ready for dispatch, so a slot in a cold start or a wake-on-request was in neither the unload set nor the saved set: it came up on the GPU while ComfyUI rendered and nothing unloaded it. The switch now also takes slots that are pulling, starting or warming. It waits up to 120 seconds for a load in progress, then unloads the slot with the rest and puts it in the saved set, so the restore brings it back. A slot left in warming by a load that timed out is unloaded straight away. A load that outlasts the wait leaves the GPU in LLM mode and fails the image request with 503 gpu.slot_still_loading. unload_set on GET /api/comfyui/arbiter lists a slot still loading in LLM mode. (#3056)

  • A failed unload during the switch into image mode rolls back to LLM mode instead of leaving the LLM slot on the GPU beside ComfyUI. The arbiter records image mode before it unloads the LLM slots, so a slot whose stop outlived its budget stayed loaded while the mode said image, the ComfyUI gate let prompts through, and a render could run next to a loaded LLM until the grace period ended. Now the first failed unload reloads the slots already unloaded, puts the GPU back in LLM mode (dropping a pin) and fails the request with the unload's error; the next render tries the switch again. After an api restart in the middle of a switch, the next switch unloads any slot of the saved set that is still loaded before the engine renders, and rolls back the same way if that fails. (#3059)

  • hal0 no longer announces ComfyUI over mDNS, and no longer calls it LAN-reachable. ComfyUI listens on the box's loopback, so the _http._tcp announcement of :8188 that Announce services wrote led other machines to a refused connection. The Discovery card announces Open WebUI only (see Migrations for clearing one an earlier hal0 wrote), and the Services page shows ComfyUI no .local link while that old announcement is still on disk. The services registry, the error.details.hint of the 422 for a video, edit or upscale model (it now says to open the editor on the box itself or through an SSH tunnel) and the docs say loopback.

  • The image starter recommendation uses the guest's RAM limit, so a capped LXC guest is no longer offered a model it cannot load. On a unified-memory host a guest limited to 48 GB still read the host's 128 GB pool and was offered qwen-image-lightning in the install-time offer, in hal0 setup and in GET /api/comfyui/models; the first render then OOM-killed the image engine. The pick now follows the smaller of the pool and the guest's RAM limit (ram_mb or cgroup_max_mb), with the same thresholds (#3057).

  • /api/comfyui/status and /logs use the right container and unit name on a box with id-keyed slots. Both looked for hal0-slot@img.service and the hal0-slot-img container, but a box migrated to id-keyed slots names them after the slot's id (for example hal0-slot@10.service), so a running engine reported as absent and the log tail read nothing (Refs #2302).

  • The ComfyUI provider no longer launches with an empty extra_model_paths.yaml. It wrote an empty file whenever the installer's copy was missing, and an empty file hides every model from ComfyUI. It now writes the canonical file when the existing one is absent or blank, and leaves a file the operator filled in alone.

  • The Qwen-Image 2512 4-step workflow loads the 2512 Lightning LoRA, not the Edit one. Qwen-Image-2512-BF16-4-Step-LoRA referenced the Qwen-Image-Edit Lightning LoRA, a file the Qwen-Image install never fetched and a LoRA made for the edit model, so the workflow failed on a missing file.

  • The image capability picker was empty on AMD ROCm hosts. Each image row was checked against a llama.cpp chat profile and all of them failed. Image rows now skip that check, and the picker lists the API-renderable models of the image catalog and your own SDXL and SD 1.5 checkpoints. Applying a model that is not installed names POST /api/comfyui/models/<id>/install.

  • Loading the img slot could start a registry pull of a LoRA. The slot's model is a catalog image model such as sdxl-lightning, which the LLM registry does not know, so the load asked the registry to pull it. A ComfyUI slot load no longer goes through the registry pull; the weights come from the ComfyUI install route.

  • Choosing an image model on the shipped img slot (device = "gpu-rocm") was refused. Applying an installed image model with that device, or with none, failed with capability.illegal_backend_model_pair, because the picker's one ComfyUI row is labelled gpu-vulkan. The seeded gpu-rocm is now read as the same GPU lane.

  • openwebui.env is written in the format podman's --env-file reads. hal0 quoted values for systemd, but podman keeps the quotes, so any value that was empty or held a space, a quote, $ or # reached Open WebUI quoted, and COMFYUI_WORKFLOW_NODES arrived as a string that made Open WebUI's image settings answer 500. Values are now written as they are, and a value with a line break is refused. A value you set by hand that an older hal0 wrote in quotes keeps its quotes; remove them by hand. HAL0_OWUI_BIND_HOST is now written first and must be a plain IP address or hostname. A bind host you set by hand in quotes, or with trailing blanks, is refused: the file is left as it was, the installer warns that it failed to write openwebui.env, and later renders fail the same way, and log components.owui_env_render_failed in the hal0-api journal, until you remove the quotes or blanks by hand. (#3045)

  • Open WebUI no longer dials ComfyUI on an address the slot does not listen on. ComfyUI listens on loopback only, which the Open WebUI container cannot reach, so every image request from Open WebUI was refused. Open WebUI now reaches images through hal0's /v1, the address it already uses for chat. (#3046)

  • Open WebUI renders SDXL-Lightning and other fixed-step models at their own step count. hal0 never set IMAGE_STEPS, so Open WebUI wrote its default of 50 into the workflow. Open WebUI's OpenAI engine sends no step count, so hal0 uses the slot's default_steps or the model's own, as for any /v1 client. (#3047)

  • /v1/images/generations no longer answers 404 after the img slot goes down while the GPU stays in image mode. The idle reaper stopped img after the global idle timeout but left the GPU in image mode, so the next request answered 404 dispatch.no_route until someone loaded the slot by hand. The reaper now leaves img alone (#3050). Image mode also starts the slot whenever it finds it down, for example after a crash or a manual unload. (#3048)

  • The idle reaper no longer unloads the img slot while you work in ComfyUI's editor. It counted only requests that went through hal0, so after the global idle_timeout_s (300 s by default) it stopped img in the middle of an editor session, even with a job running. The GPU arbiter never stops the ComfyUI container once it is up (idle, it holds next to no GPU memory), but the reaper did not follow that rule. Idle-timeout, memory-pressure and pre-load eviction now never unload the img slot. idle_timeout_s does not apply to it. hal0 slot unload img still stops it by hand. (#3050)

  • The top bar's CPU cell divides RAM by the RAM limit instead of by the GPU pool. The "CPU · RAM" cell of the Slots page's telemetry bar (labelled "CPU · Memory" before) showed this box's RAM in use over the GPU pool's cap on a unified-memory box (for example 16.1 / 116 GB), a denominator that says nothing about the RAM the box may use. It now divides by this box's RAM limit and prints "/ N GB limit" under the gauge when hal0 runs in a container or under a cgroup limit, or "/ N GB" on a plain machine. When RAM cannot be read it shows no figure. The Overview page's "RAM" health cell and Settings → Overview read the same figure.

  • A capped LXC guest's RAM limit is read live and shown. The limit came from the leaf cgroup's memory.max and from the install-time hardware probe. Inside an LXC guest that file reads max, and so does a service's own file when the limit sits on its systemd slice, and the probe is a snapshot taken at install, so the cap on a guest limited to 48 GB was never shown as a limit. The RAM limit is now the smaller of MemTotal (which lxcfs sets to the guest limit) and the tightest finite memory.max or memory.limit_in_bytes on the process's cgroup chain, read on every poll.

  • The memory ruler shows other guests on a shared GPU instead of folding them into this box. Pool usage that none of this box's slots accounted for was drawn as a striped "system" block (its tooltip read "system · KV + runtime"), which on a GPU shared with another guest is that guest's memory. The ruler now draws it as "other guests", with the image engine under its own name, and takes its total from the kernel counter. Per-slot figures are estimates, and the tooltip says so; when they add up to more than the counter, the ruler scales its segments to fit the bar and the API reports the difference as attribution_excess_gb.

  • A non-string size on /v1/images/generations is a 422, not a 500. "size": 1024, a list or an object made the workflow translator raise, and the route answered 500. It now answers 422 image.invalid_size before the model is resolved or the GPU is touched, and error.details.size_type names the type that was sent as Python spells it (int, float, bool, list, dict), never the value. null and "" still mean no size, and a string the translator cannot read still falls back to the default size.

  • A /v1/images/generations request cancelled during the GPU switch no longer reaches ComfyUI. The switch can take long enough for the client to give up, and the route went on to submit the render once it ended. A client that has gone now gets an empty 499 and nothing is submitted; the arbiter restores the GPU as usual.

Migrations

  • Toggle the Discovery card once to clear ComfyUI's old mDNS announcement. A box that announced services on an earlier version keeps its hal0-addon-comfyui avahi service after the update, and other machines on the network keep resolving a ComfyUI that refuses them: hal0 no longer announces ComfyUI and does not remove the file by itself. Turn the Discovery (mDNS) card on the Services page on or off once; either removes it (turn it back on if you want Open WebUI announced). The same update also needs hal0 slot restart img once, for ComfyUI's GPU gate (see Breaking).