This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Enter development shell (requires Nix + devenv)
devenv shell
# Run all tests
run-tests
# Run a single test file
uv run pytest src/skvaider/inference/tests/test_manager.py -vv
# Run a specific test
uv run pytest src/skvaider/inference/tests/test_manager.py::test_manager_start_model -vv
# Start all services in the background (terminal stays free)
devenv up -d
# Stop background services
devenv down
# Type checking, linting, formatting, etc. all in one:
pre-commit run -a
Skvaider is an OpenAI-compatible API proxy with two parts.
Routes requests to inference backends with load balancing, authentication, health checks and resource management.
- Entry point:
src/skvaider/__init__.py - Config file:
config.toml - Port: 8000
Key components:
proxy/pool.py- Request queue, backend load balancing, and persistent cluster state (cluster-state.jsoninserver.directory)proxy/backends.py- Backend interface (SkvaiderBackend), Ceph-inspiredmap_up/map_instate tracking viaStateFlagrouters/openai.py- OpenAI-compatible endpoints (/openai/v1/...)auth.py- Token authentication via aramaki
Runs local LLMs via llama-server subprocesses.
- Entry point:
src/skvaider/inference/__init__.py - Config file:
config-inference-{1,2}.toml - Ports: 8001, 8002
Key components:
inference/manager.py- Model lifecycle (download, start, health check, terminate)inference/routers/models.py- Model management endpoints (/models/{name}/load,/models/{name}/proxy/{path})inference/routers/manager.py- Health and VRAM usage endpoints
WebSocket-based distributed state management for authentication tokens.
Aramaki is intended to be split off later into a separate package. It is extremely important that no references (imports) from aramaki (src/aramaki) to the skvaider code base (src/skvaider) are
introduced under any circumstances.
manager.py- WebSocket connections and subscriptionscollection.py- Collection protocol and replicationdb.py- SQLite persistence
- Client → Proxy (
/openai/v1/chat/completions) - Proxy authenticates via aramaki tokens
- Pool assigns request to least-loaded backend but batches requests that are incoming at the same time.
- Backend proxies to inference server (
/models/{model}/proxy/v1/chat/completions) - Proxy starts models as needed (llama-server subprocess). At least one reserved model instance should always be available. Additional models are stopped and started as needed.
- Response streams back through the chain
Models track two status dimensions:
process_status: stopped → starting → running → stoppinghealth_status: "" → healthy/unhealthy
Combined into status set with "active" (running+healthy) or "inactive".
Backends track two independent state flags via StateFlag:
map_up: is the backend currently reachable? (up/down)map_in: should the backend be used for model placement? (in/out)
A backend transitions to out only after being down for DOWN_OUT_INTERVAL (grace period), avoiding spurious rebalancing on transient failures. These flags, along with known memory resources and model memory usage, are persisted to cluster-state.json in server.directory so proxy restarts don't lose placement context. The healthy flag is also persisted so the first health-check result after restart correctly detects state changes.
Pydantic models in config.py files. Key patterns:
- Model files: URL + SHA256 hash for verification
- Logging: structlog with IP anonymization
- prefer to create pytest fixtures for reusable code
- fix warnings if possible - if not, make a list of warnings that are still there
- run
pre-commit run -ato check linting, types, etc.
-
"-> None" is not needed on
__init__methods -
if filtering through lists in a compound statement, prefer to use the
guardianpattern to avoid long indentations.Good:
for x in mylist: if not condition(x): continue ... do the happy path work ...Bad:
for x in mylist: if condition(x): ... do the happy path work ... -
do not add superfluous comments to code that is already there. when making comments to new code you generate then do not make the comment if its basically exactly what the code already reads like or is sensibly obvious. stick to higher order "why" comments instead of superfluous comments
bad examples:
# do the foo bar thing do_foo_bar() # Get per-process VRAM usage from --showpids await self._update_per_model_vram_rocm() # Get total VRAM from --showmeminfo proc = await asyncio.create_subprocess_exec( "rocm-smi", "--json", "--showmeminfo", "all", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) -
do not make overly aggressive use of _ (underscore) methods or attributes. this is python, not java.
-
if you log an exception, use the log.exception() function to ensure we see a proper traceback
-
basedpyright strict mode
-
black + isort (line length 80)
-
ruff (ignoring E501, F401)