Files
routstrd/README.md
redshift c7735d7ad3 feat(opencode): derive reasoning variants from daemon model metadata
OpenCode 2.0.28 does not honour capabilities.reasoning for a custom
provider: openai-compatible collapses to low/medium/high and ignores the
effort allowlist, while the responses and anthropic-compatible packages
produce no variants at all. Derive explicit per-model `variants` from the
daemon's reasoning.supported_efforts instead, mapping each effort to the
setting key its package understands (reasoningEffort for the OpenAI-shaped
packages, effort for anthropic-compatible).

Derived variants replace the previous list on refresh so a level the model
no longer accepts cannot linger; when the daemon publishes no allowlist the
user's curated variants are preserved. The anthropic /messages package
skips none/minimal, which routstr-core rejects there.
2026-10-19 08:11:56 +00:00

506 lines
16 KiB
Markdown

# routstrd
Routstr daemon - A CLI tool for managing routstr processes, with a built-in Cashu wallet.
## Overview
routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It carries an in-process Cashu wallet (coco) for payments and uses the Routstr SDK to handle provider routing and model discovery.
## Routstr for Teams
For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd-auth).
## Features
- **Daemon Mode**: Run routstrd as a background HTTP server
- **Wallet Integration**: In-process Cashu wallet, with Lightning, NWC, and NPC support
- **Provider Routing**: Automatically discovers and routes requests to available providers
- **Config Management**: Stores configuration in `~/.routstrd/`
## Requirements
The standalone release does not require Bun, Node.js, or npm. Installing from
npm or running from source requires the [Bun](https://bun.sh) runtime.
## Installation
### Step 1: Install
**Standalone binary (recommended):**
Installs the standalone executable for Linux or macOS (x64 or arm64) into
`$HOME/.local/bin`. No Bun, Node.js, or npm required.
```sh
curl -fsSL https://github.com/Routstr/routstrd/releases/latest/download/install.sh | sh
```
Pin a version, change the install directory, or print the resolved asset without
installing anything:
```sh
curl -fsSL https://github.com/Routstr/routstrd/releases/latest/download/install.sh \
| sh -s -- --version 0.4.9 --dir /usr/local/bin
```
The installer downloads the release archive, verifies it against the release
`SHA256SUMS`, and only replaces an existing `routstrd` once the checksum matches
and the extracted binary reports the expected version.
<details>
<summary>Manual install</summary>
Download the archive for your operating system and architecture from the
[latest GitHub Release](https://github.com/Routstr/routstrd/releases/latest).
Release archives are available for Linux and macOS on x64 and arm64.
```sh
grep "routstrd-v0.4.9-linux-x64.tar.gz" SHA256SUMS | shasum -a 256 -c -
tar -xzf routstrd-v0.4.9-linux-x64.tar.gz
mkdir -p "$HOME/.local/bin"
install -m 755 routstrd "$HOME/.local/bin/routstrd"
```
Substitute the version, platform, and architecture for the archive you
downloaded, and ensure `$HOME/.local/bin` is on `PATH`.
</details>
Installing the standalone binary is preferred over the npm package: the npm
package runs through the Bun runtime, while the standalone executable has no
runtime dependency.
**Global with bun:**
```sh
bun i -g routstrd
```
**OR - From source:**
```sh
git clone https://github.com/routstr/routstrd.git
cd routstrd
bun install
bun link
```
### Step 2: Setup & Fund
```sh
routstrd onboard
routstrd receive <cashu> # receive a Cashu token
routstrd receive 2100 # to top up 2100 sats with lightning
```
### Step 3: Integrate with Claude Code
```sh
routstrd clients add --claude-code # or --pi-agent / --opencode
```
## Use Routstrd Skill
> **Tip:** You can also install the [routstrd skill](https://github.com/Routstr/routstrd/blob/main/SKILL.md) so the agent can manage routstrd for you.
## More Commands
[`SKILL.md`](SKILL.md) is the complete per-command reference — every command,
subcommand, and flag. The sections below cover the common ones.
### Start Daemon
Start the background daemon:
```sh
routstrd start
```
With custom port:
```sh
routstrd start --port 9000
```
The daemon binds to `127.0.0.1` by default. To expose it on another interface:
```sh
routstrd start --host 0.0.0.0
```
Only expose the daemon behind appropriate network controls.
Pin all requests to one provider (no cross-provider failover):
```sh
routstrd start --provider https://your-provider.com
```
See [Provider pinning](#provider-pinning) for the request-level header/query.
### CLI Commands
Check daemon status:
```sh
routstrd status
```
Get wallet balance:
```sh
routstrd balance
```
Test connection:
```sh
routstrd ping
```
Refresh models and client integrations on demand:
```sh
routstrd clients --manual-refresh # same as `routstrd refresh`
```
Turn the daemon's scheduled refresh on or off (no restart needed):
```sh
routstrd clients --disable-automatic-refresh
routstrd clients --enable-automatic-refresh
```
Stop the daemon:
```sh
routstrd stop
```
See which providers/models the router is currently skipping (cooldowns):
```sh
routstrd cooldowns
routstrd cooldowns --json
```
Clear all active cooldowns and failure strikes so every provider is retried now:
```sh
routstrd cooldowns --reset
```
### NPC (Lightning Address)
The in-process wallet registers the NPC (npubx.cash) plugin, which gives the
daemon a persistent Lightning address backed by the wallet's Cashu mints.
Payments to the address are imported into the wallet automatically (websocket
push, plus manual sync on demand).
```sh
# Show your NPC Lightning address (username@npubx.cash, or npub fallback)
routstrd wallet npc address
# Claim a username (quote first, then confirm to pay the claim fee from the wallet)
routstrd wallet npc username myname
routstrd wallet npc username myname --confirm
# Manually sync paid NPC quotes into the wallet
routstrd wallet npc sync
```
Equivalent daemon endpoints: `GET /wallet/npc/address`,
`POST /wallet/npc/username`, `POST /wallet/npc/sync`.
### Daemon API
The daemon exposes an HTTP server (default port 8008) with the following endpoints:
#### Health Check
```
GET /health
```
#### Version
```
GET /version
```
Returns the running daemon version plus the cached result of its background
update check (`{ version, current, latest, hasUpdate, checkedAt }`). The daemon
refreshes that cache every 6 hours; the TUI banner and the CLI's
"update available" notice both read it. The TUI falls back to direct checks
every 210 minutes only when the daemon does not support this endpoint.
#### Cooldowns
```
GET /cooldowns
POST /cooldowns/reset
```
`GET /cooldowns` lists providers and models the router is currently skipping.
Each entry reports its scope (`provider` blocks every model on that provider,
`model` blocks one), when the cooldown started, and when it lifts. Expired
entries are filtered out, and the cooldown window comes from the SDK
(`cooldownDurationMs`).
`POST /cooldowns/reset` clears every active cooldown (and the failure strikes
that turn the next failure into an instant cooldown) so the router retries all
providers immediately. It returns the number of entries cleared and the
affected providers.
#### Automatic Refresh Settings
```
POST /settings/auto-refresh
```
Request body:
```json
{ "enabled": false }
```
Enables or disables the scheduled refresh job. Persisted to the daemon's
`config.json` as `autoRefresh.enabled` and picked up on the next tick, so no
daemon restart is required.
#### Route Request
```
POST /v1/chat/completions
```
Any unmatched `POST` path is proxied to the selected provider with the incoming
path preserved, so `POST /v1/messages` (Anthropic Messages API) and
`POST /v1/responses` (OpenAI Responses API) work in their own formats too.
#### Provider pinning
Pin a request to a single provider with either the `x-routstr-provider` header
or the `?provider=` query parameter (the query takes precedence):
```sh
curl -H 'x-routstr-provider: https://your-provider.com' ... \
http://127.0.0.1:8008/v1/chat/completions
```
A pin is strict: the request is sent only to that provider. On an upstream
error — including a 400/422 the node itself rejects — the request is **not**
failed over to another node; the error is returned to the client. Retries
against the same provider (top-up, mint fallback) still happen. The config
`provider` and `routstrd start --provider` set the same strict pin as a
default for every request.
Request body:
```json
{
"model": "model-id",
"messages": [...],
"stream": false
}
```
Response:
```json
{
"choices": [...],
"usage": {...}
}
```
## Wallet storage
The in-process Cashu wallet stores its mnemonic and proof database in
`~/.routstrd/wallet/`. On first startup, an existing wallet in `~/.cocod/` is
migrated automatically after routstrd verifies that the legacy cocod daemon is
not running. Back up your mnemonic before upgrading.
Set `ROUTSTRD_WALLET_DIR` to override the canonical wallet directory. The
`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` variables are retained only for
locating and excluding a legacy external cocod process.
If both `~/.routstrd/wallet` and `~/.cocod` contain different wallets, startup
refuses to migrate rather than picking a mnemonic for you. Run
`routstrd wallet doctor` to compare the two wallets (mnemonic fingerprints,
timestamps, and balances) and see which one to keep.
## Configuration
Configuration is stored in `~/.routstrd/config.json`:
```json
{
"port": 8008,
"host": "127.0.0.1",
"provider": null,
"autoModelPath": false,
"autoRefresh": { "enabled": true }
}
```
`autoRefresh.enabled` (default `true`) controls the daemon's scheduled refresh
job, which re-fetches Nostr events, routstr21 models, and client integrations
every 21 minutes. Set it to `false` (or run
`routstrd clients --disable-automatic-refresh`) to turn the schedule off and
refresh manually with `routstrd clients --manual-refresh`. `autoRefresh.intervalMs`
overrides the 21-minute interval.
`autoModelPath` defaults to `false`. Set it to `true` to let the SDK automatically
choose and pin an advertised model path for `deepseek-v4.1-flash` requests.
Explicit `x-routstr-model-path` request headers work independently of this
setting and take precedence. Restart the daemon after changing `autoModelPath`.
`provider` pins every request to one node (same strict behavior as the
`x-routstr-provider` header / `?provider=` query). Leave it `null` to let the
router pick the cheapest eligible provider and fail over between nodes.
The OpenCode integration writes the native v2 config shape (`providers` with a
per-provider `package`) and no longer writes the v1 `provider.routstr` block,
which OpenCode v1 does not read. OpenCode picks the transport with that runtime
package, so the integration pins it per model: `claude*` models go to
`{root}/messages` and `gpt-*` models to `{root}/responses` (the same families
the pi integration pins), and every other model uses `{root}/chat/completions`,
the provider default. A `package` you curate by hand on a model outside those
two families survives a refresh, as do the other keys you add to a model entry
(`limit`, `capabilities`, ...).
Reasoning-effort `variants` are derived per model from the daemon's published
`reasoning.supported_efforts` allowlist (the same metadata the pi integration
turns into `thinkingLevelMap`), so `opencode run --model
routstr/glm-5.3#max`, `/models`, and ctrl+t only offer levels the model
actually accepts. Explicit variants are used because `capabilities.reasoning`
is not honoured for a custom provider: OpenCode 2.0.28 collapses it to
`low`/`medium`/`high` for `openai-compatible`, ignores the effort allowlist,
and produces no variants at all for the responses and anthropic-compatible
packages. `none` is omitted on models whose reasoning is mandatory, and the
`messages` package only offers the `low`/`medium`/`high`/`xhigh`/`max` subset
that the Anthropic route accepts. When the daemon publishes no allowlist the
`variants` you curated survive a refresh; when it does, the derived set
replaces them wholesale.
### Environment Variables
- `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`)
- `ROUTSTRD_SOCKET` - Socket path (default: `~/.routstrd/routstrd.sock`)
- `ROUTSTRD_PID` - PID file path (default: `~/.routstrd/routstrd.pid`)
- `ROUTSTRD_WALLET_DIR` - Wallet data directory (default: `~/.routstrd/wallet`)
- `ROUTSTRD_LOG_LEVEL` - Minimum level written to the daily log: `debug`, `info` (default), `warn` or `error`. See [docs/logging.md](docs/logging.md)
- `ROUTSTRD_COCO_LOG_LEVEL` - Same, for the Cashu wallet-engine log (default: `debug`)
- `COCOD_DIR` - Legacy external cocod directory, used only for migration and exclusion (default: `~/.cocod`)
## Development
Install dependencies:
```sh
bun install
```
Run the CLI from source:
```sh
bun src/index.ts <command>
```
Run the daemon:
```sh
bun run start
```
Run the tests:
```sh
bun test
```
Build a standalone executable for the current platform:
```sh
bun run build:binary
./dist/routstrd --version
```
Standalone installations update directly from GitHub Releases with
`routstrd update`. npm installations continue to update through Bun. PM2 is an
optional external dependency used only by `routstrd service`; normal daemon
operation does not require it.
A background check in the daemon caches whether a newer routstrd exists in
`~/.routstrd/update-state.json` (refreshed every 6 hours) and serves it from
`GET /version`. The usage TUI shows an update banner from that cache, and after
most commands the CLI prints a one-line notice pointing at `routstrd update`.
Automatic checks are enabled by default. To disable background checks, startup
prompts, CLI notices and the TUI banner, add this Boolean to
`~/.routstrd/config.json` (or the config file under `ROUTSTRD_DIR`):
```json
{ "updateCheck": false }
```
Merge this field into your existing config; do not replace the other settings.
The explicit `routstrd update` command remains available. Failed checks preserve
the last successful cache; notices expire after 24 hours and are only shown for
the version currently running.
When an update finds a process on the configured daemon port, it only stops that
process if the wallet PID file confirms a live daemon owned by the same routstrd
configuration. Otherwise the update remains installed, but automatic restart is
refused so an unrelated daemon is not interrupted.
Existing PM2 registrations created by routstrd 0.4.x continue to work through a
compatibility daemon entrypoint. Recreate the registration to use the unified
CLI entrypoint and remove its legacy path dependency:
```sh
routstrd service uninstall
routstrd service install
pm2 save
```
Typecheck:
```sh
bun run lint
```
### Manual chat-completions smoke test
With a funded daemon running, create or reuse a client API key and pass one or
more current model IDs to the smoke script:
```sh
routstrd clients add --name smoke-test
ROUTSTRD_API_KEY=<api-key> bun run smoke <model> [model ...]
```
Set `ROUTSTRD_BASE_URL` to test a daemon at a different address. The script
makes live provider requests that may spend wallet funds, so it is intentionally
not part of `bun test`.
### Publishing a standalone release
1. Set a new `package.json` version and commit it. The release tag must be the
same version prefixed with `v`, and the tag must not already exist.
2. Push the tag. The release workflow runs lint and tests, builds Linux and
macOS executables for x64 and arm64, smoke-tests them, verifies the archives
through `install.sh` itself, and publishes the archives with `SHA256SUMS` and
`install.sh`. It also publishes `routstrd` to npm via trusted publishing
(OIDC, no token), skipping gracefully if that version already exists.
3. Verify all four archives and `install.sh` appear in the GitHub Release and
validate each checksum before announcing it.
4. In disposable environments for each platform, test `--version`, `--help`,
foreground startup failure, and background `start`, `status`, and `stop`
without Bun on `PATH`.
5. Test `routstrd service install` and restart behavior with PM2 in a disposable
environment. Never run release/update lifecycle tests against a production
daemon. When isolation is needed, use both a separate `ROUTSTRD_DIR` and a
non-production port in that configuration.
See [docs/CI.md](docs/CI.md) for the full CI/CD details: job layout, npm trusted
publishing (OIDC) setup, the canonical `repository.url` requirement, and how to
order a release when it needs a newer `@routstr/sdk`.
## Project Structure
```
routstrd/
├── src/
│ ├── index.ts # CLI entry point with shebang
│ ├── cli.ts # Commander CLI commands
│ ├── daemon.ts # Compatibility daemon entrypoint (legacy PM2 registrations)
│ ├── start-daemon.ts # Daemon process launcher
│ ├── daemon/ # HTTP server, wallet, provider routing
│ ├── integrations/ # Client integrations (Claude Code, pi, OpenCode, ...)
│ ├── tui/ # Interactive usage monitor (`routstrd monitor`)
│ └── utils/ # Config, paths, daemon client, update checker
├── tests/ # Integration tests (unit tests sit beside their source)
├── scripts/smoke/ # Manual end-to-end smoke test
├── docs/CI.md # CI and release automation
├── docs/plans/ # Design/migration plans not yet executed
├── package.json
└── tsconfig.json
```
## License
MIT