1. 26 Jun, 2026 5 commits
  2. 25 Jun, 2026 10 commits
    • Stefy Lanza (nextime / spora )'s avatar
      loras: load Z-Image via ZImagePipeline (fix 4-bit text-encoder quant-state crash) · 6d83cb5e
      Stefy Lanza (nextime / spora ) authored
      QLoRA training crashed in the Qwen3 TEXT ENCODER: the unsloth build quantizes it
      too, and loading components piecemeal (AutoModel/AutoencoderKL by subfolder) left
      bitsandbytes' 4-bit quant-state unreconstructed -> "FP4 quantization state not
      initialized" / AssertionError in Linear4bit.forward.
      
      - Load all components via ZImagePipeline.from_pretrained (the proven inference
        loader, which loads each 4-bit component correctly), then LoRA-train only the
        transformer. VAE/text-encoder are encoded once then freed (4-bit modules can't
        .to('cpu'), so drop refs + free).
      - Disable gradient checkpointing for the 4-bit path: the recompute can desync bnb's
        quant-state. At 512/batch-1 the 4-bit base + activations fit without it.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      6d83cb5e
    • Stefy Lanza (nextime / spora )'s avatar
      loras: train Z-Image LoRA via 4-bit QLoRA (fast, uses the cached turbo build) · 26bdb59e
      Stefy Lanza (nextime / spora ) authored
      Training on the full bf16 Tongyi-MAI/Z-Image-Turbo was extremely slow — a ~10-min
      download plus a heavy bf16 model that doesn't fit cleanly on 24 GB. Switch _train_dit
      to QLoRA: load the transformer in 4-bit (frozen, ~4 GB, no CPU offload) and train the
      LoRA on top. This trains directly on the already-cached 4-bit (e.g. unsloth) build —
      no redirect to the full model, no download.
      
      - Load transformer with diffusers BitsAndBytesConfig (nf4); an already-4-bit
        checkpoint (embedded quant config, e.g. unsloth) is loaded as-is via fallback.
      - Enable gradient checkpointing and force the input-embedding (all_x_embedder)
        output to require grad so QLoRA grads reach the attention LoRA layers; hooks
        removed at job end.
      - Drop the quantized-base -> full-model redirect added earlier.
      
      LoRA still applies to the quantized model at inference (identical architecture).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      26bdb59e
    • Stefy Lanza (nextime / spora )'s avatar
      loras: route Z-Image to _train_dit by name; train quantized base via full model · 7e4e7eba
      Stefy Lanza (nextime / spora ) authored
      Env/fighter LoRA training for Z-Image misrouted to _train_sdxl (CLIPTokenizer
      crash: "NoneType cannot be interpreted as an integer"). Cause: _resolve_base_model_path
      returns the HF id verbatim (e.g. unsloth/Z-Image-Turbo-unsloth-bnb-4bit) when the
      model has no local `path`, so the isdir(base_path/transformer) DiT check was False
      and it fell through to the SDXL/SD15 trainer.
      
      - Detect Z-Image by NAME (id contains z-image/zimage) in addition to the
        diffusers-config class name, so an HF-id base routes to _train_dit.
      - _train_dit: when the base is a pre-quantized (bnb/nf4/4bit) build, train against
        the full Tongyi-MAI/Z-Image-Turbo instead (a 4-bit checkpoint isn't a clean
        full-precision LoRA base); the LoRA still applies to the quantized model at
        inference. Overridable via lora_train_base_model.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      7e4e7eba
    • Stefy Lanza (nextime / spora )'s avatar
      quant: fix settings showing "GPTQModel not installed" when it is installed · e72e33eb
      Stefy Lanza (nextime / spora ) authored
      is_available() reported false in the running engine even though gptqmodel + fast
      kernels import fine in a fresh process. Two causes:
      
      1. capabilities() cached a DEGRADED result (gptqmodel imported but the inner
         gptqmodel.utils.backend BACKEND import transiently came up empty, e.g. when the
         first call landed mid model-load). That empty-backends result stuck for the whole
         process life, so the settings page said "GPTQModel not installed" until restart.
         Now a degraded (available-but-no-backends) result is NOT cached — re-detect next
         call; only a clean positive or a genuine ImportError is cached.
      
      2. is_available() gated on a SPECIFIC fast kernel being detected. GPTQModel always
         has a Triton/torch fallback and picks the kernel at load, so availability now
         gates only on gptqmodel importing; backends stay informational.
      
      Also: /admin/api/quantize-capabilities re-detects live (capabilities(refresh=True))
      so the settings page never serves a stale cache.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      e72e33eb
    • Stefy Lanza (nextime / spora )'s avatar
      fix: model load/unload 401 (front auth probe); township refs honor run-page res/steps · e3d0d35c
      Stefy Lanza (nextime / spora ) authored
      1. Model load/unload (and every is_admin-gated front action) returned 401: is_admin
         probed prim.url + /admin/api/status on the engine, but that route is front-only
         (removed from the engine in def78c18), so it 404'd → never 200 → Unauthorized. Add
         an engine-side admin-gated /admin/api/whoami and point is_admin at it.
      
      2. Township reference generation (characters/environments pages — _run_regen_job and
         _run_create_profile_job) hardcoded 768x512/512x512 + 28 steps, ignoring the Run
         page. Add _ref_gen_res_steps(args) (honors keyframe_size/keyframe_steps, re-read
         from the saved config so edits apply without restart) and use it at the reference
         generators; generate_character/generate_environment now forward `steps` (the
         server already accepts it). Keyframes/match videos already honored the config.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      e3d0d35c
    • Stefy Lanza (nextime / spora )'s avatar
      loras: native Z-Image (DiT) LoRA training (_train_dit) · 1f586669
      Stefy Lanza (nextime / spora ) authored
      coderai's trainer only targeted SDXL/SD1.x U-Nets, producing unet.-prefixed LoRAs
      that silently no-op on Z-Image's DiT (ZImageTransformer2DModel, transformer. keys,
      Qwen3 text encoder) — "No LoRA keys associated to ZImageTransformer2DModel found
      with prefix='transformer'". The lora_train_base_model=SDXL workaround can't make a
      LoRA that loads on Z-Image.
      
      Add _train_dit: a native flow-matching DiT LoRA trainer for Z-Image, reusing the
      existing job/progress/checkpoint/queue infra (modeled on the Wan video DiT trainer,
      which is video-only). All deps (ZImageTransformer2DModel, ZImagePipeline, PEFT,
      Qwen3) are already in the main venv — no ai-toolkit, no separate venv.
      
      Reverse-engineered from diffusers pipeline_z_image.py so training matches inference:
      chat-template + Qwen3 hidden_states[-2] masked per-sample embed list; AutoencoderKL
      latents scaled (lat-shift)*scale; list-based transformer I/O with normalized
      timestep (1000-t)/1000; RAW target = x0-noise (Z-Image negates the model output);
      timesteps sampled from the discrete turbo schedule (set_timesteps mu-shift) to keep
      the distilled model sharp; save via ZImagePipeline.save_lora_weights (transformer.
      keys) so _apply_loras' load_lora_weights applies it.
      
      _train_lora_sync routes a Z-Image DiT base to _train_dit (detected via
      model_index/transformer config _class_name); other DiTs (Flux/SD3) still raise with
      guidance. v1 — needs one validation train; recipe + knobs in
      docs/zimage-lora-training.md.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      1f586669
    • Stefy Lanza (nextime / spora )'s avatar
      engines: cross-engine VRAM eviction for co-located GGUF/torch engines · 88063940
      Stefy Lanza (nextime / spora ) authored
      After the GGUF-isolation split, a torch engine and a gguf engine share one NVIDIA
      card but each can only evict its OWN models. So loading an image model on the torch
      engine while the gguf engine still holds a resident GGUF text model failed: local
      eviction "freed 0.0 GB ... VRAM held elsewhere" → CUDA OOM.
      
      Fix — co-located VRAM release:
      - front passes each engine its same-GPU siblings' internal URLs via
        CODERAI_COSITED_URLS (matched by identical CODERAI_ENGINE_GPUS selectors).
      - engine registers an external_vram_releaser that POSTs to each sibling's new
        /internal/evict-vram when local eviction can't free enough.
      - /internal/evict-vram → manager.release_idle_vram(): evicts all idle (non-busy)
        models and returns GB freed; busy/actively-serving models are left alone.
      
      Symmetric: the gguf engine can likewise reclaim VRAM from the torch engine's idle
      diffusers models. Driver-level free VRAM (cross-process) is re-checked after each
      releaser, so the loader proceeds once the sibling has freed enough.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      88063940
    • Stefy Lanza (nextime / spora )'s avatar
      front: route GGUF pinned to torch engine -> gguf sibling; sync cached-models refresh · ec1f7218
      Stefy Lanza (nextime / spora ) authored
      Two fixes after the GGUF process-isolation split:
      
      1. Routing: a model with engine=nvidia in its config (a hard pin) but a GGUF body
         503'd, because the torch nvidia engine dropped the `gguf` capability. pick_engine
         now falls back to the co-located "<name>-gguf" sibling (same card) when the pinned
         engine can't serve the required capability (and symmetrically for a gguf-pinned
         model needing transformers). The pin's intent — that physical card — is honoured.
      
      2. Config-save visibility: the cached-models scan is stale-while-revalidate, so the
         first read after an invalidation returned stale and refreshed in the background —
         a saved config didn't appear until a second refresh. _invalidate_cache_scan now
         sets force_sync so the next read recomputes synchronously (fresh immediately). The
         system worker invalidates on reload-config, so a save shows on the next page load.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      ec1f7218
    • Stefy Lanza (nextime / spora )'s avatar
    • Stefy Lanza (nextime / spora )'s avatar
      township: fix 404 after match delete behind reverse-proxy mount · 79bd4666
      Stefy Lanza (nextime / spora ) authored
      The post-delete redirect used `(window.ROOT_PATH||'') + '/matches'`, but
      window.ROOT_PATH was never defined, so behind the nginx /township mount it
      redirected to the bare /matches — which hits the coderai front, not the township
      tool → 404. The mount shim wraps fetch()/EventSource() to add the prefix but not
      location.href assignments. Define window.ROOT_PATH=P in the shim so the redirect
      (and any future ROOT_PATH use) resolves to /township/matches; with no prefix
      (direct access) it stays unset and the bare /matches path is correct.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      79bd4666
  3. 24 Jun, 2026 16 commits
    • Stefy Lanza (nextime / spora )'s avatar
      engines: process-isolate GGUF from torch on NVIDIA (fix CUDA-context poisoning) · 2fae0beb
      Stefy Lanza (nextime / spora ) authored
      llama.cpp's CUDA backend and PyTorch sharing one process corrupt the CUDA context:
      after a GGUF model runs on an NVIDIA card, the next torch kernel dies with
      "CUDA error: invalid argument". Seen in a township match — a gemma GGUF served a
      chat completion on the nvidia engine, then the next Z-Image (diffusers) image gen
      crashed in its Qwen3 text-encoder's sdpa mask (padding_mask.all()). Every Z-Image
      run before llama.cpp touched CUDA succeeded; the first one after crashed. Evict+swap
      in one process can't fix it — torch holds the (now-corrupted) context for the
      process lifetime.
      
      Fix: when GPUs are auto-detected and server.isolate_gguf_engine is True (default),
      each NVIDIA torch engine gets a co-located sibling gguf engine on the SAME card
      (own process -> own CUDA context). The torch engine drops the `gguf` capability and
      serves transformers/diffusers; the sibling (backend=nvidia so GGUF takes the proven
      CUDA-llama path, capabilities={gguf}) serves llama.cpp. Routing is already
      capability-based, so GGUF goes to the gguf engine and HF/diffusers to the torch
      engine. Both are real engine subprocesses, so the front's routing, VRAM/eviction and
      thermal (cooperative pause + SIGSTOP on the process group) apply unchanged — and
      SIGSTOPping the gguf engine no longer freezes the torch engine.
      
      Ignored when engine_specs is set (declare the split yourself). Disable with
      server.isolate_gguf_engine=false. Design + trade-offs (extra torch context, no
      cross-engine eviction on a shared card) in docs/gguf-process-isolation.md.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      2fae0beb
    • Stefy Lanza (nextime / spora )'s avatar
      entrypoint: alias ~/.coderai to the mounted config dir (fix HOME-based lookups) · 1b9b1588
      Stefy Lanza (nextime / spora ) authored
      Trained LoRAs, characters, environments and voices are resolved from the legacy
      HOME-based dir ~/.coderai, not from --config. In the container the user's home
      (e.g. /home/ubuntu) isn't where config is mounted ($CONFIG_DIR/coderai), so those
      lookups missed the mounted data and image-gen failed with "LoRA '<name>' not found
      on server". Symlink ~/.coderai -> $CODERAI_CONFIG_DIR/coderai so HOME-based and
      --config lookups agree, removing the need for an extra per-deployment --map. Acts
      only when safe (a symlink, missing, or an empty dir) so a user-mounted volume or
      real data is never clobbered.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      1b9b1588
    • Stefy Lanza (nextime / spora )'s avatar
      admin: refresh cached-models scan on config save (fix stale models page) · 979a93d8
      Stefy Lanza (nextime / spora ) authored
      The models page reads /admin/api/cached-models (served by the system worker),
      whose _scan_caches() merges each model's saved config (precision/quant/ctx) into
      the cards. The scan freshness check _scan_signature() only fingerprinted the
      HF/GGUF cache directories, never models.json, and model-configure never
      invalidated the scan. A config save rewrites models.json but touches no cache
      file, so the signature stayed unchanged and the scan was served stale for the
      full TTL (600s) -- the engine reloaded but the front showed old config until a
      full restart.
      
      - _scan_signature(): fold models.json mtime into the signature so any process
        serving cached-models re-scans on the next read after a save.
      - system worker /internal/reload-config: call _invalidate_cache_scan() after
        rebinding config, so the front's reload-push refreshes the scan immediately.
      - engine /internal/reload-config: same invalidation for single-process /
        system-worker-down fallback.
      
      Also adds docs/dtype-auto-selection.md (planned design: model-native dtype
      default read from the checkpoint, precision as explicit override, FA2 fp32
      guard) and ignores build artifacts (venv_build.log, .build.pid).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      979a93d8
    • Stefy Lanza (nextime / spora )'s avatar
      nvidia HF loader: parity with hf_loading (precision, flash key, buffers, RAM cap) · 8c1ea33d
      Stefy Lanza (nextime / spora ) authored
      Audit of the nvidia text loader (cuda.py) vs the shared hf_loading.py found real
      config drift — the same model config behaved differently on the text path:
      
      1. precision IGNORED — cuda.py hardcoded torch_dtype=float16. Now the manager
         passes the per-model  and cuda.py resolves it via resolve_dtype()
         (default fp16 on CUDA / fp32 on CPU when unset, preserving current behaviour).
      2. 4-bit compute dtype hardcoded float16 → now follows the resolved precision
         (_make_bnb_config takes a compute_dtype).
      3.  key was ignored — the manager only read . Now it
         honours flash_attn / flash_attention (per-model) and the global
         global_args.flash_attn (offload.flash_attention).
      4. offload_buffers was only set on the disk-spill path → now also on the GPU+CPU
         device_map ladder (with offload_folder), so CPU offload doesn't pin activation
         buffers on the GPU and OOM the forward pass.
      5. global max_ram_gb now clamps the CPU offload budget (central
         _get_gpu_memory_map_with_limit + the disk fallback), matching hf_loading.
      
      Diffusers-only items (component_quantization, GGUF components, sdcpp flash flags)
      are correctly N/A to the AutoModelForCausalLM text path.
      
      Bump version to 0.1.9.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      8c1ea33d
    • Stefy Lanza (nextime / spora )'s avatar
      hf: resolve relative offload_dir to the configured dir; forward HF_TOKEN · fe7da690
      Stefy Lanza (nextime / spora ) authored
      Loading an HF model (e.g. Qwen3.5-9B, 4-bit) failed with 'Permission denied:
      ./offload'. Cause: the model's per-model offload_dir was the relative './offload'
      (a stale auto-saved default), which _cfg_or_global lets win over the global
      config; './offload' resolves to the CWD = the READ-ONLY /opt/coderai/app tree in
      the image. The config WAS respected — a relative offload path is just meaningless
      where the CWD isn't writable.
      
      * hf_loading.resolve_offload_dir(): an absolute offload_dir is respected as-is; a
        relative/empty one INHERITS the configured GLOBAL offload directory
        (global_args.offload_dir) when absolute, then CODERAI_OFFLOAD_DIR, then the user
        cache — never the CWD. Applied in the manager (both load sites, always passed),
        hf_loading, and defensively in the cuda backend.
      * main.py + entrypoint: a container-writable CODERAI_OFFLOAD_DIR (=/cache/offload,
        created by the entrypoint) is used when the GLOBAL config is still the bare
        './offload' default; explicit config wins.
      * run_oci.sh: forward HF_TOKEN / HUGGING_FACE_HUB_TOKEN from the host env so the
        engines authenticate to the HF Hub (the 'unauthenticated requests' warning) for
        higher rate limits + gated models. HF_HOME/cache dir was already honoured
        (main.py from config.models.hf_cache_dir).
      
      Bump version to 0.1.8.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      fe7da690
    • Stefy Lanza (nextime / spora )'s avatar
      front: 'silent' wait-mode still keepalives (SSE comments), driven by the front · 595c3a9e
      Stefy Lanza (nextime / spora ) authored
      The point of the keepalive is to hold the client connection open while the engine
      is stuck loading — so 'silent' must NOT fall back to the dead legacy path (which
      sends nothing until the engine's first byte and lets short-timeout clients/proxies
      disconnect). Now ALL streaming inference goes through the front keepalive path:
      
        * silent    — SSE comment lines (': …') only: keeps the socket alive, emits no
                      chunk/content/status (invisible to event parsers).
        * invisible — empty-content chunk + x_queue_info (default).
        * visible   — visible status text.
        * thinking  — reasoning channel (when mode != silent).
      
      The front stays responsive even when the engine is GIL-blocked, so it can drive
      these regardless of engine state.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      595c3a9e
    • Stefy Lanza (nextime / spora )'s avatar
      front: wait-keepalive on the direct streaming path (queue/model-load) · 6583c394
      Stefy Lanza (nextime / spora ) authored
      Clients hitting the public API directly (e.g. township via nginx) disconnected
      during long waits: the direct proxy() path acquired the front queue slot silently
      and awaited the engine's first byte (model load) with no output. The broker path
      already kept alive; the direct path now does too.
      
      For a STREAMING inference request, commit to a 200 text/event-stream up front and
      emit keepalive while acquiring the queue slot and during the engine's model load /
      not-ready retries, then relay the real stream (token-counting for the Tasks page),
      ending cleanly if the engine dies mid-flight.
      
      Configurable mode (models.wait_status_mode, global default 'invisible'; per-model
      override via the models.json entry):
        * invisible — empty-content SSE chunk + x_queue_info (holds the connection; no
                      content pollution)
        * visible   — short visible status text (appears in the content)
        * silent    — nothing (legacy path)
      When thinking is enabled the keepalive goes on the reasoning channel instead
      (no pollution), unless mode is silent.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      6583c394
    • Stefy Lanza (nextime / spora )'s avatar
      vram: learn+persist stable offload split; front: graceful crash + task cancel routing · 223b46f7
      Stefy Lanza (nextime / spora ) authored
      manager (record_vram_delta): record the FULL footprint after a load — including
      offloaded loads (no longer skipped) — so the next load skips the OOM→move-to-RAM
      retry dance:
        * measured_vram_gb      — GPU VRAM delta + reserve (GPU portion; eviction uses it)
        * measured_ram_gb       — host-RAM delta (the CPU-offloaded layers)   [new]
        * measured_n_gpu_layers — the layer split llama.cpp settled on         [new]
        total need = vram + ram. All gated by force_vram_update (else only when
        used_vram_gb unset). New _learned_n_gpu_layers(): when n_gpu_layers is auto/-1,
        reuse the learned split so a reload jumps straight to the config that fit. Both
        load sites snapshot host RAM and read the backend's settled n_gpu_layers.
      
      frontproxy:
        * graceful streaming proxy: when an engine dies mid-stream (e.g. a CUDA
          ggml_abort SIGABRT on a VRAM-OOM decode), end the relayed stream cleanly
          instead of throwing an unhandled ASGI exception (noisy traceback).
        * task actions (cancel/interrupt/pause/resume/restart/DELETE) now route to the
          engine that OWNS the task — fan out to every live engine (each validates the
          same session cookie), first success wins — fixing 'Task not found' for tasks on
          a non-primary engine; front-only synthetic ids handled locally.
      
      Bump version to 0.1.7.
      223b46f7
    • Stefy Lanza (nextime / spora )'s avatar
      runner: pass per-tool args from coderai-docker; video editor --session on by default · b56ca99e
      Stefy Lanza (nextime / spora ) authored
      * coderai-docker (run_oci.sh): new --tool-arg TOOL VAL (one token) and
        --tool-args TOOL STR (whitespace string), repeatable, for video-editor |
        videogen | township | parler. They accumulate into CODERAI_<TOOL>_ARGS env vars
        that supervisord appends to each tool's command line. Documented in --help
        (plus the --map dir-or-file note and bring-your-config examples), and shown on
        the startup banner (tool-args:).
      * entrypoint: default+export the four CODERAI_*_ARGS (empty) so supervisord's
        %(ENV_...)s never fails on an undefined var; pre-create /cache/video_editor/sessions.
      * supervisord: append %(ENV_CODERAI_*_ARGS)s to all four tool launchers; the video
        editor now runs with --session --session-dir /cache/video_editor/sessions so
        editor-state recovery is ON by default and persists on the cache volume.
      * video_editor.py: new --session-dir to place session state/assets at a
        persistent/mapped path (defaults to ~/.cache/... when omitted).
      
      Bump version to 0.1.6.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      b56ca99e
    • Stefy Lanza (nextime / spora )'s avatar
      api: execute_api_request always routes through the front (no single-process path) · f160c1f0
      Stefy Lanza (nextime / spora ) authored
      coderai always runs as front + engines, so drop the single-process special-casing
      in execute_api_request: always hand the sub-request to the front (the single API),
      which routes it to the engine that owns the target model. Critically, remove the
      on-error fallback to in-process dispatch — that would silently run the model on the
      WRONG engine and hide a routing failure. If the front can't be located/reached it
      now returns a 502 the caller surfaces. Drops the now-unused os import.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      f160c1f0
    • Stefy Lanza (nextime / spora )'s avatar
      pipelines: route sub-steps through the front; tools use the public nginx port · 23ab750d
      Stefy Lanza (nextime / spora ) authored
      * pipelines.py: /v1/pipelines/* (image-to-video, story, video-dub, audio-dub)
        chained their steps by calling the image/video/text/TTS handlers DIRECTLY
        in-process, forcing every modality onto whichever engine received the pipeline
        request. In a multi-engine deployment each model may live on a different engine.
        Route every sub-step through the front via execute_api_request (same fix as
        characters/environments) so each lands on the engine that owns that model;
        in-process fallback for single-process mode. Drops the unused TestClient import.
      
      * supervisord.conf: the bundled tools (video_editor, videogen, township) now use
        --base-url http://127.0.0.1:8776 — the PUBLIC nginx port — so they talk to
        coderai exactly like any external client (nginx already proxies the full API
        with 4G bodies / 1h timeouts). The front's own bind stays CODERAI_PORT=18776.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      23ab750d
    • Stefy Lanza (nextime / spora )'s avatar
      api: route characters/environments reference-image gen through the front · 84f6cfbb
      Stefy Lanza (nextime / spora ) authored
      /v1/characters/generate and /v1/environments/generate produced their reference
      images by calling execute_internal_request(request.app, '/v1/images/generations')
      — an IN-PROCESS self-dispatch on whatever engine handled the request. Two bugs:
      
      1. It re-entered the engine's own _InternalAuthMiddleware without the internal
         token, so in multi-engine mode it 403'd ('engines are reachable only through
         the front proxy') — image gen failed while plain text worked (text never
         self-dispatches). Township (a normal API client) hit exactly this.
      2. Even with a token it would run the image model on the WRONG engine: the image
         model may be assigned to / pinned on a different engine than the one serving
         the characters/environments request.
      
      Add execute_api_request(): behind a front (CODERAI_INTERNAL_TOKEN set) it routes
      the sub-request THROUGH THE FRONT (the single public API) at 127.0.0.1:<front
      port>, so the front picks the engine that owns the image model and stamps the
      internal token itself; the caller's Authorization is forwarded so that engine
      authorises it like any client call. Single-process mode still dispatches
      in-process (middleware is a no-op with no token). The client never sends internal
      headers — it just talks to the one API endpoint.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      84f6cfbb
    • Stefy Lanza (nextime / spora )'s avatar
      video_editor: auto-load mapped config + --map accepts files · ac81c655
      Stefy Lanza (nextime / spora ) authored
      Mirror the township config fix for the bundled video editor:
      * supervisord: pass --config /cache/video_editor/video_editor.config.json so a
        bind-mounted (or cache-persisted) config auto-loads — previously it only
        auto-loaded video_editor.config.json from the baked cwd, which isn't mappable.
      * video_editor.py: a MISSING --config file is no longer fatal (was raise
        SystemExit) — start with defaults; the web UI Save creates it.
      * entrypoint: pre-create /cache/video_editor so the config dir exists for
        auto-load + web-UI Save (persists on the standard /cache volume by default).
      * run_oci.sh (coderai-docker): --map now accepts a single FILE, not just a dir,
        so a tool config that lives loose on the host can be linked directly, e.g.
        --map /host/video_editor.config.json:/cache/video_editor/video_editor.config.json
      
      Bump version to 0.1.5.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      ac81c655
    • Stefy Lanza (nextime / spora )'s avatar
      township: auto-load mapped township_config.json in the bundled launcher · fadc340d
      Stefy Lanza (nextime / spora ) authored
      The bundled township tool (supervisord [program:township]) launched without
      -c/--config, so a township_config.json bind-mounted via --map into
      /cache/township_output was never read — the web UI came up with blank API key /
      league upload credentials / options every restart.
      
      * supervisord: pass --config /cache/township_output/township_config.json so a
        mapped config auto-loads. Map the bare-metal dir with
          coderai-docker --map /path/to/township_output:/cache/township_output
        and the saved settings + credentials come up automatically.
      * gen_township_fighters.py: a MISSING --config file is no longer fatal (fresh
        install with no mapped dir) — fall back to defaults and start normally; a
        malformed config still errors. The web UI's Save already writes back to
        <out-dir>/township_config.json, so first-run saves persist into the mapped dir.
      
      Bump version to 0.1.4.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      fadc340d
    • Stefy Lanza (nextime / spora )'s avatar
      thermal: front-driven central supervisor with cooperative pause + SIGSTOP escalation · 707c89bb
      Stefy Lanza (nextime / spora ) authored
      The thermal guard was checkpoint-based and ran inside each engine, so it went
      blind during a single long native call (llama.cpp prefill / image encode): the
      engine can't reach a between-token checkpoint, and the [thermal][debug] heartbeat
      stops exactly when the GPU is hottest.
      
      Move the AUTHORITATIVE monitor to the front, which stays responsive regardless of
      what an engine is doing:
      
      * Front (engine_supervisor): a thermal thread reads per-card temps via gpu_stats()
        + CPU temp, maps each card to the owning engine by its CODERAI_ENGINE_GPUS
        selectors, and drives pause/resume with hysteresis (pause at *_high, resume only
        back at *_resume). A hot GPU pauses just its engine; a hot CPU pauses all.
      * Engines stop cooperatively, as before, but triggered remotely: the front POSTs
        /internal/thermal-pause; thermal.set_external_pause() makes wait_until_safe()/
        checkpoint() block at the next safe point (publishing cooldown state so the Tasks
        page shows it), until /internal/thermal-resume.
      * Escalation: if a paused engine keeps generating (inflight > 0) — stuck in a
        native call it can't interrupt — for stop_escalate_checks (default 3) consecutive
        checks, the front SIGSTOPs the engine's process group; SIGCONT on cooldown. Both
        signals target the session group so children freeze too.
      * stop_all()/restart_engine() SIGCONT a frozen engine first (a stopped process
        ignores SIGTERM until continued); _spawn() resets the thermal flags.
      * Config: thermal.supervisor_enabled (default on), thermal.stop_escalate_checks.
      * UI: per-engine temp + pause/frozen state in engines_list and the Tasks cooldown
        banner (covers a SIGSTOPped engine that can't report its own cooldown).
      
      Also: exclude coderai-runtime/*-runtime from the Docker build context (.dockerignore)
      — a root-owned runtime temp file was breaking the image build.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      707c89bb
    • Stefy Lanza (nextime / spora )'s avatar
      vram: measure multi-GPU load delta across all devices (fix split models stuck at 0) · 80b21cf5
      Stefy Lanza (nextime / spora ) authored
      record_vram_delta() measured free VRAM via _free_vram_snapshot(), which only
      read the default CUDA device (device 0). A gpu_split model (e.g. gemma on two
      cards) lands most weights on the other card, so the device-0 delta came out
      ~0, tripped the 'delta <= 0' guard, and never persisted measured_vram_gb. The
      estimate stayed 0 and every load OOM-retried from scratch even with
      force_vram_update set.
      
      Sum free VRAM across every visible CUDA device (scoped per engine via
      CUDA_VISIBLE_DEVICES), plus amdgpu sysfs when cross-pooling is on or no CUDA
      device is visible -- mirroring _get_free_vram_gb(). Before/after snapshots are
      now symmetric, so the delta captures the full multi-card footprint and the
      real value is persisted; subsequent loads size correctly and stop OOM-looping.
      
      Bump version to 0.1.3.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      80b21cf5
  4. 23 Jun, 2026 9 commits
    • Stefy Lanza (nextime / spora )'s avatar
      text: fix NameError — move 'import re as _re' to module top · 4e198169
      Stefy Lanza (nextime / spora ) authored
      The loop-guard change added a module-level _GEMMA_CALL_RE = _re.compile(...) at
      the top of the file, but 'import re as _re' sat far below (line 2266), so import
      crashed with NameError: name '_re' is not defined and every engine failed to
      start. Move the import up with the other stdlib imports (removing the late
      duplicate). Verified codai.api.text now imports cleanly.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      4e198169
    • Stefy Lanza (nextime / spora )'s avatar
      install.sh: show a progress bar while loading the image · 3af53d02
      Stefy Lanza (nextime / spora ) authored
      `docker load -i` shows no useful progress and the installer captured its output
      into a variable, so a ~12G load looked like a multi-minute hang. Stream the
      tarball through a meter into `docker load` instead — the file read tracks load
      progress closely. Prefer `pv` (bar + ETA), fall back to GNU `dd status=progress`
      (bytes + throughput), else plain load with a tip to install pv. The meter draws
      on stderr so the "Loaded image:" line is still captured. Pre-authenticate sudo
      first so its prompt doesn't collide with the bar.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      3af53d02
    • Stefy Lanza (nextime / spora )'s avatar
      text: fix streaming tool-call spill; gate native markup unconditionally; v0.1.2 · 41093571
      Stefy Lanza (nextime / spora ) authored
      The streaming content gate only ran when the request carried a tools array. But
      the AISBF relay can drop tools (as it does max_tokens), so a gemma model still
      emitting <|tool_call>call:NAME{…} from its system prompt had that markup streamed
      straight to the client as content — the spill that poisoned history and fed the
      tool-call loop.
      
      _gate_tool_content now:
        * ALWAYS withholds the unambiguous native special-token markers (<|tool_call>,
          <|tool_response>, DeepSeek DSML) and their cross-chunk partials, even with no
          tools declared;
        * gates the ambiguous <tool>…</tool> XML form only when tools are present;
        * gates the gemma-4 call:NAME{…} form by the per-model gemma_tool_parser mode
          (off: never; full: always; restricted: only declared tool names), so legit
          call:foo{…} prose/code is streamed instead of withheld+dropped.
      The gate is now invoked for every stream (not just tools-present), and the final
      flush likewise, with the mode/tool context resolved once per request.
      
      Bump version to 0.1.2.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      41093571
    • Stefy Lanza (nextime / spora )'s avatar
      parser: gemma tool-call heuristic gets a full|restricted|off mode (per-model) · c7105023
      Stefy Lanza (nextime / spora ) authored
      The gemma-4 native call:NAME{…} heuristic could eat legitimate text: the content
      stripper removed ANY call:/response:{…} span with no tool-name restriction, so a
      coding reply containing e.g. call:foo{…} in prose or a snippet got silently
      deleted; and with no tools declared the parser matched any call:word{.
      
      Add a 3-way mode, resolved per-model (models.json "gemma_tool_parser") over the
      global models.gemma_tool_parser (default "restricted"):
        * full       - parse & strip every call:/response: span (old behavior)
        * restricted - only when NAME is a declared tool; legit call:foo{…} prose/code
                       is preserved, real declared-tool calls still parse & strip
        * off        - disable the gemma heuristic entirely (bigger models that emit
                       standard structured tool calls)
      
      resolve_gemma_tool_mode() + a centralized strip_gemma_native() now gate both the
      parse (GemmaParser, via the dispatcher) and the strip (ModelParserAdapter and the
      streaming ToolCallParser, which now stashes the declared tool names from
      extract_tool_calls so 'restricted' works on the streaming finalizer too).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      c7105023
    • Stefy Lanza (nextime / spora )'s avatar
      text: add cross-turn tool-call loop guard (per-model + global) · 79ea70d6
      Stefy Lanza (nextime / spora ) authored
      A model can get stuck re-issuing the same tool call with the same arguments when
      each attempt fails (e.g. memory replace old_text=X → "No entry matched" on the
      wrong memory tier) — or when the call spills as un-parsed call:NAME{…} markup
      into assistant content. Each brokered request carries the full history, so
      coderai can see the repetition the agent's own tool-runner didn't break.
      
      _detect_tool_loop scans the recent history for a (tool, arguments) signature
      repeated >= threshold times where those attempts failed (per the tool result) or
      spilled as markup, and injects one system reminder before generation telling the
      model to stop repeating the call. Covers both structured tool_calls and spilled
      gemma-style markup.
      
      Configurable on every model: per-model models.json tool_loop_guard /
      tool_loop_repeats override the global models.tool_loop_* (default on, repeats=3);
      repeats<=0 disables.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      79ea70d6
    • Stefy Lanza (nextime / spora )'s avatar
      text: stop flattening image content to a placeholder (fixes vision over the API) · 361cba6d
      Stefy Lanza (nextime / spora ) authored
      ChatMessage.convert_content_array_to_string ran at parse time (mode=before) and
      unconditionally joined any multipart content list into a string, replacing each
      image_url part with the literal text "[image_url content]". That destroyed the
      image before text.py's vision pipeline (_vision_ok / _normalize_vision_content,
      which the backend's mmproj/MTMDChatHandler consume) ever saw it — so a vision
      model (e.g. lisa = Gemma-4 + mmproj) received only text and answered "no image
      attached", and agents looped retrying. text.py's end-to-end vision handling was
      effectively dead code because content was always pre-stringified.
      
      Now flatten only TEXT-ONLY multipart arrays (the KiloCode case); preserve the
      list whenever it carries an image_url (or any non-text part) so the multimodal
      backend receives the image. Non-vision models still degrade to the placeholder
      downstream (text.py:1439), and text-only/plain-string paths are unchanged.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      361cba6d
    • Stefy Lanza (nextime / spora )'s avatar
      front/broker: publish tokens/s for brokered streams on the Tasks page · 758d948f
      Stefy Lanza (nextime / spora ) authored
      The broker streaming relay counted tokens into engine.active["step"] but never
      set ["rate"], so _merge_engine_tasks (which overlays both onto the running task)
      showed token progression with a frozen 0 speed. Compute tok/s from the first
      streamed token (so the model-load/queue wait doesn't drag the average down) and
      publish it as m["rate"], mirroring the engine-side text path.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      758d948f
    • Stefy Lanza (nextime / spora )'s avatar
      version: bump to 0.1.1 · bd5868be
      Stefy Lanza (nextime / spora ) authored
      Ships the broker load-status / max_tokens fixes, the run-as-invoking-user
      default (+ --root opt-out), nginx console silencing, and the expanded installer
      help. --versioned builds now tag coderai:full_all_0.1.1.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      bd5868be
    • Stefy Lanza (nextime / spora )'s avatar
      packaging: run as invoking user by default, silence nginx console, richer install help · 6c87c7cd
      Stefy Lanza (nextime / spora ) authored
      Three operational fixes for the distributable image:
      
      run_oci.sh: default the container to the invoking user (uid:gid, SUDO_UID-aware)
      instead of the image's root default, with a new --root opt-out. The image has no
      USER directive and supervisord sets no user=, so a run without --user created
      root-owned dirs (logs, coderai-tmp, hf cache) in the bind-mounted /cache; a later
      --user run then couldn't write them ("cannot write /cache/logs ... logging to
      stdout only"). Running as the user by default makes a fresh install safe
      regardless of run order. --root restores the old behaviour (and throwaway config
      copy) for shared root-managed data dirs.
      
      nginx.conf: error_log crit (was info) + access_log off. nginx's [notice]
      startup/worker lines and per-request access logs were piped to the container
      console by supervisord and buried coderai's own output on attach / docker logs.
      Real failures (crit/alert/emerg) still surface.
      
      install.sh: print an extensive post-install guide (quick start, --local + --map,
      where data lives, the run-as-you default, file logging, docker logs/stop, --help).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      6c87c7cd