1. 22 Jun, 2026 40 commits
    • Stefy Lanza (nextime / spora )'s avatar
      engine: time-split chat generation (build vs prefill) to locate pre-gen latency · e501bab0
      Stefy Lanza (nextime / spora ) authored
      Direct Kilocode requests showed ~40s before the first token even though prefill
      was a cache hit (14551 prefix-match, 1 token to eval) — so the delay is before
      decode. Added timing in vulkan generate_chat_stream: logs "[timing] chat build
      (template+tokenize)" around create_chat_completion (eager chat-template render +
      tokenize) and "[timing] chat lock+prefill+first-token" around the first streamed
      token. One request now reveals whether the 40s is the gemma chat-template Jinja
      render (huge template + many tool schemas, done in Python) or lock/prefill.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      e501bab0
    • Stefy Lanza (nextime / spora )'s avatar
      perf: gate cache scans on a cheap dir-mtime signature (only re-scan on change) · 0bc9ba76
      Stefy Lanza (nextime / spora ) authored
      Capability detection is already name-based (microsecond-fast), so caching it
      wouldn't help — the real cost is huggingface_hub.scan_cache_dir() + the per-file
      size sums walking the whole cache. So instead of re-walking on a TTL, the
      cached-models/cache-stats layer now computes a cheap change-signature (the names +
      mtimes of the top-level model dirs/files — a handful of stat() calls) and only does
      the expensive scan when that signature changes. Unchanged caches return the last
      result instantly and never re-walk; a change refreshes in the background. A long
      600s backstop TTL still re-scans occasionally. Mutating ops reset the signature.
      
      Net: the models page is instant on every load and the background refresh only runs
      when models are actually added/removed/downloaded.
      
      Verified: unchanged signature → 0 rescans; changed signature → one background rescan.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      0bc9ba76
    • Stefy Lanza (nextime / spora )'s avatar
      perf: cache the cache-scans + gpu-stats so the models page paints instantly · dc3800fe
      Stefy Lanza (nextime / spora ) authored
      The models page was slow because the work itself is slow, not because of routing:
      cached-models walks the HF/GGUF cache doing per-model capability detection, and
      cache-stats walks every file summing sizes — many seconds each on a large cache.
      
      - routes.py: cached-models + cache-stats are now stale-while-revalidate (TTL 25s).
        First call computes; afterwards they return the last result instantly and refresh
        in a background thread. Mutating ops (clear-cache, delete cached model, free-disk)
        force the next refresh. The coderai-system worker pre-warms both scans on startup,
        so even the first page load is fast.
      - gpu_detect.gpu_stats(): short 1.5s TTL cache — the Tasks page polls it ~every
        second and nvidia-smi is slow on a saturated GPU, so collapse repeats.
      
      Verified: _cached returns stale instantly and refreshes in the background
      (2 underlying calls across first+expire); all modules compile.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      dc3800fe
    • Stefy Lanza (nextime / spora )'s avatar
      front: config authority — re-read config.json after a save (Phase 2) · 15e06777
      Stefy Lanza (nextime / spora ) authored
      The front is the authority for config READS (settings GET, status, queue capacity).
      A settings SAVE stays on the engine because it applies live runtime changes
      (thermal, RAM monitor, broker runtime) that only exist there and then persists
      config.json. The front now detects the config.json mtime change and re-reads it
      into self.config IN PLACE (so the supervisor/admin_data references see it), keeping
      everything it serves current without a restart. Triggered from status() and the
      settings GET handler (via an on_config_read callback) and lazily in the capacity
      helper.
      
      This is the pragmatic Phase 2: front owns/serves config; engine applies+persists;
      front stays consistent. (Fully relocating the 362-line apply logic to the front
      isn't sound — the live application must run on the engine.)
      
      Verified: front reflects an external config.json change (queue_max 6→11) with no
      restart.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      15e06777
    • Stefy Lanza (nextime / spora )'s avatar
      engine: remove dead archive handlers (front serves archive now) · fae9b6f9
      Stefy Lanza (nextime / spora ) authored
      The archive endpoints (list/get/delete/file/settings) are served by the front
      (codai/frontproxy/admin_data.py, from disk via archive_manager), so the engine's
      copies in admin/routes.py were dead. Removed.
      
      Note: the cache/download/HF handlers in routes.py are NOT removed — the
      coderai-system worker serves them through the same admin_router, so they're only
      unrouted on the engine, not dead code. Fully removing them from the engine would
      require splitting routes.py into engine vs system routers (future cleanup).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      fae9b6f9
    • Stefy Lanza (nextime / spora )'s avatar
      front: optimistic resident-model list on load/unload (front owns the list) · 18e07076
      Stefy Lanza (nextime / spora ) authored
      The front issues every load/unload, so it now updates its own per-engine resident
      set in the registry the moment the action succeeds — the models page (whose loaded
      status is front-native) reflects it immediately instead of waiting for the next
      supervisor engine-state poll. The poll still reconciles the exact keys.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      18e07076
    • Stefy Lanza (nextime / spora )'s avatar
      engine: route hot-path logs through os.write (fast_print) instead of print() · e2db1b5c
      Stefy Lanza (nextime / spora ) authored
      The engine emits a lot of debug output during generation. Builtin print() goes
      through a buffered, lock-guarded text stream — per-call overhead plus a lock that
      can serialize against other threads on the hot path. New codai/api/fastlog.py
      fast_print writes each line with a single os.write(1) syscall: unbuffered, no
      Python-level lock, atomic for pipe writes up to PIPE_BUF (log lines don't
      interleave), with a fallback to real print() for stderr/failures. Imported as
      print() in the generation hot path: api/text.py, backends/vulkan.py, backends/
      cuda.py.
      
      Verified: fast_print mirrors print's signature (sep/end/file/flush), writes to
      fd 1, falls back for stderr; all three modules compile.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      e2db1b5c
    • Stefy Lanza (nextime / spora )'s avatar
      front: /v1/models served from system worker; quant poll only while a job runs · 1b8c1c33
      Stefy Lanza (nextime / spora ) authored
      #1 /v1/models: the front fanned out to every engine's /v1/models (slow during
      generation). It now fetches the full list once from the coderai-system worker
      (config-based list_models, off the GPU engines) — added a /v1/models route to the
      system app — with a last-good cache and per-engine fallback if the worker is down.
      
      #3 quant status: the models page polled /admin/api/quantize-status (engine) every
      5s unconditionally. It now polls only while a quant job is actually running (and
      the tab is visible); startQuantize re-arms it. No idle engine hits.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      1b8c1c33
    • Stefy Lanza (nextime / spora )'s avatar
      front: cut engine load — front-native loaded-status, config reads to system worker · 858d393a
      Stefy Lanza (nextime / spora ) authored
      Fixes the models page's 6-7s stall and reduces engine hits generally:
      
      - loadCachedModels awaited /admin/api/models (engine) before painting the cached-
        models cards, so the fast list waited on the slow engine. /admin/api/models (and
        accel-presets/accel-loras/turboquant-info/model-enable/model-disable) are pure
        config reads/edits, so they now route to the coderai-system worker (it has
        config_manager + models.json) — fast, off the engine.
      
      - model-loaded-status is now served from the FRONT's own state: loaded comes from
        the registry (the front issues every load/unload; the supervisor keeps each
        engine's resident set fresh via the cheap engine-state poll) and running from the
        front's in-flight tracking. Per-model instance detail is synced from the engine
        ONLY when it's idle (inflight==0) and cached otherwise — no engine hit during
        generation. This is the "front maintains its own list, syncs when not under load"
        model.
      
      - The 1s Tasks poll now skips the engine entirely when the primary is mid-
        generation, serving the merged/synthesized list from cache + live in-flight
        tracking instead of burning the timeout.
      
      Verified: model-loaded-status returns front-native loaded/running with no engine
      call while busy; compiles.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      858d393a
    • Stefy Lanza (nextime / spora )'s avatar
      models: stop batching first-paint reads through the engine (page was slow) · ececdf83
      Stefy Lanza (nextime / spora ) authored
      The /admin/api/batch aggregator fans every sub-path to the PRIMARY ENGINE. After
      moving cache-stats/cached-models/downloads to the coderai-system worker and
      settings to the front, batching them still sent them to the (busy) engine — so the
      models page kept loading its data from the slow engine.
      
      Those endpoints now route to fast targets (front + system worker), so the batch's
      connection-saving no longer applies and was actually the bottleneck. The models
      page now loads each first-paint read directly to its correct target: settings →
      front, cache-stats/cached-models/downloads → coderai-system, only models/
      quantize-status → engine. The main model cards come from cached-models (system
      worker), so the list paints fast even mid-generation.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      ececdf83
    • Stefy Lanza (nextime / spora )'s avatar
      coderai-system: dedicated worker for cache scan / HF downloads / cache mgmt · b9bf7b18
      Stefy Lanza (nextime / spora ) authored
      Phase 1 of moving non-GPU work off the engine. The front now spawns and
      supervises a third process type — coderai-system — a lightweight (torch-free)
      worker that owns the slow, I/O-bound, GPU-irrelevant endpoints: cache scan
      (cached-models, cache-stats, cache delete), HF downloads (model-download,
      download-stream SSE, downloads, cancel), HF lookups (hf-search/files/model-info/
      model-files, ds4 defaults), and disk/bookkeeping (model-upload, model-free-disk,
      model-add-known, model-mark/unmark-download). The front routes those /admin/api
      paths to the worker instead of an engine, so they stay responsive during
      generation and survive engine restarts — fixing the models/archive-style hangs
      for the cache/download sections.
      
      Mechanics:
      - cli.py: --system-only flag. main.py: proc title coderai-system + run branch.
      - codai/system_app.py: slim FastAPI = admin_router only + session/config init +
        internal-auth gate + /healthz + /internal/engine-state + reload-config. Stays
        torch-free (the cache/download/HF handlers import no torch).
      - engine_supervisor: spawns one role="system" worker on the next internal port
        (_engine_cmd gains a mode arg), health-polls it, and excludes it from model
        assignment (it owns no models) while still queueing it model-json reloads.
      - registry: Engine.role; can_serve()==False for role="system" so it's never an
        inference target. Front excludes it from the engine tiles.
      - app.py: _SYSTEM_PATHS + _system_engine() + _proxy_passthrough() route the
        cache/download paths to the worker (long client handles JSON + SSE).
      - routes.get_current_user now validates ANY session/session_<port> cookie by
        signature, so front/engine/system (different ports) all accept one browser cookie.
      
      Engine still physically defines these handlers (now dead — front routes to the
      worker); stripping them is a later cleanup. Verified: slim app serves cache-stats
      with cross-port cookie + internal token (403 without), engine-state; front routing
      picks the worker and excludes it from inference/tiles.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      b9bf7b18
    • Stefy Lanza (nextime / spora )'s avatar
      models: live "serving a request" dot on the engine/pin tag · 41c23847
      Stefy Lanza (nextime / spora ) authored
      The front already tracks which models are in-flight (engine.active); model-loaded-
      status now returns a front-native `running` list. The models page shows a pulsing
      green dot inside each model's engine/GPU tag while that model is serving a request,
      toggled in place via a 2s poll of the fast front endpoint (no list re-render, so it
      never re-hits the slow cache scan). Dot styling lives in base.html.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      41c23847
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve archive data locally (fixes archive page hanging during generation) · 6ace8670
      Stefy Lanza (nextime / spora ) authored
      The archive page's data (/admin/api/archive[-settings], get/delete/file) was
      proxied to the GIL-busy engine, so it hung during generation. codai/api/archive.py
      is torch-free and pure on-disk, so the front now serves these directly via
      archive_manager (configured from config.archive), with disk scans run in a
      thread. The engine's archive handlers are now dead (front handles before the
      catch-all) and can be stripped in a later cleanup.
      
      Verified: list/settings/get(404) via TestClient.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      6ace8670
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve system-stats natively; strip dead page/gpu/system-stats from engine · bbff91db
      Stefy Lanza (nextime / spora ) authored
      - /admin/api/system-stats now built on the front (psutil for CPU/RAM, torch-free
        gpu_detect for GPU/VRAM) instead of poll-proxying to the GIL-busy engine, so the
        Tasks header tiles stay live during generation.
      - Removed from codai/admin/routes.py (engine), now all owned by the front: the
        dead page-GET routes (login/admin/models/tokens/users/tasks/chat/settings/
        archive/change-password GET — the front renders these), api_gpu_stats and
        api_system_stats. Kept the auth POSTs (login/logout/change-password), _tmpl,
        _do_task_cancel, build_settings_dict and api_save_settings.
      
      Verified: front _build_system_stats returns cpu/ram/gpu/vram; routes.py imports.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      bbff91db
    • Stefy Lanza (nextime / spora )'s avatar
      engine: remove dead UI/stats/data handlers now owned by the front · def78c18
      Stefy Lanza (nextime / spora ) authored
      With --single-process gone, the engine only ever runs behind the front, which
      serves these before its catch-all — so the engine copies were dead code. Removed
      from codai/admin/routes.py:
        - api_status (GET /admin/api/status) — front builds status from its own state
        - api_list_tokens/api_create_token/api_delete_token — front admin_data (auth.json)
        - api_create_user/api_delete_user — front admin_data (auth.json)
        - api_get_settings (GET) — front serves via the shared build_settings_dict
      
      Kept: build_settings_dict (imported by the front), api_save_settings (POST — the
      engine still persists/applies config), api_list_models, and the auth POSTs
      (login/logout/change-password) the front still proxies.
      
      The engine now does generation + model management only; the front owns pages,
      stats and these data reads. Verified routes.py imports and compiles.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      def78c18
    • Stefy Lanza (nextime / spora )'s avatar
      Remove the legacy --single-process mode · 7b564eae
      Stefy Lanza (nextime / spora ) authored
      coderai now always boots as the front proxy + supervised engine subprocess(es);
      the one-process "UI/API and model work in the same process" mode is gone. This
      unblocks removing the engine's now-dead UI/stats/data handlers (the engine only
      ever runs as --engine-only behind the front).
      
      Removed: the --single-process CLI flag (cli.py), config.server.single_process
      (config.py + save), the proc-title branch and the run-mode branch (main.py), and
      the arg from the engine spawn filter (engine_supervisor.py). Existing config.json
      files with a stale single_process key are tolerated (_dc ignores unknown keys).
      
      Verified: argparse rejects --single-process; --engine-only still parses.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      7b564eae
    • Stefy Lanza (nextime / spora )'s avatar
      front: always serve status/stats natively (no engine); track activity on front · b821f87c
      Stefy Lanza (nextime / spora ) authored
      Per "always use the front when it can serve it; the engine only generates and
      gets stats from the front": /admin/api/status no longer calls the engine at all.
      It's built entirely from front state — registry (loaded models, in-flight),
      config (backend/load_mode), models.json (enabled models/aliases), live torch-free
      GPU stats (VRAM) and psutil (system RAM). Recent activity is now tracked on the
      front (it relays every request) via a ring buffer recorded in both the direct
      proxy and broker dispatch paths, so the Overview activity table is front-native
      too. Removed the now-dead _overlay_engine_totals/_cached_status/_status_cache.
      
      Engine-side removal of the now-unused handlers (api_status, tokens/users CRUD,
      api_get_settings) is pending a decision on the legacy --single-process mode,
      which still serves the UI from the engine app.
      
      Verified: _native_status() returns vram+ram+recent_activity via standalone test.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      b821f87c
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve Overview status from front-native state during generation · 182aa69a
      Stefy Lanza (nextime / spora ) authored
      Overview's only call (/admin/api/status) was proxied to the engine, so during a
      generation (engine GIL-busy) it timed out and the dashboard showed nothing.
      
      The front now builds status from its OWN state — registry (loaded models, per-
      engine in-flight), config (backend, load_mode), models.json (enabled models +
      aliases) and live torch-free GPU stats (VRAM summed across cards) — with no
      engine round-trip. It's used whenever the engine can't answer, and the front
      skips the engine entirely when the primary is mid-generation (inflight > 0) so
      Overview is instant instead of waiting out the timeout. Engine-only fields
      (recent_activity, RAM watcher) are carried over from the last good status; when
      the engine is idle the richer engine status is still used.
      
      Verified: _native_status() builds a complete payload (status/backend/load_mode/
      enabled_models/vram/requests) via TestClient/standalone.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      182aa69a
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve settings (GET) locally; engine-freeze root-cause = GIL contention · f2cde28c
      Stefy Lanza (nextime / spora ) authored
      Root-cause finding (Path B): the engine event loop is genuinely freed during
      GGUF generation (work runs via asyncio.to_thread; llama_decode releases the GIL),
      so it's not a hard-blocked loop. But under CONTINUOUS generation the per-token
      Python work in llama-cpp-python keeps re-grabbing the GIL and starves the
      engine's own API handlers (sync ones especially) — an intrinsic ceiling that
      can't be cleanly removed while generating. So per "B first, A fallback", falling
      back to A: serve page data from the front (a separate process the GIL can't
      touch).
      
      This commit: /admin/api/settings GET is now served by the front. api_get_settings
      was refactored into a pure build_settings_dict(config, gpu_cards) shared by the
      engine handler and the front (front holds the same Config), so both return an
      identical payload. Front uses its torch-free gpu_detect.gpu_cards(). POST (save)
      still falls through to the engine, which persists + applies it.
      
      Verified: build_settings_dict standalone + front /admin/api/settings via TestClient.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      f2cde28c
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve tokens & users data locally from auth.json (no engine) · a31b7863
      Stefy Lanza (nextime / spora ) authored
      Pages opened from the front but their DATA was still proxied to the engine, so
      while a generation ran (engine event loop GIL-busy) Tokens/Users/etc. showed no
      data. Tokens and users are backed purely by auth.json under config_dir, which the
      front already has — so it can answer them from disk with zero engine round-trip.
      
      New codai/frontproxy/admin_data.py (register_admin_data): front handlers for
      GET/POST/DELETE /admin/api/tokens and /admin/api/users, mirroring the engine
      handlers exactly (same shapes/status codes), with sessions validated locally
      against the shared secret and admin gating. Registered before the catch-all so
      they're served here, not forwarded. The engine still reads auth.json fresh for
      bearer/session auth, so tokens created via the front work for inference.
      
      This fully unblocks the Tokens and Users pages during generation. Settings/
      status/models-list are the next candidates (config + models.json + registry are
      all local to the front too).
      
      Verified via TestClient: full CRUD, 401/403 gating, and pass-through isolation.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      a31b7863
    • Stefy Lanza (nextime / spora )'s avatar
      front: batch page-load reads into one request; bound admin-API GETs · cd24ff1f
      Stefy Lanza (nextime / spora ) authored
      The Models page fired ~10 /admin/api reads on load. Browsers cap ~6 connections
      per host, so during a generation (engine event loop GIL-busy) the proxied reads
      pile up, saturate the limit and freeze the whole page behind them.
      
      Two fixes:
      
      1. New front endpoint POST /admin/api/batch: fans out several engine GET reads
         CONCURRENTLY server-side (front's own un-capped pool), each individually
         bounded, and returns them in one response. A slow/blocked sub-call returns an
         error marker without holding up the rest. base.html gains bootstrapPage() +
         pageFetch(): the page batch-loads its first-paint reads in ONE request, and the
         existing loaders transparently serve from the bundle (falling back to a normal
         fetch on miss). models.html now collapses settings/cache-stats/cached-models/
         models/quantize-status/downloads into a single batched call, plus an in-flight
         guard on the downloads poller so ticks can't stack up.
      
      2. Bound /admin/api/* GET proxying (catch-all) with a finite read timeout +
         graceful 503 fallback, so a busy engine can never hang a page forever (HF hub
         lookups and streaming endpoints keep the unbounded path).
      
      Verified: batch unit test (concurrent aggregation, error markers, path filtering,
      auth) and node syntax-check of the bootstrap script.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      cd24ff1f
    • Stefy Lanza (nextime / spora )'s avatar
      front: own the generation queue (per-model admission gate) · e7001716
      Stefy Lanza (nextime / spora ) authored
      Move request queuing from the engine to the front so the engine does only
      generation. New codai/frontproxy/reqqueue.py FrontQueue: a per-model concurrency
      gate sized to the model's max_instances (per-model from models.json, else the
      global models.max_model_instances). A text-generation request acquires a slot
      before dispatch; when all slots are busy it waits (FIFO) and shows as "queued"
      with its position on the Tasks page; beyond server.queue_max_size waiting it gets
      HTTP 503. A client disconnect while queued cancels the wait and drops it from the
      queue with no slot leak.
      
      Wired into both dispatch paths in app.py (direct proxy + broker route) so direct
      and brokered requests share one queue, scoped to text generation only (images/
      audio/embeddings pass through unqueued). Slots are released on stream completion,
      send failure, or disconnect. Queue key is the canonical model id so all aliases/
      paths of a model share one gate. Front never over-subscribes the engine, so the
      engine's own queue stays a safety net that effectively never fills.
      
      The engine-side QueueManager + KV-affinity instance pool are intentionally left
      in place for now (trimming them is a follow-up once this is verified live).
      
      Verified: FrontQueue unit tests (capacity, FIFO hand-off, QueueFull, cancel
      cleanup, position, capacity>1) and a FrontProxy integration test (capacity
      resolution, alias-key collapsing, queued task surfacing).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      e7001716
    • Stefy Lanza (nextime / spora )'s avatar
      front: serve admin/Studio UI pages locally (engine = generation only) · a4eb2cc7
      Stefy Lanza (nextime / spora ) authored
      The front reverse-proxied every page navigation to the primary engine, so
      opening any page while that engine was mid-generation (event loop busy) made the
      whole UI hang until generation finished.
      
      Now the front renders the UI pages itself (dashboard, models, tokens, users,
      tasks, settings, archive, chat, login, change-password) and serves the admin
      static assets + favicon locally. Sessions are validated locally: the cookie is
      HMAC-signed with the secret under config_dir (shared via auth.json), so the front
      authenticates it with no round-trip to a busy engine — and matches the
      port-derived session_<port> cookie name by signature, not by exact name.
      
      Mutating auth actions (login/logout/change-password POST) and all /admin/api/*
      data calls still fall through to the catch-all proxy and reach the engine.
      
      New: codai/frontproxy/ui_pages.py (register_ui_pages), wired in build_app before
      the catch-all. Verified via TestClient: local render, unauth->login redirect,
      authed render, static, and POST/logout pass-through.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      a4eb2cc7
    • Stefy Lanza (nextime / spora )'s avatar
      front: queue engine reload-config until the engine is idle · 0a3a2836
      Stefy Lanza (nextime / spora ) authored
      When models.json changes, the front recomputed the per-engine assignment and
      immediately POSTed /internal/reload-config to every engine. If an engine was
      mid-generation it was GIL-busy, so the push blocked and timed out, stalling the
      front's poll thread ("reload-config push to 'nvidia' failed: timed out").
      
      Now the front updates its local router instantly but queues the reload push per
      engine, and _flush_pending_reloads() delivers it only once that engine's
      inflight count drops to 0 (generation finished). Failed pushes are requeued.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      0a3a2836
    • Stefy Lanza (nextime / spora )'s avatar
    • Stefy Lanza (nextime / spora )'s avatar
      front: keep the Tasks token count live by overlaying the front's SSE counter · ffb47992
      Stefy Lanza (nextime / spora ) authored
      On the Tasks page a running generation's token count (step) appeared frozen and
      only refreshed every several seconds, while the speed kept moving. Cause: the
      primary engine's task list is frequently served from the front's last-good cache
      (when the engine is briefly busy), so the task's step froze — but rate, a
      cumulative average over growing elapsed time, kept drifting, so it looked like
      "only the speed updates". The front is itself relaying the SSE stream and counting
      tokens per request (engine.active, refreshed ~2×/s by the streaming proxy), but
      that live count was deduped away in favor of the stale real task.
      
      Fix: in _merge_engine_tasks, overlay the live in-flight step/rate (from
      engine.active) onto the matching running task before the synthetic-task dedup, so
      the count stays live regardless of how stale the engine's own task snapshot is.
      Match on (engine, model); fall back to the engine's sole live generation when the
      client's model string (alias/path) differs from the task's resolved name.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      ffb47992
    • Stefy Lanza (nextime / spora )'s avatar
      vulkan: add gemma stop sequences so generation halts after the tool call · ddb48b2d
      Stefy Lanza (nextime / spora ) authored
      gemma-4 GGUF finetunes don't emit a clean EOS after a tool call — they continue
      and hallucinate a fake tool result (<tool_call|><|tool_response>response:NAME{…}
      … <turn|>), wasting tokens on a result llama.cpp never runs. Add a per-model stop
      augmentation (_augment_model_stops): for gemma, inject <|tool_response>,
      <tool_response|>, <turn|> and the real <end_of_turn> as stop strings (merged with
      any client-supplied stops). llama.cpp matches and trims stop strings, so the call
      ends cleanly and the fake-response tail is never generated. Applied to both chat
      methods and the raw completion paths; non-gemma models are unaffected.
      
      Complements 5135f8f8 (parser repair of <|"|> quotes and malformed markers).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      ddb48b2d
    • Stefy Lanza (nextime / spora )'s avatar
      parser: repair gemma-4's corrupted tool-call tokens (<|"|> quotes, bad markers) · 5135f8f8
      Stefy Lanza (nextime / spora ) authored
      gemma-4 GGUF finetunes emit a corrupted tool-call scheme that broke both
      extraction and display, e.g.:
        <|tool_call>call:bash{command:<|"|>wc -l README.md<|"|>}<tool_call|><|tool_response>…
      
      Three distinct corruptions, all now handled by normalize_gemma_tool_tokens():
      - <|"|>  is the model's stand-in for a " quote — it leaked verbatim into string
        args, so a bash command became <|"|>wc -l<|"|> and failed with
        "syntax error near unexpected token |". Restored to a real ".
      - malformed open/close markers <|tool_call> / <tool_call|> (vs the canonical
        <|tool_call|>) — stripped, so the native call:NAME{…} body parses and the
        markers don't leak as text. The canonical <|tool_call|> / <|tool_call_end|>
        are deliberately left intact so Phi-style parsing is unaffected.
      - a hallucinated <|tool_response>…/<turn|> tail (the model fakes the tool result)
        — stripped, and strip_tool_calls_from_content now also drops response:NAME{…}
        spans so the fake result never reaches the user or gets executed.
      
      Wired into ModelParserAdapter + ToolCallParser (extract & strip) and
      cleanup_control_tokens, covering the streaming, non-streaming and fallback paths.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      5135f8f8
    • Stefy Lanza (nextime / spora )'s avatar
      front: keep engine status tiles visible while the primary engine is busy · 050ef3fa
      Stefy Lanza (nextime / spora ) authored
      The task page's engine tiles vanished whenever the primary (NVIDIA) engine was
      busy generating. /admin/api/engines gated on is_admin(), which validates the
      session by round-tripping to the primary engine's /admin/api/status — that call
      times out while the engine is saturated, so the endpoint 401s and the page hides
      the whole engines card. But the tile DATA (name/health/VRAM/loaded models) comes
      entirely from the front's own cached registry, not the engines.
      
      Fix: gate /admin/api/engines on a light credential check (session cookie or
      bearer present), exactly as /admin/api/gpu-stats already does and for the same
      reason. Extracted that check into Front._has_cred() and reused it for both. The
      mutating /admin/api/engines/{id}/restart keeps full is_admin validation.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      050ef3fa
    • Stefy Lanza (nextime / spora )'s avatar
      gpu: performance split respects per-card VRAM cap; harden process naming · 5b4ff469
      Stefy Lanza (nextime / spora ) authored
      Problem 1 — the per-card cap was read and printed ("VRAM capped at 5.0 GB on
      amd:...") but the PERFORMANCE split branch ignored it: it pushed the whole
      overflow onto the slow card (_overflow * adj[i]/others == all of it when there's
      one other card), so a 5 GB-capped RX 580 got ~47% of the layers and filled up.
      Fix: treat each card's (capped) free VRAM as an absolute ceiling — fill the fast
      lead card first up to its cap, then each other card up to ITS cap; the GPU budget
      is bounded by sum(caps) and the remainder spills to CPU via the n_gpu_layers
      auto-offload. RX 580 @5 GB now yields tensor_split [0.805, 0.195] (was [0.527,
      0.473]). The VRAM (proportional) strategy already used the capped values.
      
      Problem 2 — process naming: extract _set_proc_title() and ALSO re-assert it right
      before the engine binds uvicorn (defensive), with CODERAI_ENGINE_BACKEND as a
      fallback name. The per-engine CODERAI_ENGINE_NAME is verified distinct, so this
      guarantees coderai-front / coderai-nvidia / coderai-radeon regardless of timing.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      5b4ff469
    • Stefy Lanza (nextime / spora )'s avatar
      gpu: global VRAM cap is now per physical card (nvidia/radeon), auto-enumerated · f416117a
      Stefy Lanza (nextime / spora ) authored
      The global "secondary card VRAM cap" was a single number applied to every
      non-lead card. Replace it with a per-CARD map so each physical card on the
      machine can be capped independently — e.g. RX 580 → 4 GB, RTX 3090 → 20 GB.
      
      - gpu_detect: card_key() (nvidia:<uuid> / amd:<pci>) + gpu_cards() enumerating
        every physical GPU. Stable keys the front and engines both compute identically.
      - vulkan: _per_device_card_key() (parallel to _per_device_free_vram_gb, same
        llama.cpp order) + _resolve_device_caps() combining the per-model secondary cap
        (non-main devices) with the global per-card map, lowest wins per device. Applied
        in both the auto-offload pool and the auto-split ratio.
      - config: OffloadConfig.split_card_caps_gb {key: gb}; per-model
        split_secondary_cap_gb stays a single value (one cap is enough per model).
      - manager: pass _global_card_caps to the engine; keep per-model scalar.
      - settings UI: render one cap input per detected card (name + vendor), keyed by
        the stable card key; GET returns gpu_cards + saved caps, POST saves the map.
      
      Applies on the next engine (re)load (global args are read at engine startup),
      matching the previous global cap's behavior.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      f416117a
    • Stefy Lanza (nextime / spora )'s avatar
      engine: stop event-loop freeze from huge --debug dumps; name processes by role · fd715097
      Stefy Lanza (nextime / spora ) authored
      Root cause of "engine 'nvidia' not responding" during generation: in --debug
      mode the streaming response generator print()s the ENTIRE generated text (raw
      + repr), and a prompt-echoing/runaway generation makes that multi-MB. The
      engine writes stdout to a pipe drained by the front; once the pipe fills, that
      synchronous print() — running on the event-loop thread inside the SSE generator
      — BLOCKS, freezing the engine so health polls time out and the front flips it
      to "not responding" (seen ~3s into each debug flood in debug.log).
      
      - api/text.py: add _clip_for_log() and bound every large debug/dump print
        (generated_text, second_pass/reasoning/final text, formatted_response,
        extracted_tool_calls) to ~4KB head+tail. Shared layer, so both engines covered.
      - engine_supervisor: enlarge each engine's stdout pipe to 1 MiB (F_SETPIPE_SZ)
        so bursts can't stall the event loop even if the pump lags briefly.
      
      Also: name processes by role — coderai-front for the front, coderai-<name> for
      each engine (CODERAI_ENGINE_NAME passed at spawn), coderai for single-process.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
      fd715097
    • Stefy Lanza (nextime / spora )'s avatar
      gpu: secondary-card VRAM cap (per-model + global), effective = lowest of the two · 82aec0f9
      Stefy Lanza (nextime / spora ) authored
      Add a cap on how much VRAM the auto-split may place on EACH secondary (non-main)
      card, so a slow second GPU (e.g. RX 580) stays lightly loaded and bottlenecks
      throughput less — the remainder stays on the fast card or spills to CPU.
      
      - config offload.split_secondary_cap_gb: global default, persisted + pushed live to
        every engine's global_args (each engine reads config at startup, so it's global).
      - per-model split_secondary_cap_gb overrides — but only to TIGHTEN: the effective
        cap is min(global, per-model) of whichever are set; a per-model value higher than
        the global is ignored.
      - applied in vulkan auto-split: caps each non-main device's free VRAM in the ratio
        (both vram & performance strategies), and in the auto-offload pool so the excess
        proactively spills to CPU instead of only reacting to an OOM.
      - UI: model config modal "Secondary card VRAM cap" + global Settings field.
      - added to _LOAD_AFFECTING so changes re-apply live on the next request.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      82aec0f9
    • Stefy Lanza (nextime / spora )'s avatar
      front: don't flag a busy-but-alive engine as "not responding" during generation · 021e41ec
      Stefy Lanza (nextime / spora ) authored
      The supervisor health-polls /internal/engine-state every couple seconds. While the
      engine is GIL-busy generating, that poll can't be answered in time and the engine
      was flipped to healthy=False — flapping out of the UI/routing mid-generation even
      though it's perfectly alive. Now a poll timeout only downgrades health when the
      PROCESS IS GONE (true death, already caught by the restart check); a timeout with
      the process alive keeps the last-known-healthy state. Also bump proxy_status_timeout
      2s→4s so transient GIL contention doesn't trip it. (Pairs with the engine-state
      VRAM cache that removed the per-poll CUDA call.)
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      021e41ec
    • Stefy Lanza (nextime / spora )'s avatar
      gpu: performance-split KV-aware fit + progressive CPU-offload retry on OOM · f1f11723
      Stefy Lanza (nextime / spora ) authored
      Two fixes for the performance split OOM loop at large context:
      
      1) The performance "fits on the fast card?" check used expected_vram_gb (WEIGHTS
         only), ignoring the KV cache + compute buffers. A ~20 GB model at 178k ctx looked
         like it fit a 24 GB card → tensor_split=[1.0,0.0] → llama_context creation OOM'd
         (KV didn't fit) in a retry loop. Add a context-scaled KV/compute estimate
         (~n_ctx/16000 + 1.5 GB, calibrated to observed ~10 GB at 178k) to the footprint
         before deciding what fits — tight enough not to over-load the slow card.
      
      2) On llama_context creation failure, progressively offload to CPU: move ~20% more
         layers to CPU and retry, up to 5 times, then fail. A slow CPU-spilled load beats
         a hard failure / crash loop.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      f1f11723
    • Stefy Lanza (nextime / spora )'s avatar
      front: fix SyntaxError in _counting_iter (bare try without except/finally) · cfd6cfd7
      Stefy Lanza (nextime / spora ) authored
      The previous commit (be9950a5) left a try: with no except/finally in the SSE
      throughput counter, breaking import of the front. Remove the stray try (the
      BackgroundTask _release already closes the upstream response).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      cfd6cfd7
    • Stefy Lanza (nextime / spora )'s avatar
      front: live it/s on the synthesized task (measure throughput from the relayed SSE stream) · be9950a5
      Stefy Lanza (nextime / spora ) authored
      The Tasks page lost the it/s indicator under load: when the engine is too busy to
      report its own task, the front showed a synthetic in-flight task with rate 0. Now
      the front counts SSE "data:" events (~one token each for chat/text) as it relays
      the stream and publishes step + rate (tokens/s, refreshed ~2×/s) onto the in-flight
      metadata, so the synthesized task shows a live it/s even while the engine can't
      answer its own /admin/api/tasks. Only for streaming 200 responses; the engine's
      real task (with its own rate) still wins via the (engine,model) dedup when reachable.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      be9950a5
    • Stefy Lanza (nextime / spora )'s avatar
      manager: canonicalize GGUF model_key to the file path (concurrent-race instance dedup) · c53ad452
      Stefy Lanza (nextime / spora ) authored
      Harden the max-instances guard against the concurrent case: request_model now
      resolves a GGUF text model to its absolute .gguf path (via _resolve_local_gguf)
      before building model_key, so every name form — bare id, basename.gguf, full path,
      or alias (e.g. "lisa") — collapses to ONE model_key. That gives a single
      self.models entry and a single instance pool, so two simultaneous first-requests
      under different forms converge on the same pool and the second queues instead of
      loading a second instance. Complements the earlier already-loaded fuzzy match
      (which only covered the sequential case).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      c53ad452
    • Stefy Lanza (nextime / spora )'s avatar
      manager: alias/bare name must reuse the loaded instance (was loading a 2nd despite max 1) · 83bc76d7
      Stefy Lanza (nextime / spora ) authored
      request_model's "already loaded?" fuzzy match compared basenames literally, so a
      bare request "gemma-…-Q4_0" missed the loaded "/AI/…/gemma-…-Q4_0.gguf" (the .gguf
      suffix differed) and a SECOND instance was loaded even with max_model_instances=1.
      A "lisa" alias resolved to the full path and matched, but the bare name didn't —
      so two requests for the same model (one via alias, one via name) ran as two
      instances instead of queueing on one. Normalize the .gguf extension (and compare
      short basenames) when matching, so every name form of one model maps to the same
      loaded instance and the second request queues.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      83bc76d7
    • Stefy Lanza (nextime / spora )'s avatar
      engine: cache VRAM in /internal/engine-state so health poll stays fast mid-generation (C) · 2d8d5a22
      Stefy Lanza (nextime / spora ) authored
      The front health-polls /internal/engine-state every ~2s. It called
      torch.cuda.mem_get_info + get_device_name on EVERY poll, touching the CUDA context,
      which can serialize behind the running forward pass and stall the handler past the
      poll timeout — flipping a busy engine to "not responding". Cache the VRAM snapshot
      (4s TTL) and device names (permanent), so mid-generation polls return instantly
      from cache instead of blocking on CUDA. (llama-cpp-python 0.3.30 uses ctypes, which
      already releases the GIL during eval, so the compute itself wasn't the blocker.)
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      2d8d5a22