Browse the docs

Docs / Operations

Connect llama-swap with Tailcat

Privately expose inference over Tailcat and route peers through Tailcat connection tokens.

Tailcat provides end-to-end encrypted connections without a Tailscale account or control plane. A Tailcat connection token is case-sensitive and acts like a capability: anyone who has it can attempt to reach the server. Store and share it as carefully as an API credential.

Create stable keys

Install the tailcat CLI, then create a persistent server identity with a fixed relay region. Fixing the region keeps its connection token stable across restarts:

tailcat genkey --key=/path/to/server.private.json --fixed-region

To retrieve the public key for an existing client identity, use the root --key option before printpub:

tailcat --key=/path/to/client.private.json printpub

This prints the full nodekey:... public key. Put that exact value in the server's tailcat.allow list.

Use tailcat genkey --client --key=/path/to/client.private.json only when intentionally creating or rotating a client private key. After a rotation, update tailcat.allow and each affected peer's tailcatKey value before using the new identity.

tailcat:
  allow:
    - nodekey:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
  models:
    - chat
    - coding-model # a selectors entry
    - gpu-box/embeddings
  admin: false
  debug: false

Start llama-swap with both the configuration and the persistent server key:

llama-swap -config /path/to/config.yaml \
  -listen-tailcat /path/to/server.private.json

-listen-tailcat requires a valid Tailcat server PrivateKey JSON file and a non-empty tailcat.models list. Use models: ["*"] explicitly to expose all callable local and peer IDs.

tailcat.models accepts any public model ID: a model's own ID, one of its aliases, a selectors entry, a profile pin, or a peer's peerID/model name. Listing a selector exposes it the same way as a regular model ID, and requests still resolve through the selector's normal routing logic.

allow denies by default. llama-swap checks it on every HTTP request, using the node key that Tailcat authenticated for the connection. A client whose key is not listed can still complete the Tailcat handshake (it needs the connection token for that), but every request it sends gets 403 Forbidden. An empty or missing allow denies every client, and llama-swap logs a warning at startup and on reload when that happens.

To let anyone holding the connection token in, including ephemeral clients, use the wildcard:

tailcat:
  allow: ["*"]
  models: [chat]

An explicit list of keys is strongly recommended for a persistent address. If a client you expect to work gets 403, check that its printpub output is in allow exactly, and that it is using its saved key rather than an ephemeral one.

With admin: false (the default), Tailcat serves only /health, filtered model listings, and allowlisted model-dispatched inference routes. Set admin: true only when remote callers also need the UI, /api, logs, metrics, operations, or upstream passthrough. The model allowlist still applies in admin mode.

Set debug: true to include Tailcat transport diagnostics in the proxy log. It defaults to false to keep routine proxy logs concise.

Configuration reloads (-watch-config or SIGHUP) apply changes to allow, models, admin, and debug without restarting the listener, so connected clients stay connected and the token does not change. A key removed from allow loses access on its next request, even over an open connection. Only the listener key (-listen-tailcat) requires restarting llama-swap.

Find the server token

When the listener starts, llama-swap writes the connection token to its console log. Copy the value after the colon; it is case-sensitive:

[INFO] Tailcat listening on virtual TCP port 80: tcREPLACE_WITH_CONNECTION_TOKEN

You can also open the Tailcat page in the llama-swap UI and copy the token shown under Tailcat connection token. The page is available only while the listener is running.

If llama-swap configures apiKeys, Tailcat callers must also send the normal header, for example Authorization: Bearer <key> or x-api-key: <key>. Tailcat node authorization and HTTP API-key authentication are independent.

Connect a client

Use the token from the console log or Tailcat page. An ephemeral client works only when the server's allow is "*":

tailcat socks <token> curl http://server.tailcat/v1/models

Otherwise use a stable client key whose public key is in the server's allow list:

tailcat --key=/path/to/client.private.json socks <token> curl http://server.tailcat/v1/models

Add API-key headers to the curl command when required:

tailcat --key=/path/to/client.private.json socks <token> \
  curl -H 'Authorization: Bearer <api-key>' http://server.tailcat/v1/models

Tailcat uses virtual TCP port 80 for llama-swap; do not add another port to the connection URL or the server.tailcat hostname.

An unloaded remote model may take longer than a minute to send response headers. Tailcat peers wait up to 300 seconds by default; other peers wait 60. If a cold start takes longer, set peers.<name>.timeouts.responseHeader in seconds. A 502 with timeout awaiting response headers means this wait expired while the remote server was still loading or preparing the response.

peers:
  friend:
    proxy: tailcat://tcREPLACE_WITH_CONNECTION_TOKEN
    models: [large-model]
    timeouts:
      responseHeader: 600

List models with curl

To list the models exposed by a llama-swap server's Tailcat listener, run curl through Tailcat's SOCKS wrapper. Replace the token with the exact value shown on the Tailcat page; it is case-sensitive. This ephemeral example only works when the server's allow is "*".

tailcat socks tcREPLACE_WITH_CONNECTION_TOKEN \
  curl --fail --silent http://server.tailcat/v1/models

With a saved client key listed in the server's allow, and HTTP API keys, include the normal llama-swap authentication header:

tailcat --key=/path/to/client.private.json socks tcREPLACE_WITH_CONNECTION_TOKEN \
  curl --fail --silent \
    -H 'Authorization: Bearer <api-key>' \
    http://server.tailcat/v1/models

Configure a Tailcat peer

Use the destination token verbatim after tailcat://. Tokens are case-sensitive; do not lowercase them.

peers:
  gpu-box:
    proxy: tailcat://tcREPLACE_WITH_CONNECTION_TOKEN
    tailcatKey: /path/to/client.private.json
    models: [chat, embeddings]

Omit tailcatKey (or set it to ephemeral) to create one client identity for that peer for the lifetime of the llama-swap process. That only works when the destination's allow is "*"; otherwise use a saved client key and add its printpub output to the destination's allow list.

Avoid cyclic peer graphs. If host A routes a model to B and B routes that same request back to A, requests loop until they fail and may repeatedly start or hold models.