[Unit] Description=Local LLM model router (FastAPI dispatcher) Documentation=file:%h/llm-router/CLAUDE.md # The classifier talks to Ollama on localhost and the dispatcher talks to # NeuralWatt, so both loopback and real network need to be up. Ollama is a # system service and cannot be ordered against from a user unit, so a failed # classifier call is handled by Restart= below rather than by ordering. After=network-online.target Wants=network-online.target [Service] Type=exec # router.db and router.log are referenced as relative paths, so this has to be # the repo root. config.yaml now lives under config/ and the Python modules # under src/, so PYTHONPATH points at src/ for the dispatcher import to resolve. WorkingDirectory=%h/llm-router # Resolves `dispatcher` (and any other src/ module) for the ExecStart below. Environment=PYTHONPATH=%h/llm-router/src # Holds NEURALWATT_API_KEY. Create it with: # echo "NEURALWATT_API_KEY=$NEURALWATT_API_KEY" > .env && chmod 600 .env EnvironmentFile=%h/llm-router/.env # HuggingFace's default cache (~/.cache/huggingface) falls outside # ReadWritePaths below; redirect it into the repo instead of widening the # sandbox to a new home-directory path. Only classifier.mode: local_encoder # ever touches this. Caught live 2026-09-06: without this, the service # crash-loops with "OSError: Read-only file system" the moment # local_encoder needs to download a model. Environment=HF_HOME=%h/llm-router/.hf-cache ExecStart=%h/llm-router/.venv/bin/uvicorn dispatcher:app --host 127.0.0.1 --port 8080 --timeout-graceful-shutdown 5 Restart=always RestartSec=5s # Loopback only by default — this service holds a billable API key and has no # auth of its own. Anything that widens --host should add auth first. NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=read-only # ...except the repo, which needs to be writable for router.db and router.log. ReadWritePaths=%h/llm-router [Install] WantedBy=default.target