1. 06 Sep, 2026 7 commits
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: guarantee a host node on GPU-less / unknown-backend deployments · 97d16a0a
      Stefy Lanza (nextime / spora ) authored
      For a coderai instance that only orchestrates RunPod (no local models), the node
      that hosts the proxy must advertise the runpod capability. The no-GPU path already
      builds its engine with backend "auto" (which has it), but an explicit engine_specs
      backend:"cpu" and the unknown-backend fallback did not — those would 503.
      
      - _DEFAULT_CAPS gains "cpu": {transformers, gguf, k3, runpod}.
      - Unknown-backend fallback now {transformers, gguf, runpod} (runpod needs no local
        hardware, so every node can host it).
      
      Verified by simulating pick_engine for a runpod-pinned model on auto/cpu/nvidia
      nodes — all route instead of 503.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      97d16a0a
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: per-model boot/load timeouts (big models need longer) · 256900b7
      Stefy Lanza (nextime / spora ) authored
      The 300s boot cap was a hard-coded constant; large models need much more time for
      the image pull + weight download. Both budgets are now per-model config:
      
      - boot_timeout_s (default 300) — until the pod exposes its port; a stuck machine
        is still dropped fast and the next candidate tried.
      - load_timeout_s (default 600) — until vLLM answers /v1/models; raise for big models.
      
      Parsed in RunpodModelConfig (min 30s), used by _provision_one in place of the class
      constants (which remain the fallback), whitelisted in api_model_configure, and
      exposed on the model page with hints.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      256900b7
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: try multiple log routes (the tail= param returned HTTP 400) · d9e49200
      Stefy Lanza (nextime / spora ) authored
      Live run showed get_pod_logs hitting HTTP 400 on {rest}/pods/{id}/logs?tail=N, so a
      failed boot fell back to the console link with no output. Now tries the plausible
      routes in order (no params, ?limit=, containerLogs) and keeps the first that
      answers, reporting the collected errors otherwise. The console deep-link remains
      the always-available path.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      d9e49200
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: retry stuck-boot pods on another machine + self-diagnosing vLLM logs · 0c8417fa
      Stefy Lanza (nextime / spora ) authored
      Live testing showed RunPod sometimes ACCEPTS a pod create but the machine never
      starts (port never exposed) — 2 of 4 pods hung this way, burning the full 900s
      wait. Provisioning is now resilient and diagnosable:
      
      - Boot failure retries on the NEXT ranked GPU candidate (MAX_PROVISION_ATTEMPTS=3)
        instead of failing the request; _create_with_fallback returns the next index so
        each attempt uses a different machine/type.
      - Timeouts split + tightened: port-exposed 300s (was 900 — a pod that hasn't
        exposed its port in 5min never will), vLLM /v1/models health 600s (model load).
        Cuts worst-case billing on a bad machine from ~15min to ~5min per attempt.
      - Self-diagnosing: on any boot failure the pod's container/vLLM log is fetched and
        printed to coderai's log before teardown (the user's #1 cause is a vLLM OOM /
        launch error, only visible in that log), plus the console link.
      - Pod id + console URL are now logged immediately after create (previously only at
        "ready", so a stuck pod was untraceable).
      - Booting pods appear in pods_status/stats page as state="provisioning" with their
        console link, so a slow boot is visible live.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      0c8417fa
    • Stefy Lanza (nextime / spora )'s avatar
      runpod: fix pods /v1 path; add stale-pod reaper, multi-GPU fallback, pod logs · b911310a
      Stefy Lanza (nextime / spora ) authored
      Live-tested against a real RunPod account (Secure RTX 4090, Qwen2.5-0.5B):
      provision → boot → vLLM /v1/models health → idle auto-teardown + ledger ($0.021)
      all confirmed. Serverless + spillover proxy paths confirmed (reach RunPod, correct
      URL/auth). Fixes/additions from that run:
      
      - FIX: pods mode posted to <proxy>/chat/completions (missing /v1) → 404. The pod's
        vLLM serves under /v1 (health check hit /v1/models=200), so RunpodBackend now
        appends /v1 to the pod proxy URL (serverless base already ends in /openai/v1).
      
      - Stale-pod REAPER (safety): runpod_worker.reap_orphans() lists account pods tagged
        coderai-<deployment_id>-* and terminates any NOT tracked by a live pool (orphans
        from a crash/restart) — never touches in-flight provisions or another deployment's
        pods. Runs every ~30s in the scaler loop AND once at startup; started on the
        primary engine (where pods are hosted) via start_runpod_maintenance(). New
        config.runpod.deployment_id (default "default"); pods named with the tag.
      
      - Multi-GPU capacity FALLBACK: _rank_gpus() returns ranked candidates and
        _create_with_fallback() tries the next on a RunPod "no instances available"
        capacity error instead of failing (hit a real A5000 community capacity miss).
      
      - Pod LOGS: RunpodClient.get_pod_logs() (best-effort REST) + pod_console_url deep
        link; admin GET /admin/api/runpod/pod-logs; RunPod stats page gets a per-pod
        "view logs" modal + console link. PodHandle/pods_status carry console_url.
      
      - Settings: deployment_id field.
      
      Built coderai:base=5cffe23d1d20. Relaunch to activate (running image had the /v1
      bug + no reaper). After relaunch, a pods chat returns a real completion.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b911310a
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase3-5): pod pool + scaler + cost ledger, budgets, global-gate bypass, spillover, stats UI · 7649c9c2
      Stefy Lanza (nextime / spora ) authored
      Completes the RunPod backend for testing.
      
      Pods pool (runpod_worker.py):
      - RunpodPodPool: provision via _select_gpu (cheaper=lowest $/hr, faster=SECURE+
        throughput, across allowed pools, spot-aware) → create_pod → wait_ready →
        vLLM health-check; least-loaded per-request acquire, provision-on-demand up to
        max_pods; background scaler thread tears down idle pods to min after
        idle_timeout_s, keeps min warm, reaps. get_pod_pool registry + pods_status().
      - Budget enforcement at provision: global $/hr rate cap + per-model & global
        cumulative spend caps (rolling period) via the ledger; refuses over budget.
      
      Cost ledger (runpod_ledger.py): persistent per-model + global spend, trailing
      rolling windows (hour/day/week/month), totals() for stats. Final pod cost booked
      on teardown; live pod cost added on top for enforcement/stats (no double count).
      
      Backend (backends/runpod.py): pods mode wired to the pool (per-request acquire/
      release; async stream acquires off the event loop); serverless unchanged. Config
      adds global_cost_limit_usd + global_cost_period.
      
      Global-gate bypass (app.py _model_capacity): runpod models get an effectively
      unlimited LOCAL queue cap — remote GPUs aren't bound by the local concurrency
      gate; RunPod's max_pods/workers are the real limit.
      
      Spillover (app.py): a local model can burst to a RunPod SERVERLESS target when
      local can't serve it — front reverse-proxies to the serverless OpenAI URL, guarded
      to fall back to local on any error. Wired at the direct path for on_no_gpu
      (no engine) + on_concurrency_full (queue full). NOTE partial: broker path,
      streaming-concurrency, pods-target and on_local_error triggers are follow-ups.
      
      UI: dedicated /admin/runpod stats page (pods, live $/hr vs caps, spend by period +
      model) + nav link; Tasks page RunPod box; Settings split into category tabs
      (JS-only, IDs/save untouched) + global spend cap fields.
      
      Verified: imports in-image (8b16914ee24b), ledger + GPU selection (cheaper/faster)
      unit-tested, all templates Jinja-parse. Live RunPod calls (provision/serverless)
      still need an end-to-end test with the user's account.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7649c9c2
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase2): serverless request path + per-model config + no-local-download · 17604ab4
      Stefy Lanza (nextime / spora ) authored
      End-to-end serverless: a model tagged backend:runpod (mode:serverless) proxies
      OpenAI chat/completions to a RunPod serverless endpoint. Runs on a remote GPU,
      holds NO local weights.
      
      - codai/api/runpod_worker.py: RunpodModelConfig + parse_model_runpod (mode/pools/
        criteria/gpu/price/scaling/cost/serverless), serverless_base_url + auth_headers.
      - codai/backends/runpod.py: RunpodBackend — thin OpenAI proxy (chat/stream, Bearer
        auth for serverless), uses_local_vram=False; pods/auto raise a clear "next phase"
        error for now.
      - manager.py: runpod in _ENGINE_BACKENDS + _runpod_name_claims (pin-only) +
        runpod_should_handle + load_model dispatch branch. _model_is_runpod() gate:
        set_default_model skips local download/cache for runpod-served models.
      - router.py + registry.py: runpod is an explicit-pin capability; added to every
        engine's default caps (no local VRAM → any node hosts the proxy; prefers primary).
      - admin routes: whitelist per-model `runpod` + `runpod_spillover` blocks; skip
        local-VRAM estimate for runpod models.
      - models.html: RunPod backend option + full per-model sub-form (mode w/ explainer,
        serverless endpoint, served model, pools/criteria/gpu/price/spot, min/max pods,
        idle timeout, image/disk/port/env, spend limit+period) + prefill/collect.
      
      Serverless verified wired in-image (ef1fd7a3802c). Pods pool/scaler, cost ledger,
      spillover routing, and stats UI are later phases.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      17604ab4
  2. 05 Sep, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      runpod(phase1): account config + GraphQL client + admin key/GPU-catalog plumbing · 7c057af2
      Stefy Lanza (nextime / spora ) authored
      First slice of the RunPod remote-GPU backend (rent cloud GPUs, proxy inference).
      Dormant unless enabled — no request path yet.
      
      - config.py: RunpodConfig (enabled/api_key/cloud_type/api_base/rest_base/
        serverless_base/default_gpu_type/data_center/global_max_hourly_usd); registered,
        loaded, serialized. get_active_runpod_config() in manager.py.
      - codai/api/runpod_client.py: GraphQL client — ping, list_gpu_types (+prices),
        pick_gpu (cheapest under a $/hr ceiling, VRAM filter, spot-aware), create_pod
        (on-demand + interruptible/spot), get_pod, stop/terminate, list_pods, wait_ready.
        API key via ?api_key=, masked in all error strings.
      - admin: settings GET/save runpod block (key masked, overwritten only on a real
        new value); GET /admin/api/runpod/gpu-types (key check + catalog) and
        /admin/api/runpod/status (pods + live $/hr).
      - settings.html: RunPod card (key/cloud/default-gpu/global $/hr cap/DC/advanced
        bases) + "Test key & list GPUs" button.
      
      Decisions locked: host in primary engine (Layer-2, zero local VRAM); default
      vLLM-OpenAI pod image; per-model $/hr + global budget; per-model allow_spot;
      mode pods|serverless per model. TODO (request-path phase): RunPod requests must
      bypass the GLOBAL concurrency gate only (QueueManager.max_parallel_requests).
      
      Client GraphQL field names written from docs — pending live verification with a key.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7c057af2
  3. 02 Sep, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      audio(whisper-server): convert mp4/any input with our ffmpeg (fix stuck-queue hang) · 717ab1ad
      Stefy Lanza (nextime / spora ) authored
      whisper-server models (whisper0/1/2/3) relied on whisper.cpp's --convert,
      whose ffmpeg call deadlocks on mp4 (undrained pipe) and hangs forever,
      head-of-line blocking the single-threaded server so every later request
      queues (app reported jobs "stuck in queue"; found a --convert ffmpeg stuck 4h+).
      
      - WhisperServerManager.start(): drop the --convert flag.
      - add _to_wav_16k_mono(): ffmpeg via subprocess.run (drains stdout/stderr,
        no pipe deadlock) + 120s timeout; transcribe() converts before POST so
        whisper.cpp only ever receives clean 16k mono wav. Bad file fails fast.
      - keep_resident honored for whisper-server models (vram group membership).
      
      Verified on f595eb2a6ece after relaunch: whisper3 + mp4 -> 200 (was infinite
      hang); wav control + repeat mp4 return sub-second (no head-of-line stall).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      717ab1ad
  4. 01 Sep, 2026 6 commits
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper long-form overlap windowing (no seam drops) · b6f6ca9e
      Stefy Lanza (nextime / spora ) authored
      Contiguous windows dropped words at each ~28s seam (Whisper's weak edge context +
      words straddling the boundary). Now use overlapping windows (29s wide, 24s hop =
      5s overlap), advancing by the hop and transcribing each full window, then
      de-duplicate the overlap by keeping only words that start at/after the last kept
      word's end, and rebuild the transcript from the de-duped words. Verified on an
      ~11min clip: 0 gaps >2s across all seams, continuous word timestamps to 673s,
      recovered the previously-dropped seam words.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b6f6ca9e
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper long-form windowing + worker self-heal · b0483e8c
      Stefy Lanza (nextime / spora ) authored
      - Long-form fix: the HF pipeline's internal word-timestamp stitching crashes at
        the ~30s chunk boundary for CrisperWhisper (IndexError, dim-2 index==size). The
        service now windows manually — pipeline built without chunk_length_s; audio read
        via soundfile, split into <=28s windows (each a single Whisper pass), per-window
        word/segment timestamps offset by window start and concatenated. Verified on an
        ~11min clip: 1877 words, continuous timestamps to 673s, HTTP 200.
      - Self-heal: _RemoteCrisperWhisperBackend + _RemoteNemoBackend re-ensure the
        worker and retry once on a ConnectionError, so a restarted/killed worker no
        longer leaves a stale cached service_url in acquire_stt_backend.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b0483e8c
    • Stefy Lanza (nextime / spora )'s avatar
      audio: CrisperWhisper support (isolated venv, verbatim + word timestamps) · b069137b
      Stefy Lanza (nextime / spora ) authored
      CrisperWhisper does verbatim ASR with precise word timestamps, but its custom
      generation config crashes on the server's transformers 5.x. Run it in its own
      venv worker (pinned torch 2.5.1+cu124 + transformers 4.46.3 — last pre-5.x with
      working Whisper word timestamps and a py3.13 tokenizers wheel):
      - codai/api/crisperwhisper_worker.py + tools/crisperwhisper_service.py
        (return_timestamps="word" + CrisperWhisper pause redistribution).
      - requirements-crisperwhisper.txt.
      - stt_backends.py: `crisperwhisper` is its own family -> _RemoteCrisperWhisperBackend
        (HTTP to the worker); generic `whisper-hf` still runs in the shared venv
        (text-only default + graceful timestamp fallback); HF-ASR path now converts any
        container (mp4/m4a/webm) to 16k mono wav first and uses model-correct
        return_timestamps (Whisper True/"word", CTC "chunk"/"word").
      - transcriptions.py: crisperwhisper dispatched as a GPU worker.
      - manager skip-download + admin backend option for whisper-hf/crisperwhisper.
      
      Verified: mp4 -> verbatim text + per-word timestamps, HTTP 200, on the 3090.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      b069137b
    • Stefy Lanza (nextime / spora )'s avatar
      audio: FP16 wav2vec2 on GPU (halves VRAM) · d51f46d2
      Stefy Lanza (nextime / spora ) authored
      Load the wav2vec2/HF-ASR pipeline in FP16 on CUDA (~3GB -> ~1.5GB), FP32 on CPU,
      with a fallback to FP32 if a checkpoint rejects half precision; overridable via a
      `dtype` config field. Lower the wav2vec2 VRAM reservation default 3->2GB to match.
      Enables co-residency of the full transcription/diarization/speaker stack with a
      14B LLM on one 24GB 3090 (measured ~14.5GB used with a gemma-4-14b @8k sibling).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      d51f46d2
    • Stefy Lanza (nextime / spora )'s avatar
      audio: sliding-window speaker embeddings (window/step) · ed8c014d
      Stefy Lanza (nextime / spora ) authored
      /v1/audio/speaker-embeddings gains window (seconds) + step (default = window):
      returns one embedding per sliding window, each tagged start/end, instead of a
      single whole-file embedding. Implemented in the pyannote worker's /embed via
      _crop_wav over the cached embedder; client + endpoint pass the params through;
      whole-file mode and enroll/identify/verify are unchanged.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      ed8c014d
    • Stefy Lanza (nextime / spora )'s avatar
      audio: STT backends, diarization, speaker embeddings/ID, word timestamps · 7e5a925c
      Stefy Lanza (nextime / spora ) authored
      Transcription (/v1/audio/transcriptions):
      - Route multipart uploads by peeked model (front _peek_model) so they land on
        the model's assigned engine instead of scattering by capability.
      - New pluggable STT backends (codai/api/stt_backends.py): wav2vec2 (HF), vosk
        (CPU dirs), NVIDIA NeMo-Canary (isolated venv worker) + faster-whisper/whisper.
      - Per-model language selection + validation; Canary source->target translation.
      - OpenAI timestamp_granularities[]=word: top-level words[] in verbose_json
        (vosk/wav2vec2/faster-whisper/canary).
      - VRAM-eviction tracking (acquire_stt_backend) + keep_resident co-residency for
        the small set; /v1/models surfaces languages + backends.
      
      Diarization (/v1/audio/diarization + diarize=true):
      - pyannote.audio isolated venv worker (codai/api/pyannote_worker.py,
        tools/pyannote_service.py) on the 3090; speaker-labeled segments; graceful
        degradation. Pinned torch/pyannote/hf_hub/matplotlib in requirements-pyannote.txt.
      
      Speaker embeddings + recognition:
      - /v1/audio/speaker-embeddings (ecapa via speechbrain, pyannote/wespeaker).
      - Enroll/list/delete + identify/verify (codai/api/speaker_registry.py); persistent
        voiceprint store; diarization identify=true relabels turns with enrolled names.
      
      New isolated-venv requirements: requirements-nemo.txt, requirements-pyannote.txt
      (+ vosk in requirements.txt).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      7e5a925c
  5. 27 Aug, 2026 7 commits
    • Stefy Lanza (nextime / spora )'s avatar
      vram: don't ratchet down a model's learned GPU-layer split under contention · f54428cf
      Stefy Lanza (nextime / spora ) authored
      record_vram_delta persisted measured_n_gpu_layers from every load, including
      VRAM-CONTENDED ones (another model resident → llama.cpp fit fewer layers). Since
      _learned_n_gpu_layers reuses that as a hard cap for auto (-1) configs, one unlucky
      contended load pinned a model half on CPU permanently — it never recovered even
      when the card was later free.
      
      Observed on the gemma-4-26B-A4B 32k config (alias lisa-32k): a contended load
      ratcheted 31→20 GPU layers and stuck, spilling ~5.8 GB of weights + KV to CPU on
      a 3090 with 11 GB free.
      
      Fix: in record_vram_delta, never overwrite a HIGHER known-good measured_n_gpu_layers
      with a LOWER value from an offloaded (CPU-spilled) load. A genuinely-too-big model
      still settles at its free-card maximum on an uncontended load, so keeping the higher
      value is safe; worst case is one offload-retry on a later truly-contended load.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      f54428cf
    • Stefy Lanza (nextime / spora )'s avatar
      logging: durable request-debug sink + non-silent engine log pump · ced55385
      Stefy Lanza (nextime / spora ) authored
      Fixes "embedding request debug on radeon disappeared" — a real regression
      where the radeon engine stopped emitting ALL log output the instant it
      finished loading its GGUF embedders (~13s after launch), while still serving,
      so its FULL REQUEST DEBUG + thermal debug vanished with no trace.
      
      Two changes:
      
      1) codai/api/log.py — the log_requests middleware now ALSO appends each
         FULL REQUEST DEBUG block, from the engine's own process, straight to
         <dir(CODERAI_LOG_FILE)>/requests.log via a single atomic O_APPEND write
         (tagged [ts][engine] from CODERAI_ENGINE_NAME). This survives the engine's
         stdout stream dying, so request bodies are captured regardless. Concurrent
         engines never interleave mid-block.
      
      2) codai/frontproxy/engine_supervisor.py — _pump_logs no longer dies silently:
         a bad line can't kill the pump, and on stream end it prints
         "[pump] log stream ended (<stdout EOF|pump error>); engine still ALIVE/exited"
         so a vanishing engine log stream is diagnosable instead of looking like
         "debug turned off".
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
      ced55385
    • Stefy Lanza (nextime / spora )'s avatar
      config(multi-config): fix GGUF dispatch + per-config-id measured persistence · f9f2a373
      Stefy Lanza (nextime / spora ) authored
      Follow-up to the same-path multi-config runtime fix. Two more path-keyed spots
      broke a same-file sibling addressed by its alias (e.g. lisa-32k):
      
      1) GGUF dispatch: _resolve_local_gguf matched entries by path/basename only, so
         a sibling addressed by alias wasn't recognized as GGUF and fell to the HF
         loader ("lisa-32k is not a valid model identifier"). Now also match the
         entry's alias.
      
      2) Measured footprint cross-contamination: persist_model_field matched entries
         by path, so one config's measured_vram_gb/n_gpu_layers overwrote the sibling's
         — the small-ctx variant inherited the large-ctx footprint (34 GB) and still
         offloaded to CPU. persist_model_field now targets by config_id when given;
         record_vram_delta passes cfg['config_id']; _model_cfg carries config_id/
         config_name so the config knows its own entry identity.
      
      Net: lisa (178K) and lisa-32k (32K) are now fully independent — distinct config,
      GGUF load from the shared file, and separate measured footprints.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      f9f2a373
    • Stefy Lanza (nextime / spora )'s avatar
      config: honor same-path multi-config at runtime (key siblings by alias) · 6ec4af1c
      Stefy Lanza (nextime / spora ) authored
      The web UI stores sibling configs for one model file (distinct config_id, e.g. a
      smaller-context variant), and the front already routes by alias (_route_key:
      "two configs of the same model with distinct aliases can be assigned to
      different engines"). But engine startup keyed every text-model config by path
      (_model_id -> path), so a second same-path entry OVERWROTE the first —
      BOTH aliases then resolved to the last-registered config (verified: lisa and
      lisa-32k both got n_ctx=32768, silently changing the primary too).
      
      Fix: in main.py text-model registration, key the PRIMARY config by path (stays
      path-addressable) and each SIBLING (same path, already seen) by its ALIAS, so
      the two configs are distinct and each alias resolves to its own n_ctx. A sibling
      without an alias is skipped (can't be addressed). The GGUF loader already reads
      the file from cfg['path'], so an alias-keyed sibling still loads the right file.
      Verified: lisa->178000, lisa-32k->32768, same underlying .gguf.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      6ec4af1c
    • Stefy Lanza (nextime / spora )'s avatar
      rerank: resolve model via embedding category to pass manager type-validation · b72896e5
      Stefy Lanza (nextime / spora ) authored
      The reranker is registered in embedding_models, so request_model(model,"rerank")
      was rejected ("is a embedding model and cannot be used for rerank generation").
      Resolve with model_type="embedding" (its actual category) so validation passes;
      rerank.py still loads it as a cross-encoder (AutoModelForSequenceClassification),
      re-loading if a cached embedder object is found under the same key.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      b72896e5
    • Stefy Lanza (nextime / spora )'s avatar
      deps: bump bitsandbytes to >=0.50.2 (fix 4-bit load on transformers 5.12) · 28d7711d
      Stefy Lanza (nextime / spora ) authored
      bitsandbytes 0.49.2's Params4bit.__new__ has no **kwargs and rejects the
      _is_hf_initialized kwarg that transformers 5.12 passes during 4-bit weight
      init, so every load_in_4bit model crashed with:
        Params4bit.__new__() got an unexpected keyword argument '_is_hf_initialized'
      In production this turned a single default-model chat request (Qwen3.5-9B,
      load_in_4bit) into a ~2h internal 60-retry storm. bnb 0.50.2 adds **kwargs to
      Params4bit.__new__ (absorbs _is_hf_initialized); verified end-to-end: a
      transformers load_in_4bit load now succeeds on cuda:0 (torch 2.11/cu130), and a
      GPU 4-bit quantize/dequantize round-trips. Image rebuilt with bnb 0.50.2.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      28d7711d
    • Stefy Lanza (nextime / spora )'s avatar
      rerank: add /v1/rerank cross-encoder endpoint + surface OCR in /v1/models · e44d41ac
      Stefy Lanza (nextime / spora ) authored
      Reranker (e.g. BAAI/bge-reranker-v2-m3):
      - new codai/api/rerank.py: /v1/rerank scores query↔document pairs with a native
        AutoModelForSequenceClassification cross-encoder (no new dep), cached +
        VRAM/thermal-managed via request_model like embeddings. Returns
        {index, relevance_score(sigmoid), document?} sorted desc, top_n honored.
      - router: /v1/rerank added to _INFERENCE_PATHS so the front routes it by model
        capability (transformers → nvidia engine), like /v1/embeddings.
      - app: include rerank_router.
      - capabilities: add `reranking` + `ocr` fields; detect 'rerank'/'cross-encoder'
        names as reranking.
      
      /v1/models discoverability:
      - list_models() now appends enabled OCR engines (paddle/doctr/surya) as
        type='ocr' entries (a node subsystem invoked at /v1/ocr?engine=<id>), so
        clients can discover them.
      - a reranker registered in models.json (embedding_models, capabilities
        ['reranking']) surfaces automatically via list_models, like bge-m3.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      e44d41ac
  6. 26 Aug, 2026 4 commits
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): vectorize sparse/colbert aggregation on GPU · 59f6ebe0
      Stefy Lanza (nextime / spora ) authored
      The per-token Python loops that built the sparse {token_id: weight} dict and
      the colbert vector lists held the GIL between forward passes, starving the GPU
      (steady ~10-36% util with brief bursts). Replace them with GPU tensor ops:
      
      - sparse: relu(sparse_linear·h) with invalid positions (specials/padding)
        zeroed, then max-pooled per token id via a single scatter_reduce(amax) into a
        (B, vocab) matrix; per-row nonzero extraction is O(nnz), not O(L).
      - colbert: one C-level tolist per row over its valid tokens (vs per-token).
      - valid mask computed on-device via torch.isin.
      
      Output byte-matches the previous per-token logic (dense/sparse/colbert within
      1e-4). Benefit grows with batch size, so larger client batches translate to GPU
      work instead of Python-loop time.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      59f6ebe0
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): reliable sparse_head resolution via cache glob · 8fb4f85a
      Stefy Lanza (nextime / spora ) authored
      hf_hub_download(local_files_only=True) intermittently failed to locate the
      cached sparse_linear.pt inside the engine process, so _is_bge_m3 returned
      False and bge-m3 silently degraded to the dense-only sentence-transformers
      path. _resolve_repo_file now falls back through try_to_load_from_cache and a
      direct filesystem glob of the HF cache (models--org--name/snapshots/*/file),
      which resolves reliably regardless of process context. Detection keys off the
      sparse head file rather than the model_type string. (Debug scaffolding removed.)
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      8fb4f85a
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings(bge-m3): fix loader crash + head dtype/grad · cc06016d
      Stefy Lanza (nextime / spora ) authored
      - import build_from_pretrained_kwargs inside _load_bge_m3 (it's a local import
        of _load_embedding_model, not module-level) — the NameError made every bge-m3
        load fall over so the model silently degraded to the dense-only path.
      - cast the sparse/colbert heads to the base model's dtype (bf16) so the head
        matmul over the last hidden state doesn't dtype-mismatch.
      - requires_grad_(False) on the heads (inference only) — no autograd graph per
        request.
      - log the bge-m3 backend selection at load.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      cc06016d
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings: native bge-m3 dense+sparse(+colbert) multi-vector support · 66f5540b
      Stefy Lanza (nextime / spora ) authored
      Add BAAI/bge-m3 hybrid embedding (dense + sparse lexical, optional colbert)
      without the FlagEmbedding dependency (which pins older transformers/peft and
      would conflict with the transformers 5.x stack): the sparse/colbert heads are
      plain Linear layers over XLM-RoBERTa's last hidden state, reproduced natively
      with transformers + torch already in the venv.
      
      - new 'bge-m3' embedding backend: detected by xlm-roberta model_type + a
        sparse_linear.pt head (so plain bge-large etc. stay on the dense ST path);
        loads the base model + sparse_linear.pt (+ colbert_linear.pt if present).
      - _bge_m3_encode: dense = L2-normalized CLS; sparse = max-pooled
        relu(sparse_linear·h) per token id (specials dropped) as {token_id: weight};
        colbert = L2-normalized colbert_linear·h per content token.
      - /v1/embeddings extended: request.embedding_types selects any of
        dense|sparse|colbert (overrides the model-config default); response items
        gain sparse_embedding {indices, values} and colbert_embedding. Dense stays
        in the standard `embedding` field (base64 honored). Configurable default via
        the model's embedding_types config.
      - cleanup()/eviction branch moves the base model + heads off GPU.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      66f5540b
  7. 22 Aug, 2026 1 commit
    • Stefy Lanza (nextime / spora )'s avatar
      ocr: make managed Surya-2 vLLM reclaimable by VRAM eviction · 2c419247
      Stefy Lanza (nextime / spora ) authored
      The Surya-2 engine serves its VLM via a managed vLLM subprocess
      (vllm_worker.ensure_service). That subprocess is not a manager-tracked
      model, and the OCR VRAM releaser only tore down the lightweight worker
      pools (killing the HTTP workers) — it never stopped the vLLM holding the
      bulk VRAM (gpu_memory_utilization × card). So on-request eviction saw the
      VRAM as used, found nothing it could free, and (with no CPU fallback) a
      docTR/Paddle load on the same card could OOM.
      
      - vllm_worker: add stop_service_for(cfg, model_path, served_name) (stop ONE
        service, return estimated GB freed) + is_running() helper.
      - OcrManager._release_vram now also stops the managed Surya-2 vLLM
        (_stop_surya_vllm) when surya_serve == "vllm", so the registered external
        releaser actually reclaims its VRAM. Next OCR request re-boots it via
        ensure_service. Targeted by (model_path, served_name) so an LLM vLLM
        instance is left alone.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01Mw2KQiswmD69T45fTfjKwW
      2c419247
  8. 21 Aug, 2026 10 commits
  9. 20 Aug, 2026 2 commits
  10. 16 Aug, 2026 1 commit