Files
club-3090/tools/model-switch/README.md
John Shojaei c22a9d2d84 tools: add HTTP model-switch service (thin wrapper over switch.sh)
Adds a stdlib HTTP control plane that wraps scripts/switch.sh so a harness
can POST /switch and block until the new model is serving. Introduces no new
orchestration logic — switch.sh stays the single source of truth (registry
lookup, down/up, readiness).

- tools/model-switch/server.py: GET /healthz|/status|/models, POST /switch
  ({slug}|{model}); registry-validated; /health readiness (works with or
  without VLLM_API_KEY); single-flight lock; refuses to start unauthenticated
  on a non-loopback bind.
- scripts/systemd/club3090-model-switch.service: host daemon unit.
- scripts/tests/test-model-switch.sh: hermetic HTTP/auth/validation contract.
- docs/EXAMPLES.md, .env.example: usage + config.

Mirrors the existing stdlib HTTP style (services/studio/*); zero new deps.
Experimental/opt-in per the repo's staging convention.
2026-07-03 17:44:17 -07:00

3.2 KiB
Raw Blame History

model-switch — HTTP control for swapping the served model

Status: 🧪 experimental (opt-in; validate on your rig before relying on it).

A tiny stdlib HTTP service that wraps scripts/switch.sh so an experiment harness can swap the running model programmatically instead of shelling out. Only one model fits in VRAM at a time on a 12 GPU rig, so this is the "now serve model X, tell me when it's ready" primitive.

It adds no orchestration logicswitch.sh stays the single source of truth (registry lookup, down→up, readiness probe). This is the HTTP analogue of what tools/serve-cockpit does as a TUI. stdlib only, no pip installs.

Endpoints

Method Path Auth Returns
GET /healthz no {"ok": true}
GET /status yes {"current_model","ready","port","container"}
GET /models yes {"available":[{"slug","model","status","port"}, …]}
POST /switch yes blocks until ready → {"ok","slug","model","took_s"}

POST /switch body accepts either an exact registry slug or a model id:

{"slug": "vllm/gemma-31b-dual"}      // any slug from GET /models
{"model": "gemma-4-31b"}             // resolved to the model's curated default slug

Errors: 400 unknown/ambiguous slug-or-model · 401 bad token · 409 a switch is already in progress · 500 {ok:false, detail:<switch.sh log tail>}.

Run

# Foreground (dev):
python3 tools/model-switch/server.py

# Or as a systemd service (survives reboots, auto-restarts):
#   edit scripts/systemd/club3090-model-switch.service (User=, repo path), then:
sudo cp scripts/systemd/club3090-model-switch.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now club3090-model-switch.service

Config (env — systemd loads them from the repo-root .env)

Var Default Purpose
CLUB3090_API_TOKEN Control-endpoint bearer token. Falls back to VLLM_API_KEY. If neither is set, the endpoint is unauthenticated (loopback only).
MODEL_SWITCH_PORT 8099 Listen port.
MODEL_SWITCH_BIND 127.0.0.1 Bind address. Keep on loopback; expose via a VPN, not 0.0.0.0.
PORT slug default Model http port used for the /health readiness probe.
SWITCH_SCRIPT scripts/switch.sh Overridable (used by the test to stub the switch).

Example

TOKEN=...  # your CLUB3090_API_TOKEN (or VLLM_API_KEY)
curl -s -H "Authorization: Bearer $TOKEN" localhost:8099/status
curl -s -XPOST -H "Authorization: Bearer $TOKEN" localhost:8099/switch -d '{"model":"gemma-4-31b"}'
# → blocks ~1-2 min, then {"ok":true,"slug":"vllm/gemma-31b-dual","model":"gemma-4-31b","took_s":97.3}

Security

Bind to loopback (default) and set a token. To reach it from other devices, front it with a VPN — e.g. Tailscale on its own HTTPS port: tailscale serve --https=8443 8099. Do not expose the port directly to the internet; a model-switch endpoint is a control plane.

Readiness note

The service probes the unauthenticated /health endpoint (not the auth-gated /v1/models) when waiting for a switch, so it works whether or not the model is protected with VLLM_API_KEY. It requires no change to switch.sh.