Admin API

Every /api/admin/* endpoint needs Authorization: Bearer <key> — the key is the api_key file in the data directory — reads included. The one exception is the WebSocket upgrade /api/admin/ws, which takes a single-use ticket from POST /api/admin/ws-ticket instead. Admin endpoints are also CORS-protected.

Node Management

GET /api/admin/stats

Node statistics and hardware info.

Its network_traffic figure (also on /v1/status and the dashboard's stats) says what this node's upload and download are made of. Since 0.3.243, direct_by_kind lists this node's own messages by kind, and by_protocol lists every byte by the network protocol its connection used, with opened_by: us for what this node asked, peer for what it answered. The by_protocol rows add up to out_bytes and in_bytes. Since 0.3.244, kad_inbound has one row for each kind of Kademlia request other nodes send this one (find_node, get_provider, add_provider, get_record, put_record, zeros included): requests handled, and the peers its answers listed — closer_peers, and provider_peers for the holders it named.

GET /api/admin/peers

Connected peers with latency, trust scores, and hosted models.

GET /api/admin/credits

The internal ledger figures (balance, lifetime earned and spent, escrow) and the tier name. Credits are dormant: the balance is self-minted, gates nothing, and the tier is the same for everyone.

GET /api/admin/credits/transactions

A bounded log of recent balance movements (delta, kind, reason, resulting balance), oldest first. lifetime_refunded is partly synthetic: unexplained gaps are attributed to refunds so the books close.

GET /api/admin/diagnostics

Plain-text health report, meant to be read in a terminal or pasted into a bug report. Covers internet reachability and NAT status, hole-punching counts, the last 50 completed requests with their routes and per-segment timings, per-peer serving performance, what this node has served for others, and recent failures including which peer served each one. See Troubleshooting for how to read it.

Network addresses are redacted unless ?full=1 is passed — replaced by a placeholder naming their kind, with transport, port, peer id and /p2p-circuit structure left intact. The default is the safe one because this endpoint exists to be shared: the dashboard's Copy diagnostics button and swarmllm diagnostics both use it, and the report otherwise carries this machine's addresses plus every remembered peer address.

GET /api/admin/performance

The JSON sibling of diagnostics, used by the dashboard's Performance view.

FieldContents
recentUp to 50 completed requests, newest first: route, per-phase timings, per-segment attribution (node, layer range, elapsed, transport, region), tokens, outcome
peersEvery peer that has served part of a request: round-trip time, ms per layer, latency EWMA, sample count, region — slowest first
servedSegments and layers this node computed for others, compute time, activation bytes returned, and ms per layer
hourlyOne bucket per hour for the past week: request and error counts, average total time, average time-to-first-token, average throughput, and counts by route. Persisted, so it survives a restart

Fetched on demand rather than pushed over the WebSocket. The per-peer and per-request detail here is high-cardinality by nature; it is served when asked for and never retained beyond the bounded rings above.

GET /api/admin/network-map

Geographic distribution of peers and shards across regions. Each entry includes the total peer count for that region, per-model shard-holder counts, per-model request demand rates, coverage gaps (models with zero holders in the region), and per-model replication targets derived from pool size and demand. Includes the local node in its auto-detected or configured region.

Response:

{
  "regions": {
    "US": {
      "total": 3,
      "models": { "tinyllama-1.1b-q4-k-m": 2 },
      "demand": { "tinyllama-1.1b-q4-k-m": 5 },
      "coverage_gaps": [],
      "replication_target": { "tinyllama-1.1b-q4-k-m": 2 }
    }
  },
  "recent_routes": [
    { "model": "qwen2.5-coder-7b-instruct-q4-k-m", "regions": ["TH", "IT"], "ok": true }
  ]
}

recent_routes lists this node's recent requests that left its region: the regions each one crossed, starting here, and whether it succeeded.

GET/PUT /api/admin/config

Read or update daemon configuration. Both need the API key.

PUT changes only the settings the dashboard exposes (contribution level, disk, bandwidth, concurrency, auto-manage, update mode, relaying, LAN trust, …). A key it does not apply is refused with a 400 naming it; for a config.toml setting such as network.enable_upnp or network.external_addresses, the message says which section of config.toml to put it in.

POST /api/admin/config/reload

Re-read config.toml and apply it to the running node. Bearer auth required.

The response separates what took effect from what did not:

  • applied — in force now. Most settings are simply re-read by whatever acts on them; a few (concurrency limit, batch size and window, auto-manage interval, session timeout) are pushed to the subsystem that has to resize or retime something.
  • restart_required — read once at startup and unchanged by this call. Only max_peers is in this group: connection limits are fixed when the peer-to-peer network is built.

POST /api/admin/shutdown

Gracefully shut down the node. Localhost only, Bearer auth required.

POST /api/admin/api-key/rotate

Issue a new API key and invalidate the old one. It takes effect when the node next starts — the running server keeps the key it started with, and logs that a restart is needed.

Model Management

GET /api/admin/models

List models with shard status, VRAM estimates, and acquisition state. Each model includes:

  • mmproj field with available (bool), local (bool), and holders (count) for VLM vision encoder status
  • trust_level field: one of "Discovered", "Pinned", "DemandVerified", or "NetworkPopular" indicating the model's trust status (auto-manage only downloads shards for DemandVerified+ or Pinned models, OR for any model where this node already hosts at least one shard — gap-filling exemption). R141: trusted curator publishers (meta-llama, Qwen, mistralai, bartowski, unsloth, etc.) promote at 10k HF downloads instead of 100k

POST /api/admin/models/{id}/add

Trigger model acquisition from the network.

GET /api/admin/models/{id}/status

Check model acquisition progress.

DELETE /api/admin/models/

Remove model (shards + manifest + state).

DELETE /api/admin/models/{id}/shards/

Delete a single shard.

GET/PUT /api/admin/models/{id}/auto-manage

Per-model auto-manage policy (including prune toggle).

GET/PUT/DELETE /api/admin/models/{id}/encrypted-pipeline

Per-model "Start and finish on this computer" setting (encrypted_pipeline). It is decided, first match wins, by: your choice for this model; an explicit global encrypted_pipeline = true; otherwise it is on exactly while this node holds both the first and last part of the model (encrypted_pipeline_auto, default on).

GET returns encrypted_pipeline (in force or not), decided_by (this_model, global_setting or automatic), per_model_override (your choice, or null), default_for_this_model (what it would be with no choice), global_default, automatic_when_both_ends_here, readiness (ready, has_first_shard, has_last_shard) and an overhead note.

PUT {"enabled": true} or {"enabled": false} stores a choice. Turning it on without both ends is recorded, and requests for the model are then refused until the parts are here (fails closed). A warning is logged for 2-part models (fully local, no distribution benefit). PUT {"enabled": null} or DELETE forgets the choice, so the global setting and the automatic rule decide again. A body without enabled is a 400. The choice is persisted and survives restarts.

PUT /api/admin/models/{id}/shards/{index}/lock

Lock/unlock a shard to prevent auto-pruning.

GET /api/admin/models/{id}/pipeline-plan

A preview of how a request for this model would be routed now: the ordered segments and the candidate holders of each range. "No route right now" is an answer, not a failure — 200 with routable: false and a reason carrying the same message / hint a real request would get. Only a model id this node has never heard of is a 404.

GET /api/admin/reference-models

The pinned reference models (docs/REFERENCE_MODELS.md) offered as opt-in downloads, so a speed figure from one machine means the same as one from another.

Storage & Shards

POST /api/admin/rescan-shards

Rescan local shard files on disk and update the model registry and network announcements without restarting the daemon. Useful after manually placing shard files in the data directory. Bearer auth required.

Response:

{ "status": "ok", "models_updated": ["model-id-1"], "count": 1 }

GET /api/admin/models/{id}/metadata

Read parsed GGUF metadata from a locally-stored model header (gguf_header.bin). Returns architecture parameters, tokenizer settings, quantization type, and all raw metadata key/value pairs (tokenizer vocabulary arrays are excluded). Returns 400 if no header file exists for the model.

Response shape:

{
  "model_id": "...",
  "general": { "name": "...", "architecture": "llama", "architecture_supported": true, "file_type": 11, "quantization": "Q4_K_M" },
  "model": { "context_length": 4096, "block_count": 32, "embedding_length": 4096, "head_count": 32, "head_count_kv": 8, "rope_dimension_count": 128, "rope_freq_base": 500000.0, "layer_norm_rms_epsilon": 1e-5, "vocab_size": 32000 },
  "tokenizer": { "model": "llama", "pre": "...", "eos_token_id": 2, "bos_token_id": 1, "padding_token_id": null },
  "tensors": { "count": 291, "data_offset": 131072 },
  "raw": [{ "key": "general.architecture", "value": "llama" }, ...]
}

POST /api/admin/models/{id}/shards/{index}/download

Trigger a P2P download of a specific shard that is not yet held locally. The daemon first checks for P2P peers that hold the shard (picking the best peer by LAN-proximity, latency, and trust), then falls back to returning HuggingFace source info if no peers are available. Bearer auth required.

Responses:

  • { "status": "already_local", ... } — shard is already on disk
  • { "status": "downloading", "source": "p2p", "peer": "...", ... } — P2P download started
  • { "status": "use_hf", "source": "huggingface", "repo_id": "...", "filename": "...", ... } — no P2P peers, use hf/download-shards instead
  • 400 if no peers and no HuggingFace source known

POST /api/admin/models/{id}/shards/{index}/unload

Unload a single shard from memory (VRAM/RAM) without deleting the file from disk. Narrows the model's shard window to exclude this shard and restarts the worker subprocess. If this is the last loaded shard, the model is fully unloaded. Bearer auth required.

Response:

{ "status": "unloaded", "model_id": "...", "shard_index": 0, "remaining_loaded": [1, 2] }

POST /api/admin/models/{id}/shards/{index}/load

Load a shard that is on disk into memory. The shard must already be present locally (use /download first if not). Expands the model's shard window to include the shard and restarts the worker subprocess. Bearer auth required.

Response:

{ "status": "loaded", "model_id": "...", "shard_index": 0, "loaded_shards": [0, 1, 2] }

POST /api/admin/models/{id}/unload

Unload an entire model from memory (VRAM/RAM) without deleting any files from disk. Evicts all split-model entries, kills the worker subprocess, clears GGUF metadata cache, and clears the loaded-model record. Bearer auth required.

Response:

{ "status": "unloaded", "model_id": "...", "model_name": "...", "segments_removed": 2, "estimated_freed_mb": 4096 }

GET /api/admin/shard-storage

Per-model storage breakdown, disk and VRAM usage.

GET /api/admin/prune-history

Recent auto-prune events.

GET /api/admin/storage/breakdown

The figures behind the storage bar, in MB: total_mb, used_mb, free_mb, auto_target_mb (the cap on what auto-manage may hold in total, not head-room on top of used_mb), auto_remaining_mb, auto_limited_by, plus held_shards, auto_manage_enabled and the contribution level.

GET/PUT /api/admin/schedule

Resource schedule management.

HuggingFace Integration

GET /api/admin/hf/search?q=...

Search HuggingFace for GGUF models. The parameter is q — query is ignored, and a missing or empty q returns []. Optional tasks=chat,code,vision,multilingual,reasoning narrows the results. Returns results grouped by repository with quantization variants, recommended variant, and VRAM fitness indicator.

Response format:

[{
  "repo_id": "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF",
  "downloads": 50000,
  "likes": 120,
  "variants": [
    { "filename": "...Q4_K_M.gguf", "size_bytes": 668000000, "quant": "Q4_K_M" },
    { "filename": "...Q8_0.gguf", "size_bytes": 1100000000, "quant": "Q8_0" }
  ],
  "recommended_variant": "Q4_K_M",
  "fits_vram": true
}]

GET /api/admin/hf/probe?repo_id=...&filename=...

Probe a remote GGUF file (size, shard layout).

POST /api/admin/hf/download-shards

Download specific shard indices from HuggingFace. Bearer auth required.

Name exactly one of these, or the request is refused with a 400:

  • "shards": [0, 1, 2] — these parts.
  • "all_shards": true — every part of the file, worked out from the probe the endpoint runs anyway. What the Models tab's Download sends for a model that fits this computer.
  • "peer_fair_share": true — one seed part, chosen by BLAKE3(node_id || model_id) so different computers seed different parts. Other computers with auto-manage on pick up the rest only once the model clears the auto-manage trust gate, so on its own this does not make a model usable.
curl -X POST http://localhost:8800/api/admin/hf/download-shards \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"repo_id": "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF", "filename": "qwen2.5-coder-7b-instruct.Q4_K_M.gguf", "all_shards": true}'

GET /api/admin/hf/source/

Look up the HuggingFace source (repo + filename) for a locally-known model. First checks the in-memory source cache and the probe cache, then auto-discovers by searching HuggingFace if neither has an entry. If found via auto-discovery the result is cached to the database and hf_source.json in the model directory.

Response:

{ "model_id": "...", "repo_id": "TheBloke/TinyLlama-...-GGUF", "filename": "tinyllama-...Q4_K_M.gguf" }

GET /api/admin/downloads

List the download queue with per-shard progress, speed, and source.

POST /api/admin/downloads/{model_id}/cancel

Cancel an in-progress download.

LoRA Adapters

A registered adapter is applied to any chat request that names it in lora_adapter. It runs only on the computer it was registered on, and only for a model that computer holds whole — a request that cannot use it is refused with a 400 saying why, never answered by the base model.

Registration is recorded in <data_dir>/adapters/<id>.adapter.json and survives a restart. If you replace the adapter file, unregister and register it again: a file whose contents changed since registration is refused.

GET /api/admin/adapters

List all registered LoRA adapters with their metadata (id, name, base model, rank, alpha, path).

Response: { "adapters": [ { "id": "...", "name": "...", "base_model": "...", "rank": 16, "alpha": 32.0, "path": "..." } ] }

POST /api/admin/adapters

Register a LoRA adapter from a safetensors file (PEFT naming, …layers.N.self_attn.q_proj.lora_A.weight; F32, F16 or BF16). Bearer auth required. The file must be inside <data_dir>/adapters/. id must be a plain name (no /, \ or leading .); if omitted, a UUID is generated. rank must match the file's matrices.

Request body:

{ "id": "my-adapter", "name": "My Adapter", "base_model": "tinyllama-...", "rank": 16, "alpha": 32.0, "path": "my-adapter.safetensors" }

path may be absolute or relative to <data_dir>/adapters/.

Response: { "status": "ok", "adapter": { ... } }

DELETE /api/admin/adapters/

Unregister a LoRA adapter. Does not delete the file from disk. Bearer auth required. Returns 400 if the id is not found.

Response: { "status": "ok", "message": "Adapter 'my-adapter' removed" }

Cloud Providers

GET /api/admin/providers

List configured cloud providers (name + configured flag, no keys exposed).

PUT /api/admin/providers

Update cloud provider API keys. Bearer auth required. Keys are encrypted at rest.

GET /api/admin/provider-models

List available models from all configured cloud providers. Results are cached for 30 seconds; stale results are returned immediately and refreshed in the background. Includes models from OpenAI, Anthropic (static list), DeepSeek, Mistral, Groq, NVIDIA NIM, Cerebras, SambaNova, Fireworks, Together AI, DeepInfra, and Moonshot/Kimi.

Calling this endpoint also refreshes the routing table that decides whether a model whose id carries no recognisable prefix is proxied to a provider or refused locally. Since v0.3.174 that table is also refreshed on its own every 15 minutes, so a node whose dashboard is never opened still routes its cloud models; before that it was filled only here, and a node that had not been visited refused them outright. A provider that fails to answer a refresh keeps the models it had, rather than having them dropped — a failed fetch used to be indistinguishable from a provider with no models.

Response: { "models": [ { "id": "gpt-4o", "name": "GPT-4o", "provider": "openai" } ] }

GET /api/admin/provider-health

Probe each configured provider by sending a tiny max_tokens=1 inference request (using a suitable test model per provider). All probes run in parallel with a connect timeout.

Response:

{ "providers": [ { "provider": "openai", "status": "up", "latency_ms": 320, "detail": "" } ] }

Status values: up, rate_limited, overloaded, timeout, unreachable, error_<code>.

POST /api/admin/provider-model-status

Probe availability and latency for a list of specific cloud model IDs (up to 20 per request). Sends a max_tokens=1 request to each model's provider endpoint. Anthropic models are skipped (no cloud proxy probing). Needs the API key, like every admin endpoint.

Request body: { "models": ["gpt-5.4", "claude-sonnet-5", "deepseek-v4-flash"] }

Response:

{ "models": [ { "model": "gpt-4o", "status": "up", "latency_ms": 210 } ] }

Status values: up, rate_limited, not_found, unavailable, timeout, error.

Claude Subscription (feature-gated)

Requires building with --features claude-subscription. When the feature is not enabled, these endpoints return {"error": "claude-subscription feature not enabled"}.

GET /api/admin/claude-subscription/status

Detect whether the claude CLI is installed and authenticated on this machine. Reads version from claude --version and subscription info from ~/.claude/.credentials.json (read-only).

Response:

{
  "cli_installed": true,
  "cli_version": "2.1.92 (Claude Code)",
  "authenticated": true,
  "subscription_type": "max",
  "rate_limit_tier": "default_claude_max_5x"
}

PUT /api/admin/providers (claude_subscription_enabled field)

Enable or disable the Claude subscription provider. Pass claude_subscription_enabled alongside other provider key updates.

{ "claude_subscription_enabled": true }

When enabled, claude-* model requests are routed through the local CLI subprocess instead of the Anthropic API key. The Anthropic API key (if configured) is used as fallback when disabled.

Updates

GET /api/admin/version

Current binary version info.

POST /api/admin/update/check

Check for available updates. Returns version info and changelog if update available.

POST /api/admin/update/apply

Download and apply an update. Bearer auth required.

Discovery

GET /api/admin/network-code

Get an encrypted shareable invite code and network phase. The code embeds the node's TCP listening address encrypted with ChaCha20Poly1305 — the IP is not visible in the code.

POST /api/admin/join-network

Join the network via encrypted invite code (swarm://...) or raw multiaddr. Immediately dials the peer and saves the address to the peer cache.

GET /api/admin/foreign-pool-catalog

Models other pools advertise — discovery only, and only when pool.share_model_catalog is on. Using them is a separate opt-in, pool.allow_cross_pool_inference (settable live through PUT /api/admin/config).

Responses API listing

GET /api/admin/responses

List stored Responses-API records (backs the dashboard's Responses tab). Optional query params: ?limit=N (cap on returned records, default 100, max 500) and ?status=... (filter by completed / in_progress / cancelled / failed / queued). See Responses API for the user-facing surface.

Auto-Manage Surfaces

GET /api/admin/swarm/capacity

A snapshot of the swarm's collective capacity: online nodes, total graphics memory and disk, GPU nodes, regions represented, which models it can serve, which it could with more holders, which this node hosts, and their redundancy.

GET /api/admin/swarm/capacity-plan

What-if scenarios over that snapshot, with a headline target naming how many more contributors a model needs.

GET /api/admin/wishlist

R111 — ranked list of models the swarm wants. Triggers a wishlist refresh on every call so the response always reflects current state. Response shape:

{
  "computed_at": 1727712345,
  "entries": [
    {
      "model_id": "mistral-7b-instruct-v0.3-Q4_K_M",
      "display_name": "Mistral 7B Instruct v0.3",
      "status": "serveable",
      "score": 78,
      "swarm_replicas": 4,
      "target_replicas": 3,
      "size_mb": 4368,
      "vram_required_mb": 5023,
      "shards_covered": 8,
      "total_shards": 8,
      "hosted_by_us": false,
      "why_tags": ["wishlist.why.popular_on_swarm", "wishlist.why.fits_your_memory"]
    },
    {
      "model_id": "hf-candidate:bartowski/Llama-3.2-3B-Instruct-GGUF",
      "display_name": "Llama-3.2-3B-Instruct-GGUF",
      "status": "candidate",
      "score": 65,
      "hf_repo_id": "bartowski/Llama-3.2-3B-Instruct-GGUF",
      "task_tags": ["chat"],
      "why_tags": ["wishlist.why.popular_on_hf", "wishlist.why.trusted_publisher", "wishlist.why.candidate_one_click"]
    }
  ]
}

status is one of hosting / serveable / aspirational / candidate (R141) / unreachable / blocked. Candidate entries carry an hf_repo_id field and a synthetic model_id (prefix hf-candidate:) so the frontend can route the click to the HF browse without colliding with real model_ids. See Wishlist architecture for the score formula and trust-promotion path.

GET /api/admin/hf/trending

R112 — most recent HF trending GGUF snapshot from the background HfWatcher (refreshed hourly). Response shape mirrors HfTrendingSnapshot with entries: [{ repo_id, downloads, likes, task_tags, created_at_secs }] and fetched_at: <unix-secs>. Empty entries means the watcher hasn't completed its first poll yet (30s startup delay) OR the daemon is configured with auto_manage.hf_watcher_enabled = false.

GET /api/admin/quant-recommendations

R133 — per-family quant-choice recommendations. Returns the highest-quality variant that fits the swarm's aggregate VRAM with reasonable replication for each model family. R141 flipped auto_manage.auto_switch_quants to default true, so this surface now drives automatic trust promotion of the recommended variant — the UI uses it as a transparency view rather than a manual action surface.

Authentication

GET /api/admin/api-key

Retrieve the API key. Bearer auth required.

The dashboard is the exception: it has no key yet on page load, so it may fetch one here without a Bearer token, provided both of the following hold.

  1. The request arrives over a network this node trusts — loopback always, a Tailscale-style overlay when the node is on one too, and a private/LAN address only with api.dashboard_trust_lan. See the [api] section of the configuration reference.
  2. It carries the single-use X-Dashboard-Nonce that the daemon substituted into the served HTML for that page load (60s TTL).

A bare curl against this endpoint therefore returns 401 — it has neither. To read the key from a script, use the api_key file in the data directory.

WebSocket

GET /api/admin/ws

WebSocket for live updates. Pushes the following event types:

EventTriggerData
activity_eventAny subsystem eventkind, model_id, message, timestamp, toast_level
stats_updateEvery 2sPeer count, credits, acquisitions, shard registry, swarm_capacity (R110), wishlist (R111), hardware (RAM/GPU/disk, from a cache refreshed at most every 6 s), network_traffic (bytes in and out plus the current rate — null, never zero, when nothing is counting)
peer_listPeer connect/disconnectFull peer snapshot
models_changedShard download/load/prune(none — signals dashboard to refresh)
update_availableNew version detectedVersion info, changelog