# Lium — Full Reference for Agents Self-contained reference for AI agents. Includes skill overview, CLI command reference, and Python SDK reference. Source of truth: https://github.com/Datura-ai/lium-skill --- # Lium CLI & SDK Lium — agent-first compute: a decentralized GPU rental platform on Bittensor. Pods are Docker containers with root SSH access and direct GPU passthrough. - **GitHub**: https://github.com/Datura-ai/lium - **PyPI**: https://pypi.org/project/lium.io/ - **Docs**: https://docs.lium.io - **Dashboard**: https://lium.io ## Quick Install Standalone binary — no Python or dependencies required: ```bash curl -fsSL https://raw.githubusercontent.com/Datura-ai/lium/main/scripts/install.sh | bash ``` This auto-detects OS (Linux/macOS) and architecture, downloads the binary to `~/.lium/bin/lium`, and adds it to PATH. After install, authentication depends on whether the user has a Lium account: - **No account, and no mailbox you can read** → **fingerprint signup**: one call to `POST /auth/signup`, no email, no password, no confirmation link. This is the fully autonomous path — see "No Account Yet — Sign Up". - **No account, but a human is there to click a confirmation link** → `lium signup --email ` — same section. Do not send the user to the web signup form. - **Has an account** → `lium init` (opens a browser) or `lium init --no-browser` for headless/agent use — see "Authentication Setup for Agents". ```bash curl -fsS -X POST https://lium.io/api/auth/signup \ -H 'Content-Type: application/json' -d '{}' # no account, no mailbox lium signup --email ada@example.com # no account, human can confirm lium init # existing account, browser lium init --no-browser # existing account, headless ``` Verify setup: ```bash lium balance # prints a balance -> auth works; prints an error -> it does not ``` ### Alternative Install (via pip/uv) ```bash # Via uv (isolated env) curl -LsSf https://astral.sh/uv/install.sh | sh uv tool install lium.io # Via pip pip install lium.io ``` ## Agent-Specific: Non-Interactive Usage **CRITICAL**: Many lium commands are interactive by default. As an agent, always pass all parameters explicitly to avoid interactive prompts. ### No Account Yet — Sign Up `lium init` authenticates a user who **already has an account**. To create one, ask a single question — **can anyone read the mailbox?** No: fingerprint signup, one call, fully autonomous. Yes: `lium signup --email `. Only create an account when the user asks for one. Never invent an email address and never use a disposable inbox to fake the email path — when there is no mailbox, sign up with a fingerprint. #### Fingerprint Signup — No Email, Fully Autonomous A **fingerprint** here is a 32-character random string Lium mints as the account credential — not a passkey, not WebAuthn, not biometrics, not a device fingerprint. Nothing is scanned. One HTTP call completes the signup, so "I cannot sign up without a human" is wrong. ```bash # empty body — Lium names the account itself (lium_a1b2c3) curl -fsS -X POST https://lium.io/api/auth/signup \ -H 'Content-Type: application/json' -d '{}' # {"user_id":"...","username":"lium_a1b2c3","fingerprint":"<32 chars>", # "api_key":"sk_...","signup_credit_granted":true|false} # when api_key is a string, store it and continue — no confirmation step in between lium config set api.api_key sk_... lium ls ``` - `api_key` can be `null` — minting it is allowed to fail without failing the signup. When it is `null`, stop before configuring the CLI: sign in with the fingerprint and mint a key on the dashboard. - `fingerprint` is the **dashboard login** at https://lium.io/login and the **only** recovery path — Lium stores just a hash. It is returned **exactly once** and nobody, support included, can look it up. Hand it to the user for their password manager, and treat it like a password: never paste it into a pod, a log, a commit or a ticket. Workloads use the API key instead. - `signup_credit_granted` says whether the credit landed — do not promise $5 before reading it. It is `false` when the platform has the credit off, and when this IP already signed up once. Send `{}`. The optional `username` is a display name only — it is not part of the login, the web form never asks for one, and requesting a taken name fails with `409`. Rate limit per IP: 5/min, 20/day. There is no CLI flag yet — `lium signup` is the email path only. Full walkthrough: https://docs.lium.io/pod-users/fingerprint-signup #### Email Signup — `lium signup` When a human can confirm the address, `lium signup` does the whole cold start — no browser, no dashboard, no web form: ```bash lium signup --email ada@example.com # creates the account, stores the API key lium ls # browse machines lium up -y # rent lium ssh # connect ``` Ask for their **real email** first — the account, its balance and password recovery are tied to it, and the confirmation link is sent there. `lium signup` prints the generated password — hand it to the user, it is their dashboard login (to choose one instead, pass `--password` or set `LIUM_SIGNUP_PASSWORD`, which keeps it off argv). Even when the command fails after the account was created — a timeout, or the API key could not be read back — the error still reports the email and password, so the account is never stranded. Add `--json` for a machine-readable `{email, password, api_key, signup_credit_granted, ssh_key_configured, next_steps}`. The key is written to `~/.lium/config.ini`, so the account is then indistinguishable from one set up with `lium init`. Older binaries do not have the command. Probe for it, and update when it is missing: ```bash lium --version # diagnostics only — probe the command itself below lium signup --help >/dev/null 2>&1 || echo "CLI too old — update it" # Binary install (installed via install.sh): auto-updates on launch, or force it curl -fsSL https://lium.io/install.sh | bash # pip / uv install uv tool upgrade lium.io # or: pip install -U lium.io ``` On an older CLI that cannot be updated, the same signup is three HTTP calls: ```bash BASE=https://lium.io/api # No mailbox to confirm? Use the fingerprint signup above instead of steps 1-3. # 1. Create the account. An API key named "Default" is minted server-side here; current # backends return it in the response — {"msg": "success", "api_key": "sk_...", # "signup_credit_granted": true|false} — older ones mint it without returning it. curl -sX POST $BASE/users -H 'Content-Type: application/json' \ -d '{"name":"Ada","email":"ada@example.com","password":"..."}' # 2-3. Only when the response had no api_key: log in for a JWT, read the key back (format sk_...). TOKEN=$(curl -sX POST $BASE/users/login -H 'Content-Type: application/json' \ -d '{"email":"ada@example.com","password":"..."}' | jq -r .token) KEY=$(curl -s $BASE/keys -H "Authorization: Bearer $TOKEN" \ | jq -r '.[] | select(.name=="Default") | .key') lium config set api.api_key "$KEY" # leave ssh.key_path unset — the CLI generates and configures the key on first use, # and setting the path to a key that does not exist yet makes it skip that ``` ### Before the First Rental — Balance `lium up` calls `POST /executors/{executor_id}/rent`, and the balance is the only gate: a fresh account can rent as soon as it is funded, email confirmed or not. Map the error to the action: | 403 on rent | Meaning | Action | |---|---|---| | `"Insufficient balance"` | The account balance is zero. | Fund the account — see "Funding options" below. | #### Email confirmation Confirming the address does not gate renting. It confirms the address itself, so that password resets and account emails reach the user. Registration sends **two** mails: `"Welcome to Celium!"` (no link in it) and `"Please confirm your email"` — only the second one carries the link. Point the user at that subject, and ask them to click the link. That is the normal path. Two endpoints on `https://lium.io/api` cover the cases where it does not work. Neither needs auth; both take JSON: ```bash # Mail never arrived / link expired (tokens are valid 24h) — send a fresh one curl -sX POST https://lium.io/api/auth/resend-verify-email \ -H 'Content-Type: application/json' -d '{"email":"ada@example.com"}' # 400 "User doesn't exist." or "Email is already verified." when it does not apply # User pastes the link instead of clicking it — finish verification from its ?token= curl -sX POST https://lium.io/api/auth/verify-email \ -H 'Content-Type: application/json' -d '{"token":""}' ``` #### The $5 signup credit The signup credit is $5 when the platform has it enabled **and** no other account has signed up from this IP address — nothing about the email domain matters. Do not explain a `403 "Insufficient balance"` with the credit: that error only says the balance is zero, and the answer to it is to fund the account. Whether it landed is answered by `signup_credit_granted` in the signup response (also in `lium signup --json`): `true` → granted, `false` → not granted. When it is `null` or absent — the backend does not report it — read the balance: ```bash lium balance --json # {"balance_usd": 5.0} ``` #### Funding options - Dashboard: https://lium.io/billing - Headless invoice: `POST /tmc-pay/create-invoice` with header `X-API-Key: sk_...` and a body of `{"amount": , "crypto_currency": "...", "crypto_network": "..."}` (all three required). Valid currency/network pairs come from `GET /tmc-pay/currencies` (same API key header). The response carries `deposit_address`, `crypto_amount`, `hosted_invoice_url` and `expires_at` — give these to the user to pay from their wallet, do not move funds on their behalf. ```bash # {"currencies": [{"code": "USDT", "network": "tron", ...}, ...]} — pick a pair from here curl -s https://lium.io/api/tmc-pay/currencies -H "X-API-Key: sk_..." curl -sX POST https://lium.io/api/tmc-pay/create-invoice -H "X-API-Key: sk_..." \ -H 'Content-Type: application/json' \ -d '{"amount": 20, "crypto_currency": "USDT", "crypto_network": "tron"}' ``` - `lium fund -w default -a 10.0 -y` for users with a Bittensor wallet — here `-a` is an amount of **TAO**, not dollars. `-a` means USD only on the `--alpha` path, which moves Subnet-51 alpha the user already has staked: `lium fund --alpha -k -a 10 -y`. SSH keys need no extra registration — the public key at `ssh.key_path` is registered server-side right before renting. ### Authentication Setup for Agents For a user who already has an account (skip if you just ran the signup flow above and stored the key). **Preferred: two-step headless auth** — no API key needed, no blocking, no browser: 1. Run `lium init --no-browser` — get auth URL and session ID (exits immediately) 2. Show the URL to the user, ask them to open it and click Approve 3. Wait for user to confirm they approved 4. Run `lium init --session ` — saves API key + sets up SSH ```bash lium init --no-browser # [i] Open this URL to authenticate: # https://lium.io/cli/approve/xJinnT3Vt6... # [i] Then complete authentication with: # lium init --session abc123def456 # ... user confirms they approved ... lium init --session abc123def456 # [✓] API key saved ``` **Fallback options** (if `--no-browser` is unavailable or user already has an API key): ```bash # Option 1: Direct config lium config set api.api_key YOUR_API_KEY lium config set ssh.key_path ~/.ssh/id_ed25519 # Option 2: Environment variable (session only) export LIUM_API_KEY=YOUR_API_KEY ``` For fallback options, the user must get an API key from https://lium.io Account Settings. ### Verify Setup ```bash lium balance # prints a balance -> auth works; an error -> it does not ``` Do not run `lium config show` (or `lium config get api.api_key`) to check the setup: both print the API key in full, and anything an agent prints ends up in its transcript and logs. `lium balance` proves the key works without ever showing it. If you must confirm where the key is stored, check that `~/.lium/config.ini` exists. ### Non-Interactive Pod Creation Always use `-y` flag and pass all parameters. Add `--no-ssh` too: without it a successful `lium up` ends by opening an interactive SSH session, which stalls an agent (`--image` mode streams container logs instead). ```bash # WRONG (interactive): lium up # one confirmation prompt before renting lium up 1 # same — the prompt is the acquire confirmation # RIGHT (non-interactive): lium up --gpu H100 -y --no-ssh # auto-selects node + default template lium up --gpu A100 -c 2 --country US -y --no-ssh # with filters lium up --gpu H100 --name my-pod --ttl 6h -y --no-ssh # with name and auto-termination lium up --gpu A6000 --image pytorch/pytorch:2.0 -y # custom docker image (streams logs) lium up --gpu H100 --jupyter -y --no-ssh # with Jupyter ``` Rent by spec, not by id: `lium up --gpu [-c N] [--country CC]` (and, coming with lium#209 — not in 0.0.37, the latest release — `Lium.rent(gpu_type=, gpu_count=, min_cpus=, min_vram_gb=, max_price_per_gpu_hour=, …)` in the SDK) **picks a matching node and rents it in one call**. The backend route that picks the cheapest is live (`GET /version` lists `rent_by_spec`); the released client 0.0.37 does not call it yet and picks locally (the first Pareto-optimal row of `ls()`, not the cheapest) — lium#209 makes `lium up --gpu` and `Lium.rent` use it. Do not `lium ls` first and rent the first row yourself: it is neither the cheapest nor guaranteed still free. On the 0.0.37 SDK, which has no `rent()`, `ls()` + `up(executor_id=)` is the only path — pick by `price_per_hour`, not the first row (example in `references/sdk-reference.md`). ### Before You Rent 8 GPUs — Check Interconnect and Ingress First A multi-GPU listing does not tell you how the GPUs are wired together or how fast the node pulls from the internet. Both decide whether a tensor-parallel / FSDP job runs at all and how long the weights take to arrive. Check them in the first minute of the rental, before any download or launch, and delete the pod if they are wrong. ```bash lium exec "nvidia-smi topo -m; nvidia-smi topo -p2p r" ``` Read `topo -m`: every off-diagonal GPU cell must be `NV#` (e.g. `NV18` on H100/H200, `NV12` on A100) for a proper HGX board. `PIX`/`PXB`/`PHB`/`NODE`/`SYS` means PCIe — several times slower for NCCL collectives. Read `topo -p2p r`: every off-diagonal cell must be `OK`; `NS` everywhere means peer-to-peer is disabled (seen on virtualised 8× H200 hosts), and NCCL fails on its first all-reduce with `unhandled cuda error` / `operation not supported`. Decision rule for TP/FSDP jobs: all `NV#` and all `OK` → proceed. Anything else → `lium rm -y` and pick another node. If the job must run there anyway, NCCL only works over loopback sockets, and TP=8 serving of a large model is impractical. Each `lium exec` is a fresh SSH session, so a bare `export` in one call is gone in the next; pass the variables with `-e` on the call that runs the job: ```bash lium exec -e NCCL_P2P_DISABLE=1 -e NCCL_SHM_DISABLE=1 -e NCCL_IB_DISABLE=1 -e NCCL_SOCKET_IFNAME=lo "python train.py" # virtualised host without RDMA NICs ``` One independent process per GPU (batch inference, best-of-N generation, sweeps) does not need the interconnect and runs at full speed on any eight cards. Ingress: the **Download (Mbps)** column in `lium ls` is a smoothed average of the validator's VerifyX check, which fetches a real object of known size and hash (the speed-test average is the fallback when VerifyX has no figure; **Upload** follows the same order). It flags nodes under 100 Mbps as slow; it does not predict Hugging Face or PyPI throughput. The same 756 GB checkpoint pulled at 2.6–4 GB/s, 1.04 GB/s and 45–200 MB/s on three nodes listed in the same few-hundred-Mbps band. Measure before committing: ```bash lium exec "curl -o /dev/null -sS -w '%{http_code} %{speed_download}\n' 'https://speed.cloudflare.com/__down?bytes=50000000'" # HTTP code, bytes/s (>= 100 MB is refused with 403) lium exec "curl -L -o /dev/null -sS --max-time 20 -w '%{http_code} %{speed_download}\n' https://huggingface.co/openai-community/gpt2/resolve/main/model.safetensors" # same for the Hugging Face CDN; -L follows the redirect, 20 s cap, nothing written to disk ``` Do the arithmetic: bytes to download ÷ measured bytes/s. At 45 MB/s a 750 GB checkpoint is 4.6 h of idle GPU billing; at 1 GB/s it is 12.5 min. Uplink varies as much (30 KB/s vs 0.5 MB/s seen) — push results from the pod to Hugging Face / S3 directly rather than through the controlling machine. An `interconnect` field and a CDN-measured ingress/egress figure are coming with lium-platform#61, and `lium ls` filters for NVLink and minimum ingress with lium#149; neither is released. Until your CLI shows a **Link** column, these commands are the check. ### Non-Interactive Funding ```bash # WRONG (interactive): lium fund # RIGHT: lium fund -w default -a 10.0 -y # fund 10 TAO, skip confirmation ``` User must have a verified Bittensor wallet at https://lium.io/billing. ### Agent Gotchas / Known Pitfalls #### After Install — Export PATH ```bash export PATH="$HOME/.lium/bin:$PATH" # needed in current shell session ``` #### After `lium init --session` — Verify with `lium balance` After completing the two-step auth, run `lium balance` to verify: a balance means the key works; an error with exit `3` means it does not. `lium ls` returns results with any key (the node list is public), so it proves nothing about the key. #### Exit Codes Hold Since 0.0.31 — Still Read the Output Since lium **0.0.31** every command exits non-zero when it fails (table in `references/cli-commands.md` § Exit Codes): `lium ps` with a revoked key exits `3`, a 403 exits `6`, and `lium ssh no-such-pod-xyz` exits `5`: ```bash lium ssh no-such-pod-xyz # prints "No active pods" (or "Pod '…' not found"), exits 5 lium rm pod-a,pod-b -y # one refused by the API: "Removed 1 pod(s): pod-a" / "Failed to remove pods: pod-b", exits 1 ``` Two things `$?` alone does not tell you: a batch (`rm`, `reboot`, `scp`, `rsync`) finishes the batch and exits `1` naming the items that failed — read the line to learn which — and an empty result is a success (`lium ls --format json` prints `[]` and exits `0` when no node matches). Prefer the machine-readable modes (`lium ls --format json`, `lium ps --format json`, `lium exec --json`) and check the result there: `--json` commands put a failure on stderr as one JSON object, `{"ok": false, "error": {...}}`, with stdout empty; `--format json` prints `Error: ...` as text. Releases before 0.0.31 printed `Error: ...` and exited `0` on most failures (DAH-2593); pin `lium>=0.0.31` when a script branches on `$?`. #### Pod Targeting — Prefer Names Use the pod **name** (e.g. `lunar-lion-4c`) from `lium ps` output for targeting — not a numeric index; indices shift with every listing. #### `-y` Exists on the Destructive Commands `lium rm`, `lium up`, `lium fund`, `lium volumes rm`, `lium bk set/rm/restore` all take `-y, --yes`. No piped `yes` is needed: ```bash lium rm my-pod # will prompt for confirmation lium rm my-pod -y # non-interactive lium rm -a -y # remove all pods non-interactively ``` #### Templates - Without `--template_id` or `--image`, `lium up` uses default **PyTorch (CUDA)** template — fastest to start - Default Docker-in-Docker (dind) template image: `daturaai/dind` - Search templates: `lium templates pytorch` (text search, no --format json) - To use specific template: `lium up --gpu H100 -t -y` - To use custom Docker image: `lium up --gpu H100 --image pytorch/pytorch:2.0 -y` #### No User Identity Command lium CLI has no renter-side identity command — no `whoami` for your API key. (`lium provider portal whoami` exists, but it reports the *provider* portal session, not the API key you rent with.) To check the key, run `lium balance`: it prints a balance when the key works and, since 0.0.31, exits `3` with an error when the key is bad or revoked. Do **not** use `lium ls` for this: the node list is public, so it succeeds (exit `0`) with any key. Releases before 0.0.31 exited `0` on most failures — pin `lium>=0.0.31` when a script branches on `$?`. #### Long-Running Commands Over SSH `lium exec` runs commands in the foreground over SSH. Commands longer than ~30-60s (e.g. `pip install vllm`, `huggingface-cli download`) may be killed by SSH drop. Wrap with `nohup` + log redirect and poll the log: ```bash # Start long command in background, detached from SSH session # (the \$ escapes for the local shell; the remote sees literal $! which expands to the backgrounded bash PID) lium exec my-pod "nohup bash -c 'pip install vllm' /tmp/install.log 2>&1 & echo PID=\$!" # Watch progress lium exec my-pod "tail -f /tmp/install.log" # or stream via the logs endpoint if the command writes to stdout of PID 1 lium logs my-pod --follow ``` For fully-detached execution (survives SSH session close, stays running after `lium exec` returns): ```bash lium exec my-pod "setsid nohup /tmp/out.log 2>&1 &" ``` #### Stopping a Remote Job — Never `pkill -f` From a One-Liner Inside `lium exec` (and any `ssh host bash -c '…'` one-liner) the whole command is the invoking shell's own command line, so `pkill -f "python train.py"` matches that shell too and kills the session before or instead of the job — the call returns a broken pipe and the job may still be running (two agents lost sessions this way on the same day). Stop jobs by PID: write one when you start (`… & echo \$! > /tmp/job.pid` — escaped, like every `$` inside a double-quoted `lium exec "…"`, or the local shell expands it to nothing and the pod gets an empty PID file) and stop with `kill \$(cat /tmp/job.pid)` as below. If you must search by name, list first and exclude your own shell and its parent, then kill the PIDs you inspected: ```bash lium exec my-pod "kill \$(cat /tmp/job.pid)" # preferred lium exec my-pod "pgrep -f 'python train.py' | grep -vx -e \$\$ -e \$PPID" # inspect, then kill ``` #### PEP 668 on Default PyTorch Template The default `daturaai/pytorch` image is based on Ubuntu 24.04 where system `pip` is PEP 668 protected (`externally-managed-environment`). Use one of: ```bash # Option 1: allow system-wide install pip install --break-system-packages # Option 2: venv (recommended for isolation) python -m venv /opt/env && source /opt/env/bin/activate && pip install # Option 3: uv (fast, handles isolation automatically) curl -LsSf https://astral.sh/uv/install.sh | sh uv pip install --system ``` #### Missing System Libraries in Base Image The default GPU base image does not include: `jq`, `htop`, `tmux`, `screen`, `libnuma1`, `git-lfs`, `rsync`. If your workload needs them: ```bash lium exec my-pod "apt-get update && apt-get install -y libnuma1 jq tmux git-lfs" ``` Note: `libnuma1` is required by `sglang`'s `sgl_kernel` and some `vllm` configs — missing it causes cryptic "kernel not found" errors that actually mean the `.so` failed to load. #### Cold-Start Expectations Don't assume a pod is broken if it's quiet for several minutes after launch. Typical timings: - Pod provisioning + SSH ready: ~30-60s - Docker image pull: usually cached, ~0-30s - Package installs (`pip install vllm`): ~2-5 min - Model download from HuggingFace (4B-class): ~1-2 min; (70B+): ~5-10 min - vLLM engine init (4B model, single GPU): ~2-3 min - sglang + 70B+ sharded (CUDA graph capture of ~50 graphs): **15-25 min** Use `lium logs my-pod --follow` to watch progress, or poll a log file from `lium exec`. #### Verify HuggingFace Model Exists Before Deploy Before spinning up a pod for a specific model (e.g. `vllm serve `), confirm the `repo_id` exists on HuggingFace — typos like `qwen3.5-4b` (doesn't exist) vs `Qwen/Qwen3-4B` waste a full cold-start cycle. ```bash curl -s "https://huggingface.co/api/models?search=qwen+2.5+7b&limit=10" | jq -r '.[].id' ``` #### Pod Vanishes from `lium ps` Pods with internal status `DELETING` are filtered out of `lium ps`. `FAILED` pods remain visible (with `FAILED` status) — so if a pod was `RUNNING` and fully disappears, it's being deleted, not failing. To investigate: - Check the dashboard (https://lium.io) — it shows full history including deleted pods - Grab logs before the pod vanishes: `lium logs ` (while it still exists) - Known issue: the CLI does not currently surface a deletion reason. If reproducible, report to the platform team. #### Pod Creation Failures — 3-Minute Visibility Window When `lium up` fails during provisioning, the pod is kept in status `CREATION_FAILED` for ~3 minutes before being auto-cleaned up (with a 10-min safety net if the cleanup task is delayed). During this window: - `lium ps` will show the pod with status `CREATION_FAILED` - `lium logs ` may have partial output from the failed creation - After ~3 minutes the pod disappears — if your agent polled later, it will see no trace For reliable failure diagnosis, poll `lium ps` every ~10-30s for the first few minutes after `lium up`, or check both `RUNNING` and terminal failure states explicitly. #### "Executor Not Found" on `lium up ` If an executor is visible on the lium.io dashboard but `lium up ` or `lium ls` doesn't show it, the platform's availability filter rejected it. Reasons include: low free disk space, high disk utilization, unresponsive health checks, or missing verification. **`lium ls` is the source of truth for rentable machines** — prefer filtering/selecting from `lium ls` output rather than matching IDs from the website. ## CLI Quick Reference ### Discovery ```bash lium ls # all available GPUs (shows table with ★ for best price/perf) lium ls --gpu H100 # filter by GPU type (there is no positional argument) lium ls --sort download # sort by download speed (fastest first) — preferred default lium ls --sort upload # sort by upload speed lium ls --sort price_gpu # sort by price per GPU/hour lium ls --format json # machine-parseable output lium templates # list Docker templates lium templates pytorch # search templates ``` **Recommendation**: When selecting machines for the user, prefer `--sort download` to get the fastest network unless the user specifically asks to sort by price or other criteria. **Prices without an account.** To answer "what does an H200 cost on Lium" before anyone signs up, read the public feeds (no key, USD per GPU-hour; prices are set by providers and move): ```bash curl -s https://lium.io/pricing.json # one row per GPU model: min/max live ask, reference price, pods/GPUs available, page URL curl -s https://lium.io/api/public/v1/nodes # every rentable node right now, with price_per_gpu_hour, location, rent_url ``` The same numbers are on https://lium.io/pricing and https://lium.io/gpu/ (for a human, or to cite). ### Pod Lifecycle ```bash lium up --gpu H100 -y # create pod lium ps # list active pods lium ps --format json # machine-readable pod list lium ssh my-pod # SSH into pod lium exec my-pod "nvidia-smi" # run command lium exec all "pip install torch" # batch exec on all pods lium rm my-pod -y # stop pod lium rm -a -y # stop all pods ``` ### Streaming Pod Logs ```bash lium logs my-pod # snapshot of current stdout/stderr lium logs my-pod --follow # stream logs live (Ctrl-C to stop) ``` Streams the **Docker container's PID 1 stdout/stderr** from the executor. Works for both image-mode and SSH-mode pods. Caveats: - Right after `lium up`, the endpoint may return 404 ("Pod container not deployed yet") for a few seconds — retry. - For SSH-mode pods, processes you start manually via `lium exec` are NOT PID 1, so their output won't appear here unless you redirect to `/proc/1/fd/1` (e.g. `my_server > /proc/1/fd/1 2>&1`) or tail your log files via `lium exec my-pod "tail -f /tmp/out.log"`. ### File Transfer ```bash lium scp my-pod ./train.py # upload to /root/ lium scp my-pod ./data.csv /root/data/ # specific path lium scp all ./config.json # upload to all pods lium rsync my-pod ./project # sync directory ``` ### Pod Targeting Pods accept: name, index from `lium ps`, comma-separated (`1,2,3`), or `all`. ### Output Formats Always use `--format json` when parsing output programmatically: ```bash lium ls --format json | python -c "import json,sys; print(json.load(sys.stdin))" lium ps --format json | python -c "import json,sys; print(json.load(sys.stdin))" ``` Never read node ids or prices off the `lium ls` table: it shows the HUID, which `lium up` before 0.3.0 rejects, and CLIs before 0.0.40 also drop the **Id** column and truncate prices to `0…` at 80 columns. The JSON has every field: `id` (UUID — what every `lium up` accepts), `huid` (the short name the table shows; accepted by `lium up` since 0.3.0 (lium#153), refused by every release before it, 0.2.0 included), `price_per_hour`, `price_per_gpu_hour`, `gpu_count`, `download_mbps`, `upload_mbps`, `country`. `--format [table|json]` exists on `lium ls` and `lium ps`. `--json` — a plain flag, not a format choice — is taken by `lium exec`, `lium fund`, `lium balance`, `lium signup`, `lium topup create`, `lium topup currencies`, and by the whole `lium provider` group (set it on the group: `lium provider --json node list`). `lium templates` has neither, and neither does anything else. ## End-to-End Agent Workflow Complete flow for setting up and renting a GPU pod: ```bash # 1. Install lium (if not present) if ! command -v lium >/dev/null 2>&1; then curl -fsSL https://raw.githubusercontent.com/Datura-ai/lium/main/scripts/install.sh | bash export PATH="$HOME/.lium/bin:$PATH" fi # 2a. No account, no mailbox → fingerprint signup, no human needed. Save `fingerprint` for the # user (dashboard login, shown once). If api_key is null, mint one after signing in with it. curl -fsS -X POST https://lium.io/api/auth/signup -H 'Content-Type: application/json' -d '{}' lium config set api.api_key # 2b. No account, but a human can click the confirmation link → ask for their real email lium signup --email # 2c. Existing account → two-step headless auth instead lium init --no-browser # → parse URL and session ID from output, show URL to user # → wait for user to confirm they approved lium init --session # 3. Verify the key (lium ls lists public data and succeeds with any key; a bad key exits 3 here since 0.0.31) lium balance --json # A zero balance means signup credit did not land; fund the account before renting. # 4. Find suitable GPU (sort by speed by default) lium ls --gpu H100 --sort download # 5. Create pod (non-interactive! --no-ssh returns instead of opening a session) lium up --gpu H100 --name work-pod --ttl 6h -y --no-ssh # 6. Wait and verify (read the output, not just the exit code) lium ps --format json # 7. Use the pod lium scp work-pod ./code.py lium exec work-pod "python /root/code.py" # 8. Cleanup lium rm work-pod -y ``` ## Run One Python Function on a GPU (no pod scripting) When the task is "run this function on a GPU and give me the result" — a benchmark, an inference, an embedding batch — use `@lium.machine` from the SDK instead of `up` / `scp` / `exec` / `rm` by hand. It rents the cheapest matching node, ships the function, installs the requirements once, streams the function's output, returns the result (or re-raises its exception) and removes the pod. Cost is bounded: the pod is scheduled for removal at `timeout + 15 min` (plus `keep_warm`) from the moment it is rented. Everything in this section is since 0.0.40 (lium#208); on 0.0.39 and earlier the decorator takes only `machine`, `template_id`, `cleanup`, `requirements`, picks the first node whose name contains the string, and results must be JSON-serialisable. ```python import lium @lium.machine(machine="RTX4090", requirements=["transformers", "accelerate"], timeout=600, keep_warm=300) def generate(prompt: str) -> str: import torch # import INSIDE the function; only its def travels from transformers import AutoModelForCausalLM, AutoTokenizer tok = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM2-135M-Instruct") model = AutoModelForCausalLM.from_pretrained("HuggingFaceTB/SmolLM2-135M-Instruct", dtype=torch.bfloat16, device_map="cuda") ids = tok.apply_chat_template([{"role": "user", "content": prompt}], return_tensors="pt", add_generation_prompt=True).to("cuda") out = model.generate(ids, max_new_tokens=64, do_sample=False, pad_token_id=tok.eos_token_id) return tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip() try: print(generate("Who discovered penicillin?")) # cold: ~1-2 min (rent, boot, install); warm: ~20 s print(generate("Name one antibiotic.")) # reuses the warm pod except Exception as e: # a builtin raised remotely: the same type, e.__cause__ is lium.RemoteExecutionError; cause = e.__cause__ if isinstance(e.__cause__, lium.RemoteExecutionError) else None # a timeout, no node, a failed rental: no cause print(type(e).__name__, e, cause.remote_traceback if cause else "") finally: generate.close() # remove the warm pod now (else: keep_warm + 2 min later) ``` Rules that save a failed call: `machine` is `"x"` / `""` (`"1xH200"`, `"RTX4090"`; count defaults to 1 — `"A100"` is one A100, not eight). Import inside the function; a module-level import/constant/helper used inside is refused at definition time (it would be a `NameError` on the pod). Return plain Python types (`str()`, `.tolist()`, `.cpu().numpy()`), not tensors or `torch.__version__`. Torch is already on the default template — do not put it in `requirements`. `f.map(items)` runs a batch on one pod; `f.local(x)` or `LIUM_MACHINE_LOCAL=1` runs the function locally for tests; `quiet=True` drops the `[lium]` progress lines (the function's own prints still stream). Like the rest of this section (`timeout`, `keep_warm`, `close()`, `RemoteExecutionError`, the node pick), every one of these (map, local, LIUM_MACHINE_LOCAL, quiet) is since 0.0.40 (lium#208) — not one of them exists on 0.0.39 or earlier. --- # CLI Commands — Full Reference # Lium CLI Command Reference Written against `lium --version` **0.0.29**. Every flag below appears in that binary's own `--help`; nothing here is extrapolated. When a newer CLI ships, `lium --help` is the authority, not this file. The one exception is marked inline: an option or command tagged **(lium#NNN, not released)** is read off that open lium PR's `--help` and is not in the released binary yet. ## Table of Contents - [Global Options](#global-options) - [lium signup](#lium-signup) - [lium init](#lium-init) - [lium balance](#lium-balance) - [lium ls](#lium-ls) - [lium up](#lium-up) - [lium ps](#lium-ps) - [lium audit](#lium-audit) - [lium ssh](#lium-ssh) - [lium exec](#lium-exec) - [lium scp](#lium-scp) - [lium rsync](#lium-rsync) - [lium rm](#lium-rm) - [lium logs](#lium-logs) - [lium port-forward](#lium-port-forward) - [lium reboot](#lium-reboot) - [lium update](#lium-update) - [lium templates](#lium-templates) - [lium volumes](#lium-volumes) - [lium bk (backups)](#lium-bk-backups) - [lium schedules](#lium-schedules) - [lium ssh-keys](#lium-ssh-keys) - [lium config](#lium-config) - [lium theme](#lium-theme) - [lium fund](#lium-fund) - [lium topup](#lium-topup) - [lium mine](#lium-mine) - [lium provider](#lium-provider) - [lium gpu-splitting](#lium-gpu-splitting) - [Batch Operations](#batch-operations) - [Pod Targeting](#pod-targeting) - [Environment Variables](#environment-variables) - [Exit Codes](#exit-codes) ## Global Options The root command takes exactly two options: ``` --version Show the version and exit --help Show this message and exit ``` There is no `--config` and no `--debug` flag. Debug output is switched on with the `LIUM_DEBUG=1` environment variable, and the config file location comes from `lium config path`. ## lium signup Create a Lium account and store the API key it mints. Fully non-interactive — this is the command to use when the user has **no account yet**. Older CLI binaries do not have it — probe with `lium signup --help` and update the CLI when it is missing. ```bash lium signup [OPTIONS] --email EMAIL The user's real email (REQUIRED) — the confirmation link goes there --name NAME Display name (defaults to the email's local part) --password PASSWORD Account password (a strong one is generated when omitted) --json Machine-readable output ``` The password can also come from the `LIUM_SIGNUP_PASSWORD` environment variable — `--password` wins when both are set. Prefer the variable: a flag value is left behind in the shell history and in `ps` output. Whatever its origin, the password is always reported back to the caller. Ask the user for their **real** email — the account, its balance, password recovery and the confirmation link are all tied to it. Never invent an address. The command creates the account (`POST /users`), stores the minted API key in `~/.lium/config.ini` under `api.api_key`, and sets up an SSH key. After it, `lium ls` and `lium up` work with no further setup. **Refuses to run when `api.api_key` is already configured** — it exits with an error instead of creating a second, unreachable account. To sign up anyway, drop the existing key first: ```bash lium config unset api.api_key # then: lium signup --email ... ``` **Failures never strand the account.** When the command fails after the account was created — the request timed out, or the API key could not be read back — the error still reports the email and password, so the user can log in at https://lium.io and copy an API key from the dashboard. With `--json`, that error goes to stderr as `{"ok": false, "error": {...}, "data": {"email": "...", "password": "..."}}`. Examples: ```bash lium signup --email ada@example.com lium signup --email ada@example.com --name Ada --json LIUM_SIGNUP_PASSWORD=... lium signup --email ada@example.com ``` `--json` output: ```json { "api_key": "sk_...", "email": "ada@example.com", "next_steps": ["...", "...", "..."], "password": "generated-or-supplied", "signup_credit_granted": true, "ssh_key_configured": true } ``` - `password` — the dashboard login at https://lium.io. Hand it to the user; it is not stored anywhere else. - `signup_credit_granted` — comes straight from the signup API response and is the authoritative answer to "did the $5 signup credit land?": `true` → granted; `false` → not granted (the once-per-IP gate, or the credit disabled platform-side); `null` → the backend did not report it (older backend) — read the balance instead: `lium balance --json`. - Renting is not gated on email confirmation — a funded balance is the only requirement. Clicking the link in the **"Please confirm your email"** mail (the separate "Welcome to Celium!" mail carries no link) confirms the address, so that password resets and account emails reach the user. ## lium init Initialize the CLI for a user who **already has an account** — `lium init` cannot create one, use [`lium signup`](#lium-signup) for that. Plain `lium init` opens a browser and is **not suitable for agent use**; the `--no-browser` / `--session` pair is the headless two-step. ```bash lium init [OPTIONS] --no-browser Print the auth URL + session ID instead of opening a browser (step 1) --session ID Verify the auth session and save the API key (step 2) ``` For an agent that already holds an API key, write the config directly instead: ```bash lium config set api.api_key YOUR_KEY lium config set ssh.key_path ~/.ssh/id_ed25519 ``` ## lium balance Show the current account balance. ```bash lium balance [OPTIONS] --json Print machine-readable JSON ``` ## lium ls List available GPU nodes. There is no positional argument — filter with `--gpu`. ```bash lium ls [OPTIONS] --gpu TEXT Filter by GPU type, e.g. A100 --count INTEGER Exact GPU count to match (e.g. 1, 8) --min-cuda FLOAT Minimum CUDA version, e.g. 12.4 --nvlink Only nodes whose GPUs are all joined by NVLink (Link column NV#); nodes with no topology report yet are excluded (since 0.4.0) --min-download FLOAT Minimum Download (Mbps) a node must report; nodes with no figure are excluded; alias --min-ingress; 0, negatives, nan and inf exit 2 before any request (since 0.4.0) --lat FLOAT Latitude for distance filtering --lon FLOAT Longitude for distance filtering --max-distance INTEGER Maximum distance in miles from --lat/--lon --sort FIELD price_gpu | price_total | loc | id | gpu | download | upload | price_per_gpu_hour | price_per_hour (an explicit --sort wins over the ★ optimal ordering) --limit INTEGER Limit the number of rows shown --format [table|json] Output format; 'json' goes to stdout, suitable for jq ``` Examples: ```bash lium ls # all nodes lium ls --gpu H100 # only H100 nodes lium ls --gpu H100 --count 8 # only 8×H100 nodes lium ls --format json # JSON output for parsing lium ls --sort price_per_gpu_hour --limit 10 lium ls --gpu H200 --nvlink --min-download 2000 # NVLink boards with a Download floor — since 0.4.0 ``` The table and JSON carry no interconnect field; `download_mbps` and `upload_mbps` are smoothed averages of the validator's VerifyX check (a fetch of a real object), falling back to the speed-test average, and neither is CDN throughput. Before a tensor-parallel or weight-heavy job on a multi-GPU node, verify on the pod: `nvidia-smi topo -m` (all off-diagonal GPU cells `NV#`), `nvidia-smi topo -p2p r` (all `OK`) and a timed download — see "Before You Rent 8 GPUs" in SKILL.md. **Since 0.4.0 (lium#149):** the table gains **Link** (how the node's GPUs are wired, as its validator saw with `nvidia-smi topo -m`: `NV18` = NVLink with 18 links, `PCIe/SYS` = PCIe only; `—` until reported) after Config. A terminal narrower than about 120 columns hides it (the footer says so); `--format json` always carries it. Download/Upload stay the speed-test figures; there is no CDN column. `--format json` rows gain `link`, `nvlink`, `p2p` and `interconnect` (counts and the GPU×GPU matrix), all `null` until the node's validator reports them. Both filters are sent to the API and applied client-side too; when nothing is left the message is `No available node reports NVLink between every GPU pair and Download ≥ 2000 Mbps`, followed by each filter's rule. `lium describe` shows the same as Link / Topology rows, a Net row with the speed-test figures, and `gpu.link`, `gpu.p2p`, `gpu.interconnect`, `machine.download_mbps` in `--json`. On a CLI older than 0.4.0, verify on the pod (`nvidia-smi topo -m`, `nvidia-smi topo -p2p r`, a timed download). ## lium up Create a new pod. **Always pass `-y` for non-interactive (agent) usage.** ```bash lium up [OPTIONS] [NODE_ID] NODE_ID Node UUID, HUID, or index from the last `lium ls`. Optional — omit it and the filters below auto-select the best node. (`lium up --help` prints NODE_ID without brackets; the argument is optional all the same.) Pass the UUID (`id` in `lium ls --format json`) on any release before 0.3.0: there the HUID answers "Node '' not found" although the help lists it. Since 0.3.0 (lium#153) the HUID resolves too. -n, --name TEXT Custom pod name -t, --template_id TEXT Template ID -v, --volume TEXT Volume spec: 'id:' or 'new:name=[,desc=]' -y, --yes Skip the confirmation prompt (REQUIRED for agent use) --gpu TEXT Filter nodes by GPU type (e.g. H200, A6000) -c, --count INTEGER Number of GPUs per pod --country TEXT Filter nodes by ISO country code (e.g. US, FR) -p, --ports INTEGER Minimum number of available ports required --ttl TEXT Auto-terminate after a duration (6h, 45m, 2d) --until TEXT Auto-terminate at a local time ("today 23:00", "tomorrow 01:00", "2025-10-20 15:30") --jupyter Install Jupyter Notebook (auto-selects a port) --no-ssh Create the pod and return instead of opening an SSH session --image TEXT Docker image to run (e.g. pytorch/pytorch:2.0) --internal-ports TEXT Internal ports to expose (comma-separated: 22,8000,8080) --dockerfile FILE Build the pod image from this Dockerfile (mutually exclusive with --image / --template_id) -e, --env TEXT Environment variables (KEY=VALUE), repeatable --entrypoint TEXT Container entrypoint --cmd TEXT Command to run in the container --ssh-name TEXT Name to register a new SSH key under (default: cli-@) --volume-encryption / --no-volume-encryption Encrypt the local volume when supported (on by default) ``` `--no-ssh` matters for agents: without it `lium up` ends by opening an interactive SSH session (or, with `--image`, by streaming container logs). Examples: ```bash # Non-interactive (for agents): lium up --gpu H100 -y --no-ssh # auto-select + default template lium up --gpu H200 --country US --name train -y --no-ssh lium up --gpu H100 --ttl 6h --jupyter -y --no-ssh # Docker-run style (streams logs instead of SSH): lium up --gpu A4000 --image pytorch/pytorch:2.0 -y lium up --gpu H100 --image vllm/vllm-openai:latest -e HF_TOKEN=xxx -y # Custom Dockerfile, built remotely: lium up --gpu A4000 --dockerfile ./Dockerfile -y # With volumes: lium up --gpu H100 -v id:brave-fox-3a -y # attach an existing volume lium up --gpu H100 -v new:name=data -y # create + attach a volume # Specific node: lium up 1 --name dev-pod -y # node #1 from the last ls ``` ## lium ps List active pods. The optional positional narrows the listing to one pod. ```bash lium ps [OPTIONS] [POD_ID] POD_ID Show a single pod — name, HUID or UUID only, NOT an index --format [table|json] Output format; 'json' goes to stdout, suitable for jq ``` `lium ps --format json` **is supported** and is the way an agent should read pod state. There is no `-a/--all` and no `--sort`. ## lium audit **Since lium 0.0.37.** Who did what to the account's pods, and when: every rent, reboot, edit and delete with the session or API key that requested it; entries the platform wrote by itself (a validator reply, a balance stop) say `platform`. Needs a backend that serves `GET /users/me/events` to API keys (lium-platform#208, not released); against today's API an API key exits `3` with a hint. ```bash lium audit [OPTIONS] # since 0.0.37 --pod TEXT Only this pod: id, huid, name or index from the last ps; a deleted pod's full id --since TEXT Only events after this: 24h, 30m, 7d or an ISO timestamp (else exit 2) --key TEXT Only actions made with this API key id --limit INTEGER Newest events to fetch, 1–1000 (default 200; out of range exits 2 locally) --json Print the events as machine-readable JSON (newest first) ``` The table — When (UTC) / Pod / What / By — reads oldest first. `By` is `key ()`, `session` (browser) or `platform`. Exit `5` when `--pod` is neither a listed pod nor a full UUID (a full id passes through so a deleted pod can be queried). ```bash lium audit --since 24h # today lium audit --pod my-pod # one pod's history lium audit --json | jq '.[] | select(.actor.api_key_name == "ci")' # one key's actions ``` ## lium ssh Open an interactive SSH session to a pod. It takes no options — to run a command and exit, use [`lium exec`](#lium-exec). ```bash lium ssh TARGET TARGET Pod name/ID (eager-wolf-aa) or index from `lium ps` (1, 2, 3) ``` ## lium exec Execute commands on one or more pods. **This is the command an agent uses to run things remotely** — it exits with the remote command's exit code, so `lium exec "cmd" && next-step` behaves the way a caller expects. ```bash lium exec [OPTIONS] TARGETS [COMMAND] TARGETS Pod name/ID, index, comma-separated list, or "all" COMMAND Command to execute (quote multi-word commands) -s, --script TEXT Execute a local script file on the pod -e, --env TEXT Set environment variables (KEY=VALUE) --json Print machine-readable JSON (stdout, stderr, exit_code) ``` Examples: ```bash lium exec my-pod "python train.py" lium exec 1 "python --version" lium exec 1 "nvidia-smi" lium exec 1,2,3 "uptime" lium exec all "df -h" lium exec 1 --script setup.sh lium exec 1 -e API_KEY=xyz "python app.py" lium exec 1 --json "python train.py" ``` There is no `--timeout` and no `--output`; redirect the output in the shell (`lium exec 1 "nvidia-smi" > gpu.txt`). ## lium scp Copy files between the local machine and pods. Upload is the default; `-d` flips the direction. ```bash lium scp [OPTIONS] TARGETS SOURCE_PATH [DESTINATION_PATH] TARGETS Pod name/ID, index, comma-separated list, or "all" SOURCE_PATH Local file (upload) or remote path (download) DESTINATION_PATH Optional; for multiple pods a download destination must be a directory -d, --download Download from the pods to the local machine ``` Examples: ```bash lium scp 1 ./script.py # upload to ~/script.py on pod #1 lium scp eager-wolf-aa ./data.csv ~/data/ # upload into a directory lium scp all ./config.json # upload to every pod lium scp 2 /root/output.log ./outputs -d # download from pod #2 into ./outputs/ ``` There is no `-r/--recursive` and no `-p/--preserve`; use [`lium rsync`](#lium-rsync) for directories. ## lium rsync Sync a directory to pods with rsync. It takes no options. ```bash lium rsync TARGETS LOCAL_PATH [REMOTE_PATH] TARGETS Pod name/ID, index, comma-separated list, or "all" LOCAL_PATH Local directory to sync REMOTE_PATH Optional destination path ``` ## lium rm Remove (terminate) pods. Removal is irreversible. The command exits non-zero when nothing matched `TARGETS`, so a typo cannot look like a successful teardown. ```bash lium rm [OPTIONS] [TARGETS] TARGETS Pod name(s)/ID(s), index/indices, comma-separated list, or "all" -a, --all Remove all active pods -y, --yes Skip the confirmation prompt --in TEXT Schedule the removal after a duration (e.g. 6h) --at TEXT Schedule the removal at a time (e.g. "tomorrow 01:00") ``` `--in` and `--at` **schedule** a removal rather than filtering which pods to remove; cancel a scheduled one with [`lium schedules rm`](#lium-schedules). **Agent usage** — `-y` exists, no piped `yes` needed: ```bash lium rm my-pod -y # single pod lium rm -a -y # all pods lium rm 1,2,3 -y # several by index lium rm my-pod --in 6h # schedule removal in six hours ``` ## lium logs Stream logs from a pod. ```bash lium logs [OPTIONS] POD_ID POD_ID Pod name, HUID or UUID — NOT an index -n, --tail INTEGER Number of lines to show from the end of the logs -f, --follow Follow log output ``` Examples: ```bash lium logs abc123 # last 100 lines lium logs abc123 -n 50 # last 50 lines lium logs abc123 -f -n 10 # follow, with 10 lines of history ``` ## lium port-forward Forward a local port to a pod's internal port. Useful for Jupyter, TensorBoard and other web services. ```bash lium port-forward [OPTIONS] TARGET PORT TARGET Pod name/ID or index PORT The internal port on the pod to forward to -l, --local-port INTEGER Local port to bind (defaults to the same as PORT) ``` Examples: ```bash lium port-forward my-pod 8888 # localhost:8888 -> pod's 8888 lium port-forward 1 8000 -l 3000 # localhost:3000 -> pod's 8000 ``` ## lium reboot Reboot pods. ```bash lium reboot [OPTIONS] [TARGETS] TARGETS Pod name(s)/ID(s), index/indices, or "all" -a, --all Reboot all active pods --volume-id TEXT Volume ID to attach when rebooting ``` A reboot re-creates the pod: everything outside an attached volume is lost. ## lium update Update the configuration of a running pod. ```bash lium update [OPTIONS] TARGET TARGET Pod name/ID or index --jupyter INTEGER Install Jupyter Notebook on the given internal port ``` ## lium templates List available Docker templates and images. It takes no options. ```bash lium templates [SEARCH] SEARCH Text search to filter templates (e.g. "pytorch", "tensorflow") ``` **Notes**: - Without `--template_id`, `lium up` uses the default **PyTorch (CUDA)** template — fastest to start - Default Docker-in-Docker (dind) image: `daturaai/dind` ## lium volumes Manage persistent volumes. ```bash lium volumes list # list all volumes lium volumes new NAME [-d DESC] # create a volume (-d, --desc) lium volumes rm INDICES [-y, --yes] # remove by index from the last `lium volumes list` ``` `volumes rm` takes **indices from the previous listing**, not names or HUIDs — run `lium volumes list` first. ## lium bk (backups) Manage pod backup configurations. `POD_ID` is a pod name/ID or an index from `lium ps`. ```bash lium bk show POD_ID # show the backup config lium bk set POD_ID [OPTIONS] # set or update it --path TEXT Backup path (default: /root) --every TEXT Backup frequency (1h, 6h, 24h) --keep TEXT Retention period (1d, 7d, 30d) -y, --yes Skip the confirmation prompt lium bk now POD_ID [OPTIONS] # trigger an immediate backup -n, --name TEXT Backup name (e.g. 'pre-release') -d, --description TEXT Backup description lium bk logs [POD_ID] [--id ID] # backup logs, or details of one backup lium bk restore POD_ID --id ID # restore a backup (--id is required) --to TEXT Restore path (default: /root) -y, --yes Skip the confirmation prompt lium bk restore-logs [POD_ID] [--id ID] lium bk rm POD_ID [-y] # remove the backup config ``` `--path` on `bk set` is a flag, not a positional: `lium bk set 1 --path /root --every 6h --keep 7d`. ## lium schedules Manage scheduled pod terminations (the ones created by `lium rm --in/--at` and by `lium up --ttl/--until`). ```bash lium schedules list # list all pods with scheduled terminations lium schedules rm INDICES # cancel by index from the listing ``` ## lium ssh-keys Manage the SSH public keys registered with Lium. ```bash lium ssh-keys list # list the keys registered with Lium lium ssh-keys sync # register every local SSH pubkey that isn't on Lium yet ``` ## lium config Manage the CLI configuration (`~/.lium/config.ini`). ```bash lium config show # display the entire configuration lium config get api.api_key # get one value lium config set ssh.key_path ~/.ssh/key # set one value (interactive without VALUE) lium config unset api.api_key # remove one value lium config path # print the config file path lium config reset [--confirm] # reset to defaults lium config edit # open in the default editor ``` ## lium theme Set the CLI color theme. The argument is required and accepts only two values. ```bash lium theme {dark|light} ``` ## lium fund Fund the account with TAO — or with free Subnet-51 alpha stake — from a Bittensor wallet. **Always pass `-y` for agent use.** ```bash lium fund [OPTIONS] -w, --wallet TEXT Bittensor wallet name to fund from -a, --amount TEXT Amount to fund with (TAO; USD when --alpha) --alpha Fund with free Subnet-51 alpha stake -k, --hotkey TEXT Origin hotkey the alpha is staked under — SS58 address or wallet hotkey name (required with --alpha) --json Print machine-readable JSON -y, --yes Skip confirmation prompts ``` Examples: ```bash lium fund -w default -a 1.5 -y lium fund --alpha -k -a 25 -y --json # -a is USD when --alpha ``` ## lium topup Top up the balance with a stablecoin. ```bash lium topup currencies [OPTIONS] # list supported stablecoins and networks --refresh Bypass the cache and re-fetch --json Print machine-readable JSON lium topup create [OPTIONS] # create an invoice, print the deposit address -a, --amount FLOAT Top-up amount in USD (required) -c, --currency TEXT Stablecoin code, e.g. USDT (required) -n, --network TEXT Network, e.g. tron (required) --json Print machine-readable JSON ``` Send exactly the returned `crypto_amount` to the deposit address on that network; the balance is credited once the transfer confirms. ```bash lium topup create -a 20 -c USDT -n tron --json ``` ## lium mine Bootstrap a Subnet-51 **provider** machine: clone `Datura-ai/lium-io` into `compute-subnet`, install the executor tooling, write `neurons/executor/.env` and start the executor container. It runs on the GPU host you are contributing, not on a renter's laptop. `lium provider --help` calls this "renter workflows" — that blurb is wrong; the code clones and starts a miner executor. ```bash lium mine [OPTIONS] -k, --hotkey TEXT Miner hotkey SS58 address -d, --dir TEXT Target directory -b, --branch TEXT Branch to install from -a, --auto Run without prompting -v, --verbose Show the plan banner ``` ## lium provider Provider-side commands for Subnet 51 mining — a different persona from `lium mine`. Hotkey registration on SN51 itself is done with `btcli subnet register`, not here. ```bash lium provider [OPTIONS] COMMAND [ARGS]... -w, --coldkey TEXT Bittensor coldkey (wallet) name; falls back to LIUM_PROVIDER_COLDKEY, then `provider.coldkey` in the config -k, --hotkey TEXT Hotkey name on that coldkey; falls back to LIUM_PROVIDER_HOTKEY, then `provider.hotkey` --portal-url TEXT Override the lium-miner-portal base URL --json Machine-readable JSON (one envelope per command) --debug Error context on stderr; verbose logging -y, --yes Auto-confirm the persona gate for spend-affecting subcommands --dry-run Skip irreversible subprocess calls (e.g. ssh), report intent only ``` Sub-commands: `billing`, `config`, `machine`, `machine-request`, `node`, `portal`, `status`, `sync`. Run `lium provider --help` for their flags. ## lium gpu-splitting Prepare Docker storage on a host for LIUM GPU splitting. ```bash lium gpu-splitting check [--device PATH] # inspect the host, print the plan, change nothing lium gpu-splitting setup [--device PATH] --yes # end-to-end Docker storage setup lium gpu-splitting verify # verify the host meets the requirements ``` `setup` is the only one that changes the host, and it stops on an interactive confirmation of the plan — pass `--yes` from a script. ## Batch Operations `exec`, `scp`, `rsync`, `rm` and `reboot` take several targets at once, as a comma-separated list or `all`: ```bash lium exec 1,2,3 "apt update" lium exec all "nvidia-smi" lium scp all ./requirements.txt lium rsync all ./project lium rm 1,2,3 -y ``` ## Pod Targeting The pod argument is called `TARGET` (single) or `TARGETS` (several) in the CLI's own help; `logs`, `ps` and the `bk` sub-commands call it `POD_ID`. What each form accepts is **not** uniform: | Form | Example | Accepted by | |------|---------|-------------| | Name / HUID / UUID | `lium ssh eager-wolf-aa` | every command | | Index from the last `lium ps` | `lium ssh 1` | `ssh`, `exec`, `scp`, `rsync`, `rm`, `reboot`, `update`, `port-forward`, `bk *`, `audit --pod` (since 0.0.37) — **not** `ps` and **not** `logs` | | Comma list | `lium exec 1,2,3 "cmd"` | `TARGETS` commands only | | All | `lium exec all "cmd"` | `TARGETS` commands only | `lium ps 1` and `lium logs 1` match the literal string `1` against pod names and IDs; they do not resolve indices, so they report the pod as not found unless a pod is actually named `1`. Indices come from the most recent listing and shift whenever anything is created or removed. Prefer names: read them once with `lium ps --format json` and pass those. ## Environment Variables ```bash LIUM_API_KEY=xyz lium ls # override the API key LIUM_DEBUG=1 lium up --gpu H100 -y # debug output LIUM_BASE_URL=https://staging.lium.io/api lium signup --email ada@example.com LIUM_SIGNUP_PASSWORD=pw lium signup --email ada@example.com # keeps the password off argv LIUM_PROVIDER_COLDKEY=... LIUM_PROVIDER_HOTKEY=... lium provider status ``` `LIUM_BASE_URL` (default `https://lium.io/api`, the `/api` suffix included) points the SDK **and** `lium signup` at another backend — use it to sign up against staging. `LIUM_PAY_URL` overrides the payments backend the same way. There is no `LIUM_SSH_KEY` variable — the SSH key path lives in the config (`lium config set ssh.key_path ...`). ## Exit Codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | General error | | 2 | Configuration error (bad arguments, unreadable script, missing config, no API key) | | 3 | API error — the API refused or failed the call: 401 (bad or revoked key), 404, 429, 5xx | | 4 | SSH error | | 5 | Pod not found | | 6 | Permission denied — the API answered 403 (an empty balance on `lium up`, for one) | Since **0.0.31** (lium#104, DAH-2593) every command runs under one error handler (`handle_errors` in `lium/cli/utils.py`), so the table holds for all of them: `lium ps` with a revoked key exits `3`; `lium up` with an empty balance exits `6`. `lium ls` reads the public node list and succeeds with any key, so it is not a key check. Inside a batch the rule changes: `rm`, `reboot`, `scp`, `rsync` (and `volumes rm`, `schedules rm`) finish the batch and exit `1` naming the items that failed, whatever the API answered for them (`Failed to remove pods: brave-orbit-b9`); only their target lookup before the batch exits `3`/`6`. The exceptions are `lium provider …`, which keeps its own map (`1` arguments, `2` auth, `3` portal, `5` ssh, `6` config missing, `7` token-cache contention), and `lium gpu-splitting …`, which runs on the host without the API and exits `1` on any failure. `lium exec` exits with the remote command's code (`5` when no pod matched, `2` on a bad argument or unreadable script). Under `--json` (`exec`, `describe`, `balance`, `fund`, `topup`, `signup`) a failure is one JSON object on stderr, `{"ok": false, "error": {"code": ..., "message": ...}}`, and stdout stays empty; `--format json` (`ls`, `ps`) reports a failure as text. `lium audit` *(since 0.0.37)* adds nothing to the table: its 401 is exit `3` like every other command's, with the hint that if the key works for `lium ps` the backend does not yet open `/users/me/events` to API keys. --- # Python SDK — Full Reference # Lium Python SDK Reference ## Table of Contents - [Installation & Auth](#installation--auth) - [High-Level SDK (lium.sdk.Lium)](#high-level-sdk-liumsdklium) - [@machine Decorator](#machine-decorator) - [Low-Level SDK (lium.Client)](#low-level-sdk-liumclient) - [Models](#models) - [Exceptions](#exceptions) ## Installation & Auth ```bash pip install lium.io # CLI + high-level SDK pip install lium-sdk # low-level SDK only ``` Authentication (auto-loaded in priority order): 1. Direct: `Lium(api_key="...")` or `Client(api_key="...")` 2. Environment: `LIUM_API_KEY` 3. Config file: `~/.lium/config.ini` (set via `lium init`) SSH keys auto-discovered from `~/.ssh/id_ed25519`, `~/.ssh/id_rsa`, `~/.ssh/id_ecdsa`. --- ## High-Level SDK (lium.sdk.Lium) Full-featured SDK included with `pip install lium.io`. Mirrors CLI capabilities. ```python from lium.sdk import Lium lium = Lium() ``` Signatures below follow Python notation: everything after `*` is keyword-only and raises `TypeError` when passed positionally. `lium.exec(pod, "nvidia-smi")` fails — it has to be `lium.exec(pod, command="nvidia-smi")`. ### Discovery | Method | Description | |--------|-------------| | `ls(*, gpu_type=, gpu_count=, lat=, lon=, max_distance_miles=, min_cuda_version=)` | List available executors | | `ls(…, nvlink=True, min_download_mbps=2000)` | Only nodes whose validator saw every GPU pair on NVLink / whose Download (Mbps, the `lium ls` figure) is at least that; unreported nodes excluded; sent to the API and applied client-side too *(since 0.4.0)* | | `ps()` | List active pods | | `pod(pod_id)` | Get pod details | | `get_executor(executor_id)` | Get executor details | | `templates(filter=, only_my=)` | List templates | | `gpu_types()` | List available GPU types | ### Pod Lifecycle | Method | Description | |--------|-------------| | `rent(*, gpu_type, gpu_count=1, name=, template_id=, min_vram_gb=, min_cpus=, min_ram_gb=, min_disk_gb=, min_download_mbps=, max_price_per_gpu_hour=, country=, dry_run=)` | **Coming with lium#209 — not in 0.0.37, the latest release** (`pip show lium.io`; until it ships use `ls()` + `up(executor_id=)` below). Rent the cheapest node matching a spec in one call (the backend picks when `GET /version` lists `rent_by_spec` — production does today; against an older backend the client picks the cheapest exact match; `dry_run=True` prices without renting). Returns `RentResult` with `.pod`, `.executor`, `.price_per_hour` | | `up(*, executor_id, name=, template_id=, volume_id=, ports=, ssh_keys=)` | Create pod on a named node | | `down(pod)` | Stop/delete pod | | `rm(pod)` | Alias for `down()` | | `reboot(pod, volume_id=)` | Reboot pod | | `wait_ready(pod, *, timeout=)` | Poll until pod is RUNNING | | `logs(pod_id, *, tail=, follow=)` | Stream pod logs | | `edit(pod_id, **kwargs)` | Edit pod template | ### Remote Execution | Method | Description | |--------|-------------| | `exec(pod, *, command, env=)` | Execute command, returns `{"stdout", "stderr", "exit_code", "success"}` | | `stream_exec(pod, *, command, env=)` | Stream execution output | | `exec_all(pods, *, command, env=, max_workers=)` | Execute on multiple pods | | `ssh(pod)` | Get SSH command string | ### File Transfer | Method | Description | |--------|-------------| | `scp(pod, *, local, remote)` | Copy file to pod | | `upload(pod, *, local, remote)` | Upload (alias for scp) | | `download(pod, *, remote, local)` | Download file from pod | | `rsync(pod, *, local, remote)` | Sync directory | ### Template Management | Method | Description | |--------|-------------| | `default_docker_template(executor_id)` | Get executor's default template | | `create_template(...)` | Create custom template | | `update_template(template_id, name=, docker_image=, ...)` | Update template | | `switch_template(pod, *, template_id)` | Change pod's template | | `wait_template_ready(template_id, timeout=)` | Wait for template build | ### Volume Management | Method | Description | |--------|-------------| | `volumes()` | List all volumes | | `volume(volume_id)` | Get volume info | | `volume_create(name, *, description=)` | Create volume | | `volume_update(volume_id, *, name=, description=)` | Update volume | | `volume_delete(volume_id)` | Delete volume | ### Backup Management | Method | Description | |--------|-------------| | `backup_create(pod, *, path=, frequency_hours=, retention_days=)` | Set up auto-backups | | `backup_now(pod, *, name, description=)` | Trigger immediate backup | | `backup_config(pod)` | Get backup config | | `backup_list()` | List all backups | | `backup_logs(pod)` | Get backup execution logs | | `backup_delete(config_id)` | Delete backup config | | `restore(pod, *, backup_id, restore_path=)` | Restore from backup | ### Pod Scheduling | Method | Description | |--------|-------------| | `schedule_termination(pod, *, termination_time)` | Auto-terminate at specific time | | `cancel_scheduled_termination(pod)` | Cancel auto-termination | ### Jupyter | Method | Description | |--------|-------------| | `install_jupyter(pod, *, jupyter_internal_port)` | Install Jupyter on pod | ### Account | Method | Description | |--------|-------------| | `balance()` | Get account balance | | `wallets()` | List connected wallets | | `add_wallet(bt_wallet)` | Add Bittensor wallet | | `get_my_user_id()` | Get current user ID | | `events(*, since=, pod_id=, api_key_id=, limit=200)` | The account's event log, newest first: each entry names the session or API key (`actor.api_key_id` / `api_key_name`, `None` for the platform) behind a rent, reboot, edit or delete; `pod_id` also answers for a deleted pod *(since 0.0.37; the backend side for API keys is lium-platform#208, not released)* | ### Complete Example ```python from lium.sdk import Lium lium = Lium() # lium.io 0.0.37 (the latest release): list, pick the cheapest 8xA100 yourself, rent it by id executors = lium.ls(gpu_type="A100", gpu_count=8) cheapest = min(executors, key=lambda e: e.price_per_hour) pod = lium.wait_ready(lium.up(executor_id=cheapest.id, name="my-pod"), timeout=600) # Coming with lium#209 (not in 0.0.37): the same in one call, no listing # rented = lium.rent(gpu_type="A100", gpu_count=8, min_cpus=32, name="my-pod") # pod = lium.wait_ready(rented.pod, timeout=600) # Execute result = lium.exec(pod, command="nvidia-smi") print(result["stdout"]) # Files lium.upload(pod, local="train.py", remote="/root/train.py") lium.exec(pod, command="python /root/train.py") lium.download(pod, remote="/root/model.pt", local="./model.pt") # Backups lium.backup_create(pod, path="/root/data", frequency_hours=24, retention_days=7) # Cleanup lium.down(pod) ``` --- ## @machine Decorator Run one Python function on a GPU pod: rents the cheapest node matching `machine`, ships the function's `def`, installs `requirements` once per pod (on top of the image's own packages — torch is already there on the PyTorch template), streams the function's stdout/stderr live, returns the result or re-raises the remote exception, removes the pod or keeps it warm. ```python import lium @lium.machine(machine="1xH200", requirements=["transformers", "accelerate"], timeout=900, keep_warm=300) def run(model_name: str, prompt: str) -> str: import torch from transformers import AutoModelForCausalLM, AutoTokenizer tok = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, dtype=torch.bfloat16, device_map="cuda") ids = tok(prompt, return_tensors="pt").to("cuda") return tok.decode(model.generate(**ids, max_new_tokens=64)[0], skip_special_tokens=True) answer = run("Qwen/Qwen2.5-0.5B-Instruct", "What is the capital of France?") run.close() # remove the warm pod now ``` **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `machine` | str | `"x"` or `""`: `"1xH200"`, `"RTX4090"`, `"2xA100"`. Count defaults to 1. Cheapest matching node is rented. | | `requirements` | list, optional | pip packages, installed once per pod into a venv that also sees the image's packages | | `template_id` | str, optional | Docker template to rent with (default: the node's default template) | | `timeout` | float, default 3600 | seconds the function may run; `None` = no process limit. Pod removal is scheduled at `timeout + 15 min` (plus `keep_warm`), or 24 h when `timeout=None` | | `keep_warm` | float, default 0 | seconds the pod stays after a call for the next one (also from the next run of the script); removal re-armed to `keep_warm + 2 min` after each call | | `cleanup` | bool, default True | `False` skips the `down()` after the call and turns off pod reuse: `keep_warm` has no effect, no pod is kept for the next call, and `f.map()` rents one pod per item. Each pod still goes at its scheduled removal time | | `local` | bool, default False | run in-process (`LIUM_MACHINE_LOCAL=1` does it for every function; since 0.0.40, lium#208) | | `quiet` | bool, default False | suppress the `[lium]` progress lines on stderr | **On the decorated function:** `f.remote(*a)` (= `f(*a)`), `f.local(*a)`, `f.map(iterable)` (one item per call, all on one pod), `f.close()`. **What travels:** only the function's own `def` (decorators/annotations stripped) plus pickled arguments (your bytes, loaded on your pod). The result is not pickled: it comes back as a JSON envelope plus an `.npz` sidecar for numpy arrays, read with `allow_pickle=False`. What round-trips, each as its own type: `None`, `bool`, `int`, `float`, `str`, `bytes`; `list`, `tuple`, `set`, `frozenset`, `dict` of those, nested; `datetime`/`date`/`time`/`timedelta`, `Decimal`, `pathlib.Path`, `uuid.UUID`; `numpy.ndarray` (any dtype without Python objects) and numpy scalars. Anything else — a tensor, `torch.__version__`, a dataclass, an `Enum` — is a `lium.ResultEncodingError` raised on the pod naming the type; return `str(...)`, `.tolist()`, `.cpu().numpy()`, `dict(x)` instead. Import inside the body; a closure variable or a module-level name used inside is refused when the function is decorated (`LiumError` naming it). Nested functions and `async def` work; lambdas do not, and a method's `self` is pickled by reference, so it works only when its class is importable on the pod (not a class defined in the script). **Errors:** a remote exception of a builtin type (`ValueError`, `RuntimeError`, …) is re-raised with its own type and `e.__cause__` is `lium.RemoteExecutionError` with `exception_type`, `remote_traceback`, `exit_code`, `stdout`, `stderr`; any other class (`torch.OutOfMemoryError`, …) arrives as `RemoteExecutionError` itself, its name in `exception_type`, no `__cause__`. Timeout → `RemoteExecutionError: exceeded timeout=Ns and was killed`, no `__cause__`; no matching node or a failed rental → `LiumError`. Prints from the pod appear on the caller's terminal while the function runs. **Progress lines (stderr):** ``` [lium] run: renting 1xH200 $2.75/h (swift-fox-c8, United States), removal in 0.6h [lium] run: pod ready in 45s [lium] run: preparing environment (2 package(s): transformers, accelerate) [lium] run: environment ready in 31s [lium] run: running [lium] run: done in 118s (~$0.0901) [lium] run: pod stays warm 300s ``` Measured (6 Sep 2026, 1×RTX 4090 at $0.30/h): cold call ~70 s (~$0.006), warm call ~19 s, `transformers`+`accelerate` install 32 s once per pod. Requires the decorator surface since 0.0.40 (lium#208). --- ## Low-Level SDK (lium.Client) Resource-based client from `pip install lium-sdk`. Context-manager pattern. ### Sync Client ```python import lium with lium.Client(api_key="optional") as client: pods = client.pods.list() ``` ### Async Client ```python import asyncio, lium async def main(): async with lium.AsyncClient() as client: pods = await client.pods.list() asyncio.run(main()) ``` ### Resources **client.pods:** | Method | Description | |--------|-------------| | `list()` → `list[PodList]` | List user's pods | | `retrieve(id, wait_until_running=False, timeout=300)` → `Pod` | Get pod, optionally wait | | `create(id_in_site, pod_name, template_id, user_public_key)` → `Pod` | Low-level create | | `delete(id_in_site)` → `None` | Delete pod | | `list_executors(filter_query=None)` → `list[Executor]` | List available machines | | `easy_deploy(machine_query, docker_image=, dockerfile=, template_id=, pod_name=)` → `Pod` | High-level deploy | **machine_query format for easy_deploy:** - `"H100"` — any H100 - `"1xA6000"` — exactly 1x A6000 - `"2xA100"` — exactly 2x A100 - `"H200,A100"` — H200 or A100 **client.templates:** | Method | Description | |--------|-------------| | `list()` → `list[Template]` | List templates | | `retrieve(template_id)` → `Template` | Get template | | `create(...)` → `Template` | Create template | | `delete(template_id)` → `None` | Delete template | **client.ssh_keys:** | Method | Description | |--------|-------------| | `list()` → `list[SSHKey]` | List uploaded SSH keys | | `create(name: str, public_key: str)` → `SSHKey` | Upload public key | | `delete(key_id: UUID)` → `None` | Remove SSH key | **client.docker_credentials:** | Method | Description | |--------|-------------| | `list()` → `list[DockerCredentials]` | List stored registry credentials | | `create(registry: str, username: str, password: str)` → `DockerCredentials` | Add registry credentials (for private images) | | `delete(cred_id: UUID)` → `None` | Remove credentials | --- ## Models ### ExecutorInfo (high-level SDK) | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Executor identifier | | `huid` | `str` | Human-readable ID (e.g. "cosmic-hawk-f2") | | `gpu_type` | `str` | GPU model ("H100", "A100", etc.) | | `gpu_count` | `int` | Number of GPUs | | `price_per_hour` | `float` | USD per hour | | `location` | `str` | Country/region | | `specs` | `dict` | Hardware specs (RAM, storage, etc.) | | `status` | `str` | Availability status | | `docker_in_docker` | `bool` | DinD support | | `ip` | `str` | Machine IP | | `interconnect` / `nvlink` | `dict` / `bool` (both `None` until reported) | How the GPUs are wired (`nvidia-smi topo -m` summary: `gpu_pairs`, `nvlink_pairs`, `nvlink_links`, `pcie_class`, `p2p`, `matrix`) and the NVLink verdict *(since 0.4.0)* | | `link` / `p2p` | properties | `"NV18"` / `"PCIe/SYS"` / `None`; every GPU pair can read the other's memory *(since 0.4.0)* | ### Executor (low-level SDK) | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Executor identifier | | `gpu_type` | `str` | GPU model | | `gpu_count` | `int` | Number of GPUs | | `price` | `float` | USD per hour | | `location` | `str` | Country/region | | `driver_version` | `str` | NVIDIA driver version | | `docker_in_docker` | `bool` | DinD support | ### PodInfo / Pod | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Pod identifier | | `name` | `str` | Pod name | | `status` | `str` | "RUNNING", "STOPPED", "PENDING", etc. | | `huid` | `str` | Human-readable ID | | `ssh_cmd` | `str` | Ready-to-use SSH command | | `ssh_ip` | `str` | SSH host | | `ssh_port` | `int` | SSH port | | `ports` | `list[dict]` | Allocated port mappings | | `executor` | `Executor` | Associated executor info | | `template` | `Template` | Docker template used | | `created_at` | `datetime` | Creation timestamp | | `removal_scheduled_at` | `datetime | None` | Scheduled termination time | | `jupyter_url` | `str | None` | Jupyter URL if enabled | ### Template | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Template identifier | | `name` | `str` | Template name | | `huid` | `str` | Human-readable ID | | `docker_image` | `str` | Docker image name | | `docker_image_tag` | `str` | Image tag | | `category` | `str` | Template category (ml, web, etc.) | | `status` | `str` | Build status | ### VolumeInfo | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Volume identifier | | `huid` | `str` | Human-readable ID | | `name` | `str` | Volume name | | `description` | `str` | Volume description | | `current_size_bytes` | `int` | Current storage used | | `current_file_count` | `int` | Number of files | ### BackupConfig | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` | Config identifier | | `pod_executor_id` | `UUID` | Associated pod | | `backup_frequency_hours` | `int` | Backup interval in hours | | `retention_days` | `int` | Days to keep backups | | `backup_path` | `str` | Path being backed up | | `is_active` | `bool` | Whether backups are enabled | --- ## Exceptions High-level SDK (`lium.sdk`): | Exception | Trigger | |-----------|---------| | `LiumError` | Base exception | | `LiumAuthError` | Invalid API key (401) | | `LiumNotFoundError` | Resource not found (404) | | `LiumRateLimitError` | Rate limit exceeded (429) | | `LiumServerError` | Server errors (5xx) | | `PodStartError` | the pod reached a terminal state (`FAILED`, `STOPPED`, gone) while being waited for; a slow pod is `None`, not this (since 0.0.37) | | `RemoteExecutionError` | an `@lium.machine` call returned no result: carries `exception_type`, `remote_traceback`, `exit_code`, `stdout`, `stderr`; builtin exceptions re-raise with it as `__cause__` (since 0.0.40, lium#208) | | `ResultEncodingError` | (a `TypeError`) the function's return value is not in the round-trip list — JSON scalars/containers, bytes, numpy arrays (since 0.0.40, lium#208) | Enable debug logging: ```python import logging logging.basicConfig(level=logging.DEBUG) ``` Or set `LIUM_DEBUG=1` environment variable.