Files
spark-control/docs/guides/fastapi-image.md
T
Keysat 7e0759846f v0.27.0:0 - in-app settings gear + swap-lock route fix
Move the ~20 optional cluster knobs out of the StartOS "Configure Sparks"
action (now just the 4 required fields) and into a dashboard ⚙ Settings gear,
backed by a /data/app_settings.json overlay keyed by env-var names. One shared
mutable Settings instance + Settings.reload() applies edits live without a
restart; existing installs' values migrate automatically on first boot.

Also: support-service ports (parakeet/kokoro/embed/qdrant + vllm) are now
configurable, and GET /api/swap/lock no longer 404s (it was shadowed by the
/api/swap/{job_id} catch-all). WebhookNotifier is re-pointed on save so its
url/secret reload live too.
2026-06-18 13:41:28 -05:00

5.3 KiB

paths
paths
image/**

FastAPI image (image/)

Standalone FastAPI app (Python ≥3.11; ships on python:3.12-slim; UI on port 9999; vanilla HTML/CSS/JS, no framework). Python has no configured linter/formatter — match the style of the file you're editing.

Local dev (no StartOS)

cd image
python3 -m venv .venv && source .venv/bin/activate   # one-time
pip install -e .
export SPARK1_HOST=<ip> SPARK1_USER=<user> SPARK2_HOST=<ip> SPARK2_USER=<user> SSH_KEY_PATH=<private-key>
# Required outside the container — these default to paths under /data, which only exists in the image
# (missing REDACTION_MAP_DB crashes startup; missing CONNECTIVITY_LOG 500s /api/status):
export REDACTION_MAP_DB=/tmp/redaction_maps.db CONNECTIVITY_LOG=/tmp/connectivity.json
uvicorn app.server:app --host 0.0.0.0 --port 9999 --reload

Other env vars: BIND_PORT, MODELS_YAML, SSH_DIR, SSH_KNOWN_HOSTS, MODELS_OVERRIDES, SERVICES_OVERRIDES.

Tests

Two kinds, both run with the image/.venv interpreter (system python3 has no deps):

  • pytest unit suite — offline, pure functions, no cluster. .venv/bin/python -m pytest from image/. Lives in image/tests/; currently covers build_launch_command (incl. the shell-injection / shlex round-trip invariant) and the transcript↔diarizer label-merge (_merge_words_with_speakers). Install the test dep once with pip install -e '.[dev]'. Add new pure-function coverage here.
  • Standalone scripts — the redaction suites and the live-cluster audio e2e are run directly (not via pytest). See the redaction and audio rules.

Conventions

  • Pydantic request models go at module scope, never inside a build_router() body (FastAPI silently 422s otherwise).
  • New external-facing endpoints get documented in docs/ (AUDIO_API.md, EMBEDDINGS.md, REDACTION_GATEWAY.md) and noted in release notes.
  • SSH-input safety: any user-supplied value that reaches an SSH command on the Sparks MUST go through app/shellsafe.py — validate against a whitelist at the API boundary, then quote_arg/quote_args (shlex.quote) at the sink. Never raw f-string a user value into a command string. Existing sinks: models.build_launch_command, download, nim, services; disk.py keeps its own _SAFE_DIRNAME because it needs $HOME to expand server-side. The vLLM pre-flight (validate.py) relies on shlex.split cleanly reversing this quoting — preserve that invariant.
  • CSRF / same-origin: state-mutating control endpoints are guarded by the csrf_guard middleware in server.py (rejects requests whose Origin/Referer host ≠ the served host). A new endpoint meant to be called cross-origin by downstream apps (a proxy/data endpoint) must be added to _CSRF_EXEMPT_PREFIXES, or browser POSTs from those apps will 403. No app-layer token auth by design (LAN/VPN-only; would break consumers).
  • Settings split (gear vs StartOS action): only the four required fields (both Spark IPs + SSH users) live in the StartOS "Configure Sparks" action → config.yaml → env. Every optional knob (ports, container names, support-service hosts, integrations, webhook) is edited in the dashboard's ⚙ Settings gear, backed by the /data/app_settings.json overlay (app_settings.py), keyed by the same env-var names. Precedence (config._effective_env): os.environ first, overlay on top. app_settings.seed_from_env runs once at startup to migrate a pre-gear install's env values into the overlay (don't move seeding into from_env/reload — it writes, and from_env runs on every build → it would clobber across calls, which it did once already). Settings is deliberately not frozen: one shared instance is threaded by reference into every router closure/manager, and Settings.reload() (called after a gear save) recomputes its fields in place so changes apply live with no restart and no call-site changes. A new gear knob = add one entry to app_settings.FIELDS (the front-end renders it generically); the matching config.Settings field must already read that env var.

Layout

  • image/app/server.py — FastAPI entry; routers live in sibling modules (audio_proxy.py, llm_proxy.py, embeddings_proxy.py, redaction_gateway.py, swap.py, health.py, deep_health.py, connectivity.py, …).
  • image/app/discovery.py — the disk-driven model menu. /api/models lists what's actually downloaded on the Sparks (via disk.list_cached_models); models.yaml/overrides are launch recipes matched by repo, not the menu. An on-disk model with no recipe is needs_setupinfer_recipe reads its config.json to prefill a setup form the operator confirms once.
  • image/app/app_settings.py — the in-app settings overlay backing the ⚙ gear: FIELDS metadata (drives /api/settings + the UI form), load_overlay() (pure read), seed_from_env() (one-time migration), apply() (validate + persist). GET/POST /api/settings in server.py read/write it, then settings.reload().
  • image/app/static/ — the dashboard UI.
  • image/models.yaml — bundled vLLM launch recipes (how to launch a known model), NOT the dashboard menu — the menu is the on-disk scan.
  • image/spark_embed/ — Dockerfile + app for the embeddings container; built ON a Spark (ARM64, NGC PyTorch base — see the audio/cluster rule for NGC torch-pinning caveats).