mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
Merge pull request #100 from hzrd149/chore/doc-cleanup
Naive cleanup of repo documentation
This commit is contained in:
@@ -1,253 +0,0 @@
|
||||
# Migrate wallet storage from `~/.cocod` to `~/.routstrd/wallet`
|
||||
|
||||
Task: [`routstrd-migrate-cocod-to-routstrd-files`](https://github.com/nodestrich/routstrd)
|
||||
Event ID: `2f420644005154c5b106017c4a753854bb2660ba834cde7e6e267532cb5448e9`
|
||||
Priority: 997 · Status: open
|
||||
|
||||
---
|
||||
|
||||
## Baseline
|
||||
|
||||
This plan is based on `coco-integration` at `86ac688` (after the default-mint, NPC/npubx.cash, and Windows-support changes).
|
||||
|
||||
The task branch is currently based on `04f8c22`; rebase it onto the latest `coco-integration` before implementing this plan.
|
||||
|
||||
## Goal
|
||||
|
||||
Move the in-process Cashu wallet's persistent data from:
|
||||
|
||||
```text
|
||||
~/.cocod/config.json
|
||||
~/.cocod/coco.db
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```text
|
||||
~/.routstrd/wallet/config.json
|
||||
~/.routstrd/wallet/coco.db
|
||||
```
|
||||
|
||||
The migration must preserve the mnemonic, proofs, default mint, and NPC identity. Existing users must not accidentally get a new wallet, and `cocod` and routstrd must never open the same database concurrently.
|
||||
|
||||
Fresh installs must use `~/.routstrd/wallet` immediately. Existing installs must be migrated automatically and safely; merely continuing to run from `~/.cocod` is not sufficient for this task.
|
||||
|
||||
---
|
||||
|
||||
## Important corrections to the previous plan
|
||||
|
||||
### 1. Do not rename or migrate `cocod.sock`
|
||||
|
||||
The in-process `coco-core` wallet does not expose a Unix socket. `cocod.sock` belongs to the legacy external `cocod` daemon and is used only to determine whether that daemon is still alive.
|
||||
|
||||
There should therefore be no `wallet.sock`. Do not copy a socket inode into the new directory.
|
||||
|
||||
### 2. Keep legacy-process paths separate from wallet-data paths
|
||||
|
||||
On the current base, `src/daemon/wallet/coco-client.ts` derives all of these from one `CONFIG_DIR`:
|
||||
|
||||
- wallet config and database;
|
||||
- legacy cocod socket;
|
||||
- legacy cocod PID/process lock.
|
||||
|
||||
That coupling must be removed. After migration:
|
||||
|
||||
- wallet data comes from `~/.routstrd/wallet`;
|
||||
- probes and shutdown logic for an external legacy cocod continue to use `~/.cocod/cocod.sock` and `~/.cocod/cocod.pid`;
|
||||
- routstrd uses `~/.routstrd/wallet/wallet.pid` as the in-process wallet lock.
|
||||
|
||||
While routstrd owns the migrated wallet, it should also retain the existing legacy-cocod exclusion mechanism (claiming the legacy `cocod.pid`) so an old cocod cannot be started against a stale or partially migrated legacy wallet. Acquisition and release of both locks must be rollback-safe.
|
||||
|
||||
### 3. Do not rename `cocodPath`
|
||||
|
||||
`RoutstrdConfig.cocodPath` is an executable path for the external compatibility client, not a wallet storage path. Renaming it to `walletPath` would change its meaning and would not help this migration.
|
||||
|
||||
The current daemon creates `createCocoClient()` directly and injects it into `createWalletAdapter()`, so `cocodPath` is effectively bypassed on the normal in-process path. Cleanup or removal of that compatibility setting is a separate task.
|
||||
|
||||
### 4. Do not implement fallback as the steady state
|
||||
|
||||
A resolver that permanently falls back to `~/.cocod` does not move the data. Legacy detection is needed to initiate migration, but successful startup should resolve to the canonical directory afterward.
|
||||
|
||||
### 5. Account for the updated base
|
||||
|
||||
The latest `coco-integration` adds:
|
||||
|
||||
- `defaultMintUrl` persistence in the wallet's `config.json`;
|
||||
- the NPC plugin, whose Nostr identity is derived from the same mnemonic;
|
||||
- `src/daemon/wallet/coco-client.npc.test.ts`;
|
||||
- Windows support and `USERPROFILE` fallback.
|
||||
|
||||
Migration must preserve the complete config JSON rather than reconstructing selected fields. Path construction must use `path.join` rather than hard-coded `/` separators.
|
||||
|
||||
---
|
||||
|
||||
## Current runtime path references on the latest base
|
||||
|
||||
### Wallet data and legacy process coordination
|
||||
|
||||
| File | Current responsibility | Required change |
|
||||
|---|---|---|
|
||||
| `src/daemon/wallet/coco-client.ts` | Uses one `.cocod` directory for config, DB, socket, and PID; owns the in-process wallet and legacy-cocod guard | Split canonical data paths from legacy process paths; migrate before opening the DB; use `wallet.pid` for the in-process lock |
|
||||
| `src/daemon/wallet/cocod-client.ts` | External cocod compatibility client; defaults to `.cocod/cocod.sock` | Keep legacy defaults and clarify that they are external-cocod paths |
|
||||
| `src/cli.ts` | Initializes `.cocod`; two restart paths wait on `.cocod/cocod.pid` | Initialize/migrate the canonical wallet and wait on `wallet.pid` |
|
||||
| `src/utils/config.ts` | Defines `~/.routstrd`, but no wallet subdirectory | Export canonical and legacy wallet/process path helpers or constants |
|
||||
| `src/daemon/index.ts` | Calls `createCocoClient()` before building the adapter | Ensure migration occurs before the client opens the DB |
|
||||
|
||||
### Tests and documentation
|
||||
|
||||
| File | Required update |
|
||||
|---|---|
|
||||
| `src/cli.test.ts` | Change fresh-wallet expectations and add migration coverage |
|
||||
| `src/daemon/wallet/coco-client.test.ts` | Update data/lock paths and add migration/dual-lock tests |
|
||||
| `src/daemon/wallet/coco-client.npc.test.ts` | Use the canonical wallet layout in fixtures; verify migrated mnemonic/config still drives NPC identity |
|
||||
| `README.md` | Document the new location and migration behavior; retain legacy cocod terminology only where discussing compatibility |
|
||||
| `SKILL.md` | Update wallet storage and environment-variable documentation |
|
||||
|
||||
The fixture `src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz` keeps its name because it records the fixture's provenance.
|
||||
|
||||
---
|
||||
|
||||
## Target path model
|
||||
|
||||
Define the path model in one dependency-light module (for example `src/daemon/wallet/paths.ts`, or in `src/utils/config.ts` if that does not introduce a cycle):
|
||||
|
||||
```text
|
||||
ROUTSTRD config root process.env.ROUTSTRD_DIR || ~/.routstrd
|
||||
canonical wallet directory process.env.ROUTSTRD_WALLET_DIR || <config root>/wallet
|
||||
canonical wallet config <wallet directory>/config.json
|
||||
canonical wallet database <wallet directory>/coco.db
|
||||
canonical wallet lock process.env.ROUTSTRD_WALLET_PID || <wallet directory>/wallet.pid
|
||||
|
||||
legacy cocod directory process.env.COCOD_DIR || ~/.cocod
|
||||
legacy cocod socket process.env.COCOD_SOCKET || <legacy directory>/cocod.sock
|
||||
legacy cocod PID process.env.COCOD_PID || <legacy directory>/cocod.pid
|
||||
```
|
||||
|
||||
Use `HOME` with `USERPROFILE` fallback, matching the updated Windows-support base. Use functions or injectable path objects where tests need to change environment variables after module import.
|
||||
|
||||
`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` remain compatibility controls for locating an old external cocod. They must not redirect the new in-process wallet away from `~/.routstrd/wallet`. `ROUTSTRD_WALLET_DIR` is the new explicit data-directory override.
|
||||
|
||||
---
|
||||
|
||||
## Implementation plan
|
||||
|
||||
### Phase 1 — Centralize and separate paths
|
||||
|
||||
1. Add the canonical wallet and legacy cocod path definitions above.
|
||||
2. Remove the local `.cocod` path construction from `coco-client.ts` and `cli.ts`.
|
||||
3. Change `CreateCocoClientOptions` so tests/tooling can independently override:
|
||||
- wallet data directory;
|
||||
- wallet lock path;
|
||||
- legacy cocod socket and PID paths.
|
||||
4. Keep `cocod-client.ts` pointed at the legacy socket. Do not make it import a resolver that prefers the new wallet directory.
|
||||
|
||||
### Phase 2 — Add a safe automatic migration primitive
|
||||
|
||||
Add an idempotent `migrateLegacyWallet()` helper with injectable paths/filesystem operations for tests.
|
||||
|
||||
Preconditions and behavior:
|
||||
|
||||
1. If the canonical wallet already contains both `config.json` and `coco.db`, return `already-current` and do not touch legacy data.
|
||||
2. If neither canonical nor legacy wallet data exists, return `fresh`; initialization will create the canonical directory.
|
||||
3. If only the legacy wallet exists:
|
||||
- first verify that legacy cocod is not running using the existing PID and socket guard;
|
||||
- create a private staging directory under `~/.routstrd` with mode `0700`;
|
||||
- copy the complete `config.json` and `coco.db` into staging without parsing or rewriting them;
|
||||
- apply `0600` to both files;
|
||||
- validate that the staged files exist and have the expected byte sizes;
|
||||
- atomically rename the staged directory to `wallet`;
|
||||
- only after the canonical directory is committed, remove the legacy `config.json` and `coco.db`;
|
||||
- never copy `cocod.sock` or `cocod.pid`.
|
||||
4. If canonical storage is partial, legacy storage is partial, both contain wallet data, or files conflict, stop with an actionable error. Never merge databases and never generate a new mnemonic over an ambiguous state.
|
||||
5. Clean up an uncommitted staging directory after failure. A committed canonical wallet remains authoritative if cleanup of the old files fails; report that cleanup warning clearly.
|
||||
|
||||
The migration should preserve unknown config fields, including `defaultMintUrl`, and preserve the database byte-for-byte. This also preserves the NPC identity because that identity is derived from the mnemonic.
|
||||
|
||||
A staging copy plus atomic directory rename is preferred over two independent file renames: a crash must not expose a half-created canonical wallet. Copying also permits a clear rollback before commit and supports a legacy directory located on another filesystem through `COCOD_DIR`.
|
||||
|
||||
### Phase 3 — Run migration from every wallet-opening path
|
||||
|
||||
1. **CLI onboarding/init:** run migration before `initializeWallet()`. Only initialize a new mnemonic when migration reports `fresh`.
|
||||
2. **Daemon direct startup:** run migration before `createCocoClient()` opens `coco.db`. This covers users who invoke the daemon without rerunning onboarding.
|
||||
3. **Start/restart/update/service paths:** retain `stopLegacyCocod()` before migration/startup where those paths already stop legacy cocod. Direct daemon startup should refuse with the existing actionable error rather than silently running two wallet engines.
|
||||
4. Print a concise success message showing old and new directories, but never print config contents or the mnemonic during migration.
|
||||
|
||||
A separate `routstrd wallet migrate` command is optional as a manual recovery/preview entry point, but it must call the same migration primitive. It is not a substitute for automatic migration.
|
||||
|
||||
### Phase 4 — Separate process locking
|
||||
|
||||
Refactor the current `claimLegacyCocodPidFile()` behavior into explicit responsibilities:
|
||||
|
||||
1. Claim `wallet.pid` for the lifetime of the in-process wallet to prevent two routstrd wallet instances from opening `coco.db`.
|
||||
2. Continue claiming the legacy `cocod.pid` while routstrd is active, after verifying no real cocod owns it, to prevent an old cocod from starting.
|
||||
3. If either claim fails, release any claim already acquired before returning the error.
|
||||
4. On `CocodClient.dispose()`, startup failure, and daemon shutdown, release only PID files still owned by the current process.
|
||||
5. Keep `stopLegacyCocod()` and `assertLegacyCocodNotRunning()` operating only on legacy cocod paths.
|
||||
6. Update both PID-release waits in `src/cli.ts` (`restartDaemonsAfterUpdate` and `restart`) to wait for the canonical `wallet.pid` rather than `.cocod/cocod.pid`.
|
||||
|
||||
There is no canonical wallet socket.
|
||||
|
||||
### Phase 5 — Update initialization and client defaults
|
||||
|
||||
1. Make `initializeWallet()` default to the canonical wallet directory.
|
||||
2. Preserve directory mode `0700` and config mode `0600` on both migrated and fresh wallets.
|
||||
3. In `createCocoClient()`, derive `config.json` and `coco.db` from the canonical wallet directory, but take legacy guard paths separately.
|
||||
4. Update the wallet-access comment near `unlock()` to reference `~/.routstrd/wallet/config.json`.
|
||||
5. Leave `CocodClient`, `resolveCocodExecutable()`, and `cocodPath` compatibility behavior unchanged.
|
||||
|
||||
### Phase 6 — Tests
|
||||
|
||||
Add or update tests for:
|
||||
|
||||
- fresh initialization creates `~/.routstrd/wallet`, not `.cocod`;
|
||||
- a legacy config and database migrate byte-for-byte;
|
||||
- `defaultMintUrl` and unknown config fields survive migration;
|
||||
- the NPC-derived identity is unchanged after migration;
|
||||
- migration never copies socket or PID files;
|
||||
- migration refuses while a real legacy cocod is running;
|
||||
- stale legacy socket/PID handling remains safe;
|
||||
- an existing complete canonical wallet wins without modifying it;
|
||||
- partial canonical, partial legacy, and conflicting dual-wallet states fail without generating a mnemonic;
|
||||
- staging cleanup and retry after an interrupted migration;
|
||||
- custom `ROUTSTRD_DIR`, `ROUTSTRD_WALLET_DIR`, and legacy `COCOD_DIR` paths;
|
||||
- Windows-compatible path construction;
|
||||
- acquiring the second process lock rolls back the first on failure;
|
||||
- `dispose()` releases both owned locks and does not unlink another process's lock;
|
||||
- both CLI restart paths wait for `wallet.pid`.
|
||||
|
||||
Keep `cocod-0.0.24-wallet.db.gz` unchanged and continue using it to prove the migrated database opens successfully with the current in-process wallet.
|
||||
|
||||
### Phase 7 — Documentation
|
||||
|
||||
Update `README.md` and `SKILL.md` to state:
|
||||
|
||||
- wallet data is stored in `~/.routstrd/wallet`;
|
||||
- existing `~/.cocod` data is migrated automatically on first startup;
|
||||
- users must back up the mnemonic before migration;
|
||||
- `ROUTSTRD_WALLET_DIR` overrides the canonical wallet directory;
|
||||
- `COCOD_DIR`, `COCOD_SOCKET`, `COCOD_PID`, and `cocodPath` refer only to legacy external-cocod compatibility.
|
||||
|
||||
Do not replace every use of the word `cocod`: references to the external daemon, compatibility client, legacy guard, package, and fixture are still accurate.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A fresh install creates wallet data only under `~/.routstrd/wallet`.
|
||||
- Starting from a valid `.cocod` wallet results in the same config and database under the canonical directory without changing balances, mnemonic-derived NPC identity, or default mint.
|
||||
- No startup path silently creates a new mnemonic when recoverable legacy data exists.
|
||||
- A running legacy cocod blocks migration and database opening.
|
||||
- The new wallet directory contains no copied Unix socket and uses `wallet.pid` only as an in-process lock.
|
||||
- Legacy cocod probing and exclusion continue to use `.cocod/cocod.sock` and `.cocod/cocod.pid`.
|
||||
- The implementation works with `USERPROFILE`/Windows path construction and custom directory overrides.
|
||||
- All wallet, CLI, typecheck, and build tests pass on the rebased `coco-integration` branch.
|
||||
|
||||
---
|
||||
|
||||
## Suggested commit sequence
|
||||
|
||||
1. `refactor(wallet): separate routstrd wallet paths from legacy cocod paths`
|
||||
2. `feat(wallet): atomically migrate legacy cocod wallet data`
|
||||
3. `fix(wallet): use independent routstrd and legacy cocod process locks`
|
||||
4. `test(wallet): cover migration, conflicts, NPC identity, and lock rollback`
|
||||
5. `docs: document routstrd wallet storage and legacy migration`
|
||||
@@ -1,10 +1,10 @@
|
||||
# routstrd
|
||||
|
||||
Routstr daemon - A CLI tool for managing routstr processes, similar to `cocod` (a Cashu wallet daemon).
|
||||
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 integrates with `cocod` for wallet management and uses the Routstr SDK to handle provider routing and model discovery.
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd-
|
||||
## Features
|
||||
|
||||
- **Daemon Mode**: Run routstrd as a background HTTP server
|
||||
- **Wallet Integration**: Works with cocod for Cashu token management
|
||||
- **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/`
|
||||
|
||||
@@ -102,6 +102,9 @@ routstrd clients add --claude-code # or --pi-agent / --opencode
|
||||
> **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:
|
||||
@@ -207,9 +210,13 @@ daemon restart is required.
|
||||
|
||||
#### Route Request
|
||||
```
|
||||
POST /
|
||||
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.
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
@@ -269,6 +276,8 @@ overrides the 21-minute interval.
|
||||
- `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`)
|
||||
- `COCOD_DIR` - Legacy external cocod directory, used only for migration and exclusion (default: `~/.cocod`)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -277,14 +286,19 @@ Install dependencies:
|
||||
bun install
|
||||
```
|
||||
|
||||
Run CLI:
|
||||
Run the CLI from source:
|
||||
```sh
|
||||
bun src/index.ts <command>
|
||||
```
|
||||
|
||||
Run the daemon:
|
||||
```sh
|
||||
bun run start
|
||||
```
|
||||
|
||||
Run daemon:
|
||||
Run the tests:
|
||||
```sh
|
||||
bun run start
|
||||
bun test
|
||||
```
|
||||
|
||||
Build a standalone executable for the current platform:
|
||||
@@ -326,7 +340,7 @@ more current model IDs to the smoke script:
|
||||
|
||||
```sh
|
||||
routstrd clients add --name smoke-test
|
||||
ROUTSTRD_API_KEY=<api-key> scripts/smoke/chat-completions.sh <model> [model ...]
|
||||
ROUTSTRD_API_KEY=<api-key> bun run smoke <model> [model ...]
|
||||
```
|
||||
|
||||
Set `ROUTSTRD_BASE_URL` to test a daemon at a different address. The script
|
||||
@@ -356,12 +370,17 @@ not part of `bun test`.
|
||||
```
|
||||
routstrd/
|
||||
├── src/
|
||||
│ ├── index.ts # Entry point with shebang
|
||||
│ ├── cli.ts # Commander CLI commands
|
||||
│ ├── cli-shared.ts # IPC utilities
|
||||
│ ├── daemon.ts # HTTP server daemon
|
||||
│ └── utils/
|
||||
│ └── config.ts # Path configuration
|
||||
│ ├── 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/plans/ # Design/migration plans not yet executed
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
## SECURITY.md
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
# routstrd CLI Reference
|
||||
|
||||
Routstr daemon — a Bun-based CLI tool that runs a background HTTP server for the Routstr protocol. It integrates with `cocod` for Cashu wallet management and routes LLM requests to available providers.
|
||||
Routstr daemon — a Bun-based CLI tool that runs a background HTTP server for the
|
||||
Routstr protocol. It carries an in-process Cashu wallet (coco) for payments and
|
||||
routes LLM requests to available providers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```sh
|
||||
routstrd onboard # Initialize (creates config, sets up cocod)
|
||||
routstrd onboard # Initialize (creates config, sets up the wallet)
|
||||
routstrd receive 2100 # Top up over Lightning
|
||||
routstrd start # Start the daemon
|
||||
routstrd stop # Stop the daemon
|
||||
```
|
||||
|
||||
After onboarding, the daemon listens at `http://localhost:8008` and exposes an OpenAI-compatible API.
|
||||
After onboarding, the daemon listens at `http://localhost:8008` and exposes an
|
||||
OpenAI-compatible API.
|
||||
|
||||
## Commands
|
||||
|
||||
### `routstrd onboard`
|
||||
|
||||
Initialize routstrd for the first time:
|
||||
- Creates `~/.routstrd/` config directory
|
||||
- Creates `~/.routstrd/config.json` with defaults (port 8008, apikeys mode)
|
||||
- Installs `cocod` globally via bun if not present
|
||||
- Runs `cocod init` to set up the wallet
|
||||
- Starts the daemon and configures integrations
|
||||
- Creates `~/.routstrd/` (mode 0700) and `~/.routstrd/config.json` (mode 0600)
|
||||
with defaults (port 8008, host 127.0.0.1, apikeys mode)
|
||||
- Generates a Nostr identity (`nsec`) for NIP-98 authentication
|
||||
- Migrates a legacy `~/.cocod` wallet to `~/.routstrd/wallet` if one is present,
|
||||
stopping any running external `cocod` first
|
||||
- Initializes the in-process Cashu wallet
|
||||
- Starts the daemon and configures a client integration
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--opencode` | Set up OpenCode integration (non-interactive) |
|
||||
| `--openclaw` | Set up OpenClaw integration (non-interactive) |
|
||||
| `--pi-agent` | Set up Pi Agent integration (non-interactive) |
|
||||
| `--claude-code` | Set up Claude Code integration (non-interactive) |
|
||||
| `--hermes` | Set up Hermes integration (non-interactive) |
|
||||
| `--skip-integration` | Skip integration setup |
|
||||
|
||||
Use at most one integration flag. For several clients, run `routstrd clients add`
|
||||
afterwards. `--skip-integration` cannot be combined with an integration flag.
|
||||
|
||||
### `routstrd start`
|
||||
|
||||
@@ -30,28 +48,50 @@ Start the background daemon process.
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--port <port>` | Port to listen on (default: 8008) |
|
||||
| `--host <host>` | Bind address (default: 127.0.0.1) |
|
||||
| `-p, --provider <provider>` | Default provider to use |
|
||||
|
||||
### `routstrd daemon`
|
||||
|
||||
Run the daemon in the foreground (same options as `start`). Useful for debugging
|
||||
and for process supervisors that expect a non-forking process.
|
||||
|
||||
### `routstrd stop`
|
||||
|
||||
Stop the background daemon.
|
||||
|
||||
### `routstrd restart`
|
||||
|
||||
Restart the daemon (stops if running, then starts).
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--port <port>` | Port to listen on |
|
||||
| `-p, --provider <provider>` | Default provider to use |
|
||||
Restart the daemon (stops if running, then starts). Same options as `start`.
|
||||
|
||||
### `routstrd status`
|
||||
|
||||
Check daemon and wallet status. Returns JSON with current state.
|
||||
|
||||
### `routstrd ping`
|
||||
|
||||
Test connectivity to the daemon.
|
||||
|
||||
### `routstrd balance`
|
||||
|
||||
Get wallet and API key balances. Shows per-mint wallet balances, per-key API balances, and a grand total (all in sats).
|
||||
Get wallet and API key balances. Shows per-mint wallet balances, per-key API
|
||||
balances, and a grand total (all in sats).
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--api-keys` | List all stored API keys (baseUrl + key + balance) |
|
||||
| `--delete-api-keys <baseUrl>` | Delete the API key stored for a provider base URL (refunds its balance first) |
|
||||
| `--mint-url <url>` | Mint to refund the deleted API key balance to (defaults to the active wallet mint) |
|
||||
|
||||
### `routstrd refund`
|
||||
|
||||
Refund pending tokens and API keys to a mint.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-m, --mint-url <mintUrl>` | active wallet mint | Mint URL to refund to |
|
||||
| `-y, --yes` | false | Skip confirmation prompt |
|
||||
| `--xcashu` | false | Refund xcashu tokens only |
|
||||
|
||||
### `routstrd models`
|
||||
|
||||
@@ -60,6 +100,7 @@ List available routstr21 models (discovered via Nostr).
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-r, --refresh` | Force refresh models from Nostr |
|
||||
| `-m, --model <id>` | Show the providers serving a specific model |
|
||||
|
||||
### `routstrd usage`
|
||||
|
||||
@@ -69,7 +110,19 @@ Show recent usage logs and total sats cost.
|
||||
|--------|---------|-------------|
|
||||
| `-n, --limit <number>` | 10 | Number of recent entries (max 1000) |
|
||||
|
||||
Shows timestamp, model, provider, sats cost, token counts, and request ID for each entry.
|
||||
Shows timestamp, model, provider, sats cost, token counts, and request ID for
|
||||
each entry.
|
||||
|
||||
### `routstrd history`
|
||||
|
||||
Show wallet transaction history.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-n, --limit <number>` | 50 | Number of entries to show |
|
||||
| `--offset <number>` | 0 | Number of entries to skip |
|
||||
| `-v, --verbose` | false | Show full details including encoded Cashu tokens |
|
||||
| `--json` | false | Output raw JSON with token objects (no encoding) |
|
||||
|
||||
### `routstrd providers`
|
||||
|
||||
@@ -77,7 +130,12 @@ List and manage providers (subcommand required).
|
||||
|
||||
#### `routstrd providers list`
|
||||
|
||||
List all providers with their enabled/disabled status. Shows index, status, and base URL.
|
||||
List all providers with their enabled/disabled status. Shows index, status, and
|
||||
base URL.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--refresh` | Force re-fetch all Nostr events and refresh models from every enabled provider |
|
||||
|
||||
```
|
||||
Providers (12 total, 2 disabled):
|
||||
@@ -103,6 +161,10 @@ Enable providers by their index numbers.
|
||||
routstrd providers enable 0 2 5
|
||||
```
|
||||
|
||||
#### `routstrd providers reviews`
|
||||
|
||||
Show all known providers with their stored review events and event IDs.
|
||||
|
||||
### `routstrd clients`
|
||||
|
||||
List and manage API clients (subcommand required).
|
||||
@@ -113,7 +175,11 @@ List and manage API clients (subcommand required).
|
||||
| `--disable-automatic-refresh` | Disable the daemon's scheduled refresh job |
|
||||
| `--enable-automatic-refresh` | Re-enable the daemon's scheduled refresh job |
|
||||
|
||||
The daemon refreshes models and client integrations on a schedule (every 21 minutes by default). Use `--manual-refresh` to do it on demand, and `--disable-automatic-refresh` to stop the scheduled job — the setting is stored in the daemon's `config.json` (`autoRefresh.enabled`) and takes effect without a restart.
|
||||
The daemon refreshes models and client integrations on a schedule (every 21
|
||||
minutes by default). Use `--manual-refresh` to do it on demand, and
|
||||
`--disable-automatic-refresh` to stop the scheduled job — the setting is stored
|
||||
in the daemon's `config.json` (`autoRefresh.enabled`) and takes effect without a
|
||||
restart.
|
||||
|
||||
```sh
|
||||
routstrd clients --manual-refresh # refresh models + integrations now
|
||||
@@ -125,7 +191,6 @@ routstrd clients --enable-automatic-refresh # scheduled refresh back on
|
||||
|
||||
List all registered clients with their ID, name, API key, and creation date.
|
||||
|
||||
|
||||
#### `routstrd clients add`
|
||||
|
||||
Add a new client or set up a client integration.
|
||||
@@ -137,10 +202,11 @@ Add a new client or set up a client integration.
|
||||
| `--openclaw` | Set up OpenClaw integration |
|
||||
| `--pi-agent` | Set up Pi Agent integration |
|
||||
| `--claude-code` | Set up Claude Code integration |
|
||||
| `--hermes` | Set up Hermes integration |
|
||||
|
||||
```sh
|
||||
routstrd clients add --opencode --pi-agent --claude-code # multiple integrations
|
||||
routstrd clients add -n "My App" # generic client
|
||||
routstrd clients add -n "My App" # generic client
|
||||
```
|
||||
|
||||
Returns the client ID and API key for use with the OpenAI-compatible API.
|
||||
@@ -151,56 +217,58 @@ Delete a registered client by its ID.
|
||||
|
||||
### `routstrd npubs`
|
||||
|
||||
Manage registered npubs and their roles/names (subcommand required). Management commands route through the auth proxy (`--auth-url`) and use NIP-98 auth.
|
||||
Manage registered npubs and their roles/names (subcommand required). Management
|
||||
commands route through the auth proxy (`--auth-url`) and use NIP-98 auth.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `routstrd npubs list` | List registered npubs with role and display name |
|
||||
| `routstrd npubs register [--name <name>]` | Register yourself as the first admin (bootstrap only) |
|
||||
| `routstrd npubs add <npub> [--role <role>] [--name <name>]` | Add an npub (accepts hex or npub1...) |
|
||||
| `routstrd npubs add <npub> [--role <role>] [--name <name>]` | Add an npub (accepts hex or npub1...); defaults to the `user` role |
|
||||
| `routstrd npubs update <npub> [--role <role>] [--name <name>]` | Update role and/or name (admin only) |
|
||||
| `routstrd npubs delete <npub>` | Delete an npub |
|
||||
|
||||
### `routstrd remote <url>`
|
||||
### `routstrd remote [url]`
|
||||
|
||||
Configure a remote daemon URL. Generates a Nostr identity (nsec/npub) for NIP-98 authentication automatically.
|
||||
With no URL, print the configured remote daemon. Pass a URL to configure one — a
|
||||
Nostr identity (nsec/npub) is generated automatically for NIP-98 authentication.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--auth-url <authUrl>` | URL of the auth proxy used by management commands (`npubs`, `clients`, `usage`) |
|
||||
|
||||
```sh
|
||||
routstrd remote https://your-remote-daemon.com
|
||||
routstrd remote # show current remote
|
||||
routstrd remote https://your-remote-daemon.com # configure one
|
||||
```
|
||||
|
||||
### `routstrd local`
|
||||
|
||||
Switch back to local daemon mode (clears the configured remote daemon URL).
|
||||
|
||||
### `routstrd refresh`
|
||||
|
||||
Refresh routstr21 models from Nostr and re-run integrations for all registered clients. Equivalent to `routstrd clients --manual-refresh`.
|
||||
Refresh routstr21 models from Nostr and re-run integrations for all registered
|
||||
clients. Equivalent to `routstrd clients --manual-refresh`.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `port` | number | 8008 | Daemon HTTP port |
|
||||
| `provider` | string\|null | null | Default provider URL |
|
||||
| `daemonUrl` | string\|null | null | Remote daemon URL |
|
||||
| `nsec` | string\|null | null | Nostr secret key for NIP-98 auth |
|
||||
| `cocodPath` | string\|null | null | Custom path to cocod executable |
|
||||
| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) |
|
||||
| `autoRefresh` | object | `{ enabled: true }` | Scheduled refresh job settings (`enabled`, `intervalMs`) |
|
||||
### `routstrd update`
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `ROUTSTRD_DIR` | `~/.routstrd` | Config directory |
|
||||
| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory |
|
||||
| `ROUTSTRD_WALLET_PID` | `<wallet>/wallet.pid` | In-process wallet lock path |
|
||||
| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory |
|
||||
Update routstrd to the latest version. Standalone-binary installs update
|
||||
in place; npm/bun installs are updated through the package manager.
|
||||
|
||||
### `routstrd mode`
|
||||
|
||||
Interactive prompt to set the client mode:
|
||||
1. **lazyrefund/apikeys** (default) — Pseudonymous accounts kept with Routstr nodes, refunded after 5 mins if unused.
|
||||
2. **xcashu** (coming soon) — Balances never kept with nodes, all refunded in response.
|
||||
1. **lazyrefund/apikeys** (default) — Pseudonymous accounts kept with Routstr
|
||||
nodes, refunded after 5 mins if unused.
|
||||
2. **xcashu** (coming soon) — Balances never kept with nodes, all refunded in
|
||||
response.
|
||||
|
||||
Changing mode restarts the daemon automatically.
|
||||
|
||||
### `routstrd monitor`
|
||||
### `routstrd monitor` / `routstrd top`
|
||||
|
||||
Open an interactive TUI (htop-like) for usage monitoring.
|
||||
Open an interactive TUI (htop-like) for usage monitoring. `top` is an alias.
|
||||
|
||||
### `routstrd logs`
|
||||
|
||||
@@ -211,17 +279,50 @@ View daemon logs.
|
||||
| `-f, --follow` | false | Follow log output (like `tail -f`) |
|
||||
| `-c, --coco` | false | Show Cashu wallet-engine (coco) logs instead of daemon logs |
|
||||
| `-n, --lines <number>` | 50 | Number of lines to show |
|
||||
| `-r, --recent` | false | List recent request IDs with their model |
|
||||
| `-i, --request-id <id>` | | Only show log lines for a specific request ID |
|
||||
|
||||
Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine (Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log` so they don't pollute the main daemon logs.
|
||||
Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine
|
||||
(Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log`
|
||||
so they don't pollute the main daemon logs.
|
||||
|
||||
### `routstrd service`
|
||||
|
||||
Manage routstrd as a system service using PM2, so it survives reboots.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `routstrd service install` | Install and start routstrd under PM2 |
|
||||
| `routstrd service uninstall` | Stop and remove routstrd from PM2 |
|
||||
| `routstrd service logs` | View PM2 logs for routstrd |
|
||||
|
||||
## Wallet Commands
|
||||
|
||||
New wallets automatically trust `https://mint.cubabitcoin.org` as their default mint. The default is used when a wallet command does not include `--mint-url`.
|
||||
New wallets automatically trust `https://mint.cubabitcoin.org` as their default
|
||||
mint. The default is used when a wallet command does not include `--mint-url`.
|
||||
|
||||
### `routstrd send <target>` / `routstrd receive <value>`
|
||||
|
||||
Shortcuts for the common wallet operations:
|
||||
|
||||
| Command | Behaviour |
|
||||
|---------|-----------|
|
||||
| `routstrd send 2100` | Numeric target → create a Cashu token for that many sats |
|
||||
| `routstrd send lnbc1...` | Non-numeric target → pay that Lightning invoice |
|
||||
| `routstrd receive 2100` | Numeric value → create a Lightning invoice for that many sats and wait for payment |
|
||||
| `routstrd receive cashuB...` | Non-numeric value → receive that Cashu token |
|
||||
|
||||
Both accept `--mint-url <url>`.
|
||||
|
||||
### `routstrd wallet status`
|
||||
|
||||
Check wallet status.
|
||||
|
||||
### `routstrd wallet doctor`
|
||||
|
||||
Diagnose conflicting wallets — the current routstrd wallet versus a legacy
|
||||
`cocod` wallet.
|
||||
|
||||
### `routstrd wallet unlock <passphrase>`
|
||||
|
||||
Unlock the wallet with a passphrase.
|
||||
@@ -230,6 +331,17 @@ Unlock the wallet with a passphrase.
|
||||
|
||||
Get wallet balance.
|
||||
|
||||
### `routstrd wallet cleanup`
|
||||
|
||||
Clear stuck pending/in-flight wallet operations.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--mint-url <url>` | all mints | Only clean up operations for this mint URL |
|
||||
| `--min-age <hours>` | 168 | Minimum age for reclaiming sends and cancelling melts (expired mint quotes are always failed) |
|
||||
| `--dry-run` | false | Report what would be cleaned without applying changes |
|
||||
| `-y, --yes` | false | Skip confirmation prompt |
|
||||
|
||||
### `routstrd wallet receive cashu <token>`
|
||||
|
||||
Receive funds via a Cashu token.
|
||||
@@ -274,6 +386,41 @@ Set the persistent default mint. If necessary, the mint is added as trusted firs
|
||||
|
||||
Get info about a specific mint.
|
||||
|
||||
### `routstrd wallet npc`
|
||||
|
||||
NPC (npubx.cash) Lightning address operations.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `routstrd wallet npc address` | Show this wallet's NPC Lightning address |
|
||||
| `routstrd wallet npc username <name> [--confirm]` | Claim an NPC username; `--confirm` pays the claim fee |
|
||||
| `routstrd wallet npc sync` | Manually sync paid NPC quotes into the wallet |
|
||||
|
||||
## NWC (Nostr Wallet Connect)
|
||||
|
||||
Connect an external Lightning wallet and let it fund the Cashu wallet.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `routstrd nwc connect [connection-string]` | Connect via `nostr+walletconnect://...` (prompts if omitted) |
|
||||
| `routstrd nwc disconnect` | Disconnect from the NWC wallet |
|
||||
| `routstrd nwc status` | Show connection status and wallet info |
|
||||
| `routstrd nwc fund <amount>` | Manually fund the Cashu wallet from the connected NWC wallet |
|
||||
|
||||
### `routstrd nwc auto-refill on`
|
||||
|
||||
Enable automatic wallet refill from NWC.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--threshold <sats>` | 500 | Refill when the Cashu balance drops below this |
|
||||
| `--amount <sats>` | 1000 | Refill this many sats at a time |
|
||||
| `--cooldown <seconds>` | 300 | Minimum time between refills |
|
||||
|
||||
### `routstrd nwc auto-refill off`
|
||||
|
||||
Disable auto-refill.
|
||||
|
||||
## Daemon API
|
||||
|
||||
The daemon exposes an OpenAI-compatible HTTP API at `http://localhost:8008`:
|
||||
@@ -298,6 +445,10 @@ Route a chat completion request.
|
||||
}
|
||||
```
|
||||
|
||||
The incoming request path is forwarded to the provider, so the Anthropic
|
||||
Messages API (`POST /v1/messages`) and the OpenAI Responses API
|
||||
(`POST /v1/responses`) are proxied in their own formats as well.
|
||||
|
||||
## Configuration
|
||||
|
||||
Config file: `~/.routstrd/config.json`
|
||||
@@ -305,10 +456,20 @@ Config file: `~/.routstrd/config.json`
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `port` | number | 8008 | Daemon HTTP port |
|
||||
| `host` | string | `"127.0.0.1"` | Bind address |
|
||||
| `provider` | string\|null | null | Default provider URL |
|
||||
| `cocodPath` | string\|null | null | Custom path to cocod executable |
|
||||
| `cocodPath` | string\|null | null | Custom path to a legacy cocod executable |
|
||||
| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) |
|
||||
| `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit |
|
||||
| `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) |
|
||||
| `authUrl` | string | — | Auth proxy URL for management commands |
|
||||
| `nsec` | string | — | Nostr secret key for NIP-98 auth |
|
||||
| `relays` | string[] | — | Nostr relays to use for discovery |
|
||||
| `routstrPubkey` | string | — | Override the Routstr announcement pubkey |
|
||||
| `routstrModelsPubkey` | string | — | Override the routstr21 models pubkey |
|
||||
| `nwc` | object | — | NWC settings (`mode`, `connectionString`, `autoRefill`) |
|
||||
| `autoRefresh` | object | `{ enabled: true }` | Scheduled refresh job settings (`enabled`, `intervalMs`) |
|
||||
| `requestResponseLogging` | object | — | Request/response log sink settings |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
@@ -317,17 +478,26 @@ Config file: `~/.routstrd/config.json`
|
||||
| `ROUTSTRD_DIR` | `~/.routstrd` | Config directory |
|
||||
| `ROUTSTRD_SOCKET` | `~/.routstrd/routstrd.sock` | IPC socket path |
|
||||
| `ROUTSTRD_PID` | `~/.routstrd/routstrd.pid` | PID file path |
|
||||
| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory |
|
||||
| `ROUTSTRD_WALLET_PID` | `<wallet>/wallet.pid` | In-process wallet lock path |
|
||||
| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory |
|
||||
|
||||
## Remote Mode
|
||||
|
||||
When `daemonUrl` is configured, commands connect to a remote daemon instead of a local one:
|
||||
When `daemonUrl` is configured, commands connect to a remote daemon instead of a
|
||||
local one:
|
||||
- Client names are suffixed with the last 7 chars of your npub
|
||||
- All requests are automatically NIP-98 signed using your local nsec
|
||||
- Local-only commands (`onboard`, `start`, `restart`, `mode`, `logs`, `service`) are disabled
|
||||
- Local-only commands (`onboard`, `start`, `restart`, `mode`, `logs`, `service`)
|
||||
are disabled
|
||||
|
||||
Run `routstrd local` to switch back.
|
||||
|
||||
## Pi Integration
|
||||
|
||||
When `routstrd onboard` runs, it automatically configures a `routstr` provider in `pi`'s `models.json` with an OpenAI-compatible base URL and API key. This allows pi (the AI coding agent) to use Routstr providers seamlessly.
|
||||
When `routstrd onboard` runs, it automatically configures a `routstr` provider in
|
||||
`pi`'s `models.json` with an OpenAI-compatible base URL and API key. This allows
|
||||
pi (the AI coding agent) to use Routstr providers seamlessly.
|
||||
|
||||
## File Locations
|
||||
|
||||
@@ -337,5 +507,21 @@ When `routstrd onboard` runs, it automatically configures a `routstr` provider i
|
||||
| `~/.routstrd/routstr.db` | SQLite database |
|
||||
| `~/.routstrd/routstrd.sock` | IPC socket |
|
||||
| `~/.routstrd/routstrd.pid` | PID file |
|
||||
| `~/.routstrd/wallet/` | In-process Cashu wallet data (`config.json`, `coco.db`, `wallet.pid`) |
|
||||
| `~/.routstrd/logs/YYYY-MM-DD.log` | Daily daemon log files |
|
||||
| `~/.routstrd/coco-logs/YYYY-MM-DD.log` | Daily Cashu wallet-engine (coco) log files |
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run lint # tsc --noEmit
|
||||
bun test
|
||||
bun run build # bundles dist/index.js and dist/daemon/index.js
|
||||
```
|
||||
|
||||
End-to-end smoke test against a running daemon (needs a funded client API key):
|
||||
|
||||
```sh
|
||||
ROUTSTRD_API_KEY=<api-key> bun run smoke <model-id> [model-id ...]
|
||||
```
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Coco 2.0.0 migration plan
|
||||
|
||||
> **Status:** not started as of routstrd v0.4.9.
|
||||
> Verified against `package.json`: still on `@cashu/coco-core ^1.0.1`,
|
||||
> `@cashu/coco-sqlite-bun ^1.0.1`, `coco-cashu-core 1.1.2-rc.50`,
|
||||
> `coco-cashu-plugin-npc 2.4.1`. The NPC compatibility gate in section 1 is
|
||||
> unresolved.
|
||||
|
||||
## Objectives
|
||||
|
||||
The migration must accomplish four things safely:
|
||||
@@ -23,6 +23,7 @@
|
||||
"monitor": "bun src/index.ts monitor",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"smoke": "scripts/smoke/chat-completions.sh",
|
||||
"build": "bun build src/index.ts --target=bun --outfile=dist/index.js --external better-sqlite3 && bun build src/daemon/index.ts --target=bun --outfile=dist/daemon/index.js --external better-sqlite3",
|
||||
"build:binary": "bun build --compile --no-compile-autoload-dotenv --no-compile-autoload-bunfig src/index.ts --outfile=dist/routstrd",
|
||||
"prepublishOnly": "bun run build"
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
const token =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGIBhc3hAMDJkZmYwZjUzZGYyYmM1NTJmNmVlZjdmMzhmOWMzZTU5MTY4MGY5ODM5M2I0MjA1YWQ0ZDA5YzlmMTUyNmVkZGFjWCED0Ln-y1Sx8ukCNDoKK4zepISUXAbIMnAj1yuyJGj3hVFhZKNhZVggk4ymtXIrLMRJf2bV7BOo5wmGfPeH1GlYRnG-mPKg-5phc1ggXf-FYynNa0xYgjub6aGIr_mFM-OEAwyx5VlXtWw7NXlhclggM9X2fnOOW8M-cL5Yvn_iAcb7uAcWremL2ZBZi1-TEu2kYWEBYXN4QGQxODY3MTQ4YWJlYTRhMmI5ODIzZDYyNmQ5ZWIwMmJiM2FiYmJlMjNiMzg0Y2U5M2U4N2MyMjAxOTU3YWNiZDVhY1ghA0S4fM1z2KiJKBmGk6qHVvGwnC6t78dcxX0A8Loy8_72YWSjYWVYIKVwButD6sTWCuMrdLGKj0SSdkO_Jl_sP6bAmnL65otzYXNYINBF5s_e9_WmXrI7GZ3n1KliaZk5Q9i4xN2N1JBUlVEdYXJYIGCJVxKUoIUDdtXLz7HFQg7nrBRIb3DJXt6eiveL03EqpGFhAmFzeEAxNTU3NmE4YTQ5Y2RhMzhhZjQ4N2YxYjhlYzk4ZWYwM2Q0MmZlZWY4NThjYzI3YWJlYjlmYmNlNzhlZTBlZWUxYWNYIQKLaUbivnGndI-kvs_u8KvUH0Sd3Mn7fP1iF2MfQPHgvWFko2FlWCCCCI_auq8R_eFrumwLYRHD_Nab7qe4_Ok8hTmGjkQ_FWFzWCANCP4IAbAw38HJktVCeZMScqYR-BMq-1VxIYRLbI-mXmFyWCDe3L3omLIxu02vqDYsQkIjIoy5HVwV5nz-bmB4F32VzaRhYRhAYXN4QDY1ODk1ZjViYTJkOTA4MjJmMzhmZjkyNWJlZDk4Y2Q1ZTM5NDgxNGJjZGI2MDA1ODUyMThlM2EzY2Y0MDM4ZWJhY1ghA0orpNWfYQihhMLqFuqYVdiCRnvzUs0R2LNbmxK0uivLYWSjYWVYIEKL2qFpFy00hsymiyfIpaKoRTbKexGHVIrO4MlF3FjzYXNYILzd6WqS2IDZrmPi2fFC3_w-LQc6aH0Z_-gr_PUL4vtpYXJYIATNPgMmsbuhLQYaWG-c9Fk0QmHrmxZap5SChukvmbM6pGFhEGFzeEA0MWE5OGY2NTlkM2E5YzgwNjIyN2I0Njg0ODljMTAzOGQxNzE5ZGI5ZTIxOTYwMWRkYmQ3ZGRkNTk1YmQ1ZjkwYWNYIQPMufMoE6Of8fyD2MEhPd3Y3KeKhWzepqOZXKYMQewQeGFko2FlWCCCdcrtZzqfHlXkF4Cm_23dztbXX92YiT8qmotuAxoRt2FzWCBTwbmVsAmQraHkJqsrobFhD3WZ8Uc8g8VBMKHlQBwHGmFyWCDOIIFn71ydyJGOeBuxrWc87hCWV1w_6KxzCLhLFIznDQ";
|
||||
const token2 =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCEpGFhGIBhc3hAMWE2YTEzZWFmMWY5Y2FiZDdjYzJhZDZhOTM0MmU4ZTE1MzJhMzljYmFhMjRiYmEzMTVmYzA0ZDM1ZGY5YzVjZmFjWCEDxc6EeUBD1ykxGkr5se8B6M0KIaoanysabCXFM3kTsTlhZKNhZVgggsoE5igqfQrqxxpuA4h7IOlu58rTPd5fgSDbt0PsvJ5hc1ggN1c8DaDHAKP7JeskTBCKMYod8Hbc1UASWMb1PdNYiithclggiFuUJSZ4xQKXpUSF3i56M3wMNfbAOoime-NEAJNvbzukYWEYQGFzeEBkZmNhNDJmNDYyMDcyOTFmNGRjNjY5NzE3MzVmMWY1NmRhNmVhODg3OGZhYTZjMzJkOGZhODEyMWI3YzU5NWE5YWNYIQPvTgwE_lEc4y5AvoR4afrvVeBg8aFaZbX-J2pi1ODQD2Fko2FlWCCKHJQPvkqTBzXSlGi7IohfC7dmZYKnJstkrh090EAAJmFzWCCL6_TLTYOtlPtb7E5zpJrlAwmVoaEeasDbWFdHq8bts2FyWCAkwP2KY06reSkqAre6S0eNp6tH6MOamrBYnMIdkTKCD6RhYRBhc3hAOTYwOTBhOWRhNDM0NzgwM2YyODMyM2MyZTQwNTJmMzE4MjBmMGVmZGMzMTBkMzYwNzZkYzc1NzU1MDRkNzgyZGFjWCECLAnqF1xnlwRHKkgcEpl1w73wXSco_TrqvHzvADRur-NhZKNhZVggs79rTYHAsMXduTS8VRFr17wY8BeS76I2w0plRSzGBdNhc1ggn5YnBEqQhPH7WbiQOIVmVoUL6DuOjXghnH6jsqEiVJphclggl5PftuvVrNaO-FaqHB_iALFFtAlccnG87PIWaTxW7NqkYWECYXN4QDcwNjQ5NWU4YWJkMDIyNjY2ZTc5ZjkwNWU2ZTk5NzU2MmVkZmJkZDU1NDA5NDMwMjNlYjA2ZjU5NDAzNjE2YTlhY1ghA_5PsdXzk5DXHulsXP6n9IadnIYcgqBiKo2ssr7APBFsYWSjYWVYIBj6uJBqZ5tJTqxDgPqhrBCJH3WsU8W73LZITtF_MhM8YXNYIDOLWwQWZ6S8y6Tr3IkuP7rfUSbjw2yP1ut5PNIif6KhYXJYIE-CACyXELfybso_kRN4w1uNKSJO7tc4Ke560ZnbLgFq";
|
||||
const token3 =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGIBhc3hAOGNhMTRlMjc1ZGM3ZTU2ZjgzMmNiMWQzMzg3MWQ1NTA0ZmY4ZDdjNDExYTlmMWIzZmY4NDliMDVlMWYwYjdkZWFjWCECqkWTlYeBbMjf9Gu9EvXuwMZSwahjlLdtoKYVUjGvMGphZKNhZVggZAlbzaB-f1W9V6DCXx9-UBR3Wrx6q5YPkdPzqdl2NLhhc1gg0iX6AuofKqUpxtYzTS8RK7dDSOhNeuqo4d-OAyEOY7thclggZJpwNkrTQmDQdvPgPuMz1UZoxJB2ntuAB_BJ3B8V_-ykYWEIYXN4QDAwZGUzNjc0OWI5OTMxNzUzODIwMWRiN2FmYmFkZGQyYmVkYWIwYzE4MGQ3YTk4YmQwMTU5ODhhNmU2NzNkN2ZhY1ghArRuuYHPk0zZDarPLFVKXKsWJdrOoVrX2cGOmJHlzj1fYWSjYWVYIA3tdTlmP3AWgUm67_8OGecU__-Bznp_5Z4gxdog4j-BYXNYIC4v6A0OeJ1ftOGaJ5xyDaRoeUcvxZC50k1zmptwca2uYXJYIKwoi2amxdoo6DmakRNC9o1BNBbcVLRovVLTZ_HWrwz6pGFhAmFzeEBlZTJjMDdjNDIyNWY1ZDc5NjUxOGEyN2VkYmVhMjVlNWNiODNlMTYyOTBlYzQ1MTA3MmZhMDMyNDZlZjMzYmZjYWNYIQLnKi-IHFdCqOzKcejPC325v_q7DDBWG4kGbEeEfYj1wmFko2FlWCDJFzWJNhHqQVocBT34rokTvd5MNc2l0yd-22mlrA_J42FzWCCnrH5rsQBu8M5xzz0jdh6iDw798PKXwIjjThsBrJmJ5GFyWCAP5YPIf8NRVQACOX8qTmCFJLly6MZdrPpLKW9crxX3TKRhYRhAYXN4QGU1YTVkODczY2JiN2ZiYjkwZjBhNWJhMjcwODZlNWUwNzczYjhhZmM2MjkxYjkyZDZjNmRjNWUyOWIwNjBmYThhY1ghAsOHo3BVNDkz9aexnLCBGC7fbjAt9qrkMLd5MFSwlkxDYWSjYWVYILVffHeqv2Wj3XBCAvVs5XMizFyO6QXKJX-WkgYejDbhYXNYIHwpn3nGOU8qIU05mvUoLjsQPC50iSp-m0pQc5h_TojGYXJYIFqpEyKa6zmx-es7MGHrBjS34unW7gHirPiWBtNhOYPjpGFhCGFzeEBjZTdjNWQwYTdhMTA1YjFhMGFlODdmNjQ5YjgxY2Y5MDUzNDBkZDg2ZWRmY2EyZDg5ZGI3ZmIwMGU3ZTEwNmFjYWNYIQKHOiRDX17csadc2W9ApXSC9z9OA9Ow_B_0Z3t-LjXlVWFko2FlWCA_BVVJUfd_8aqq6wyc2hna274nMlFJVcPUap8jfLHdZ2FzWCBf7TMpb7gy4H5p2JU4QU9aftsngHLoNRT03e-9IxpdtWFyWCAONKjqsMSZODlyVgbtHJH2Hs3R25mlK3tbM-GsR5KGJQ";
|
||||
const token4 =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGEBhc3hAM2JiY2M1OTk4MzA3YWQ2MDcyMTUxYzgyMmQ0N2ZmZGMzYTA4MGQ3MDIwZmM2NGM5MGY0NWYyNDA3ZDYwY2Y3Y2FjWCED1kLTzhP_9Z-emLn9p6ypf5WJnu37aXntJiLGczkuJIhhZKNhZVgg0gm2HX3PRE1e73hD53ETXD9K2sa7g74KME6OPHFyQ2xhc1ggD9yp5h77Lw5iVSeEM0pnvx3iOYi2KZ7USzzupHZkUNRhclggp5x96k0_bpDlYzUpsSS_YejP5xdbRKppx2ryUFkJQzakYWEBYXN4QGY5YzFmMjlkM2Q1MzgxODRjYmViNDA5Yjg4ZTNjM2MzNjU5M2I4NDVjMTA1N2RkNGI4MjY3N2M4NzQ1OWRiZjNhY1ghAzUJP4iEl3rzII-lBI6SD5BgsNPPIyb0otqvCjjeRMEKYWSjYWVYIBB_v8-hl5kgLCUuetZyS8bKYR0qDct9FatrHmktjtIPYXNYIDpeyARyUcMX8VI-aitlYYRrDZwnI0i2JK32h6vW7lNmYXJYIIw64U0Fnur7GKglqxmZLhqGwilg7EEvw7CF59ZpPrXepGFhGIBhc3hAOGZmOGYwODA1Zjg2NWFlMDUzNzY3NGZmMmQ0MTk2YjczNWViZWVkMDYxY2Q4NmQ5MmFkOTVkNjIxNGIyNmFkOWFjWCECJSfVy1j8HGKlIW8WPjaL7laDkh8S74bHgpo827L_yw5hZKNhZVggg24YJokE1Xz_g8rv1Lvs_YtoEHpbNNHnIE1pjJrYYo1hc1ggz5P19DDGdG3V_zNOdMoeBe-OlkhBcIiFFQ1h0ayfatphclgg6__UEWXTYs5oI4_VPh16pa6AYuqNf9IQjT_a4WDK8yKkYWEQYXN4QGMxNmMzM2ZjZTI4YmZhNzc5OWUyYmMwNDA3MzVkNmQzZmFlMTUyYjZkZTE0OTZhNjU5NzlmZjBmYzkwOWJjZDBhY1ghApQQGNGslVLEqkkT-lDnRo2aYqyXoT-_McAz4oS4sa-WYWSjYWVYIHFK50vYiJblhpmrkxfN-p0pyylK2dEinSuobMdJt8rRYXNYIBrTahdM6XVoJ02F9G7L9sLxdDX-I15k9rysVA33bF7iYXJYIIkE3phWIy8O1F-_7uiw8MbyVSvzmnipHdMV572pynoPpGFhAWFzeEA1NGNjM2UyODM1MGM4YzIzNjdlZjFkZjg0NGQ3NzRhMjM0NzYxZTA1OTJkNjg2OTRiMGQzOGMyZWU5MTEzNWQ3YWNYIQNmdhuY0oNhnb4QMBKtyP3QXXpUkmBDD9zTetSgK9op0mFko2FlWCCh5r-bIQFTdyFtpOU6YZoJW3cMDf3ruAdNgElSUPm9JmFzWCCiIT_E7W1gJjuATLUJVd7t1qhkPX94SJRd1f2kmvF1uGFyWCAr4psTvW75ntrNFghNvw1VBJ-Qt6elhDCSGpRcLXNN_Q";
|
||||
const token5 =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCEpGFhGIBhc3hAMzgxMTViZWY0NzA3MjNmZjczZGJhOTk5YWY2M2MwZDlhMmViMzQ3OTA1MWUzMjM3NjQ4YWFjYTVkMWFhN2Y1MGFjWCECw0MPnt_M_lwq5ZkmMvgtJHVa2A8PYTtbM6-C0do3NF5hZKNhZVggONdGnCnTzAOKKL9v1CnruOayiVpWDwGV9zRRHhh6KX9hc1ggSdj6FOCa1-ub6eI8hF42RbdMonb0-CYwRoWm0fSH7_phclggeQXZy7n8e7RzL1rjqwhYDjcqrUxj1v3aPAM_4cEaauqkYWEYQGFzeEA5NTg5YTFkODEyOTg3MzE0ZDFiYTE5ZDdjZmM1MzZlMTlmNGVkNGVlNTg3MDA4NjRmNjg4NjA0MjI2Mjk5YzFlYWNYIQNWML_D2vtlMYtimuEgIkGNYvaFZFWjyWwkZdyrT2ijeGFko2FlWCD1N0B9J5B91SxMhTg1PhWytUjWJPFOWvlm1UpXJ7UhqWFzWCASNE9dGijBP9jHIgyjnHnHI0G8a7qWsPd8Uhc0PwxS4mFyWCA3vukTqXvkF0Yit9w8wkyPWTIYLxHefjSHzaOuyxI6XaRhYRBhc3hANWJhOWM1MTA3ZTc1MWM4MGE2NzBiMjI2MTUxYjMyZDIzNzNiZjU1M2EwM2VlNTk2ZWQ1NTEyOTFiY2UwMWZmMWFjWCEC6f7l5IXXSaP9eRZmdUYs9liU2dVxNTaNPgPlPgnmzs1hZKNhZVggQaxRZNwg_cPymPLpgWLetqAMuUd5kRfMjNID22snM5dhc1gg0CDW54cjN5HF3FIogQAB3TqowQiH5aJ5ZbHIIKbX59Fhclggolu8liKdOYxCrwmU4p7VsPAflDJecMiUePmPLRmUwS6kYWECYXN4QDY5MDRjM2RkZmJlNjRmOGRmY2NkOWI0NWFmNWEyZTMzYzcyMzIzZTU5N2VkMGNiM2NmZTJhMWIxYzRmOGE4YjZhY1ghA5wv4H1I4NBMUy9qQWavrG6AW3pduPm-F7JUxYt0KC9kYWSjYWVYIIthil2Aj1kiIVmRlziK9NjEI_Dk1JKDn0pazj4Z8qVQYXNYIMNyB7-TbSR_9gNyO_YKohy3D79KhcsbAxV6MLi_RAnEYXJYIM4g3a130UC94KjWYufe-DWVGAQsGNTgd2fLT-DZlrS9";
|
||||
|
||||
// Hash the token with SHA256
|
||||
const hash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
console.log("SHA256 hash:", hash);
|
||||
|
||||
// Make the POST request
|
||||
const url = `https://llm.satsandsports.cash/v1/cashu-refund/${hash}`;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("Response:", JSON.stringify(data, null, 2));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error:", err.message);
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
const token =
|
||||
"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCGpGFhGIBhc3hAYzNmMDFjNDYxYjMxNmEwM2E2NGMzNjBmZDJhNjk1OThmMzE2MmM4NGMzZjlmZWExYWYxYTNjN2EyNWY3OTdmNGFjWCEC8iVrY11X2Hs_cYpWG7iD8Xt4DPcIZbVO0RAYoemP7udhZKNhZVggD1RQud6FdadMsbgJ_JB6h-qsJX0qLY83AIig3ocSZdNhc1ggOJIqxJpCn7dIqUO4lLLQE4VMs4TDu-bXWr0Kb3W8cYNhclgg_ZB2BEjqNIemFZ3GGGt-ZROgKNntpcyLX6eAJMYM6p2kYWECYXN4QGRlZjdhMjc5ZmNlZjY2ZmY5YzMyZDczODljYmJmNjUyNGIzY2M0OTQzYmM3MWJkNjA2ZDZiNzdhMjIzODIyMzNhY1ghA591WOMW3zhSXMajVUsgcXzoVReBsnxvq_xuy0gmRSI_YWSjYWVYIASUNdNthr7DNLBsCuIMisKt_KAUNXKVbcvHuUXVrxWCYXNYIBB5rsypISo_LcFKnaqEqbdEBi1SpWRgRLZfbFxqAo1IYXJYIMp7sk2t2k9vh1ZmhnILSlfo2245EqEvjgw8BryNY8QapGFhCGFzeEBhMWY2MzgwYjA2ZmE2NDhlMTY1NmQ5OWNmMThiYWNiYTdhZDVlMmNmNTY1MDAxMTc3NWQ3YjJkNTg5ODkwNzI1YWNYIQKSoj8-ipFq_s6Jes9APqaC5sNUz1icoZ0R6B3HCWoQ3GFko2FlWCAoiS9cHLbzHD9RWU6osZDCfo-czzEidp-pea9ejCEeSGFzWCA_YnzFgPvmXJX65r4QUJQICBcThdC7ZvdZlKqba598pmFyWCBHEneehhb0PxiFrwr6DgSZXWmWKft9WJwXlxr05HHmWqRhYRhAYXN4QDQ3NDkxMWZmYTk4ZTAzYTBkYWM5ODdjMjBjZWI5ZjlkMGE1MGNkNDM1ZjkwMDBhZWFhMjRkMGJlODgzMGM4ZjFhY1ghA0V01nVSIT46umSZL1XABGFGT4nAPnSYEOHUK5eT5xa-YWSjYWVYIHLOM7_1xe30zcNFFQEXgijF8joTo6yFLFDovHXWCr-sYXNYIILMBt2l053_K8HRRj5P-OWrVhg3oUF9NRYyzFQixEW3YXJYIIOu8UCudw8LMvm87O9X6A2VOS_AtDbXNGHEe2krTPTmpGFhCGFzeEA1N2I0OGNlNTNjMDI2MDM4ZmFlMTcxZjYzM2M5NzYzY2U3ZTA0M2E5MTk0N2VhZDM0Y2JhZDQwZGNmMTI5NmMwYWNYIQJ8KXhq9MuwGEw3xf4CFVpSuLmlMHohE4DByPpeW9NYqWFko2FlWCCHLILey0LgSBMe79A0f8gc4vzoI2AZ5vv53jWH9GUcFmFzWCBB7PcuhuKazJYtRXKMWtmQbRv66qkgdt3I5qetd7DfNGFyWCDBj4R1dWGOulvmQxsbpVJwAAM1RkOdSlgztd1ZOUxHfKRhYQRhc3hAMDJjZWQ4NzZjNGU1MGRjYmNmNDViN2U2MGZjNGRiYThiNzMzNzRkMDU2MWFiMTc0Nzc2ZDNjMmE5MjliOWU0NWFjWCED4OBpEfpelcMak8IAzXO8xRjon5bj5VC3dMjRF1js-gJhZKNhZVggpN8QqMUwG52q-t7xrwkDE6lS8Cw67-_68ERu0X2QuAphc1ggrYyoFeGIT73AT-3gd9tCK_1NYh84s9f63oBytxKTv2Vhclggvi2atyfhijg7QLbdEkNQkrHcW0ocJElhPrNmND9_so8";
|
||||
|
||||
// Make the POST request
|
||||
const url = `https://routstr.otrta.me/v1/balance/refund`;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Cashu": `${token}`,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("Response:", JSON.stringify(data, null, 2));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error:", err.message);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
# routstr Proxy Cost Logging Issue
|
||||
|
||||
## Problem
|
||||
|
||||
The routstr proxy at `localhost:8009` returns usage data that differs slightly from what the pi-ai `openai-completions` provider expects.
|
||||
|
||||
## What routstr Returns
|
||||
|
||||
**Streaming response (last chunk):**
|
||||
```json
|
||||
{
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 9,
|
||||
"total_tokens": 18,
|
||||
"cost": 0.000018,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0,
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0
|
||||
}
|
||||
},
|
||||
"cost": {
|
||||
"total_usd": 0.000018
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What pi-ai Expects
|
||||
|
||||
The `openai-completions` provider in pi-ai expects the standard OpenAI format:
|
||||
|
||||
```json
|
||||
{
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 9,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Current Handling
|
||||
|
||||
The pi-ai provider already handles the standard format correctly via `parseChunkUsage()`:
|
||||
- `input` ← `prompt_tokens - cached_tokens`
|
||||
- `output` ← `completion_tokens + reasoning_tokens`
|
||||
- `cacheRead` ← `prompt_tokens_details.cached_tokens`
|
||||
- `cacheWrite` ← **NOT CURRENTLY PARSED** (hardcoded to 0)
|
||||
|
||||
The provider then calculates cost using `calculateCost()` based on the model's configured cost per million tokens, ignoring any `cost` field from the response.
|
||||
|
||||
## Gap
|
||||
|
||||
The `parseChunkUsage()` function in `packages/ai/src/providers/openai-completions.ts` does not currently extract:
|
||||
1. `cache_write_tokens` from `prompt_tokens_details` (routstr-specific field)
|
||||
|
||||
Currently `cacheWrite` is hardcoded to 0.
|
||||
|
||||
## Resolution
|
||||
|
||||
The existing pi-ai `openai-completions` provider should work with routstr as-is since routstr returns the standard OpenAI format fields. The usage should be logged correctly if:
|
||||
1. `stream_options: { include_usage: true }` is passed
|
||||
2. The model has a `cost` configuration in the registry
|
||||
@@ -1,113 +0,0 @@
|
||||
# TUI refactor plan
|
||||
|
||||
## Goals
|
||||
- Move the usage TUI implementation out of `src/cli/usage-tui.ts` into a dedicated `src/tui/` folder.
|
||||
- Reduce the size and responsibility of the current monolithic file.
|
||||
- Keep the existing CLI entrypoint stable so current usage does not break.
|
||||
- Preserve behavior while making future TUI work easier.
|
||||
|
||||
## Current state
|
||||
`src/cli/usage-tui.ts` currently mixes several concerns in one file:
|
||||
- TUI-specific types and constants
|
||||
- ANSI/terminal helpers
|
||||
- scroll/search/vim navigation state
|
||||
- data fetching from the daemon
|
||||
- usage aggregation/stat helpers
|
||||
- rendering for all tabs
|
||||
- app lifecycle and keyboard event handling
|
||||
|
||||
This makes the file hard to extend safely.
|
||||
|
||||
## Refactor strategy
|
||||
Do this incrementally and keep a thin compatibility wrapper in `src/cli/usage-tui.ts`.
|
||||
|
||||
### Target structure
|
||||
- `src/tui/usage/index.ts`
|
||||
- public entrypoint: `runUsageTui()`
|
||||
- `src/tui/usage/types.ts`
|
||||
- `UsageStats`, tab ids, tab metadata, derived stat types
|
||||
- `src/tui/usage/constants.ts`
|
||||
- tabs, colors, model/client color maps
|
||||
- `src/tui/usage/terminal.ts`
|
||||
- ANSI helpers, width/height helpers, `stripAnsi`
|
||||
- `src/tui/usage/state.ts`
|
||||
- vim/search/scroll state and state mutation helpers
|
||||
- `src/tui/usage/data.ts`
|
||||
- `fetchUsage()` and usage aggregation helpers
|
||||
- `src/tui/usage/render.ts`
|
||||
- shared render helpers and tab renderers
|
||||
- `src/tui/usage/app.ts`
|
||||
- main loop, render orchestration, input handling, cleanup
|
||||
- `src/cli/usage-tui.ts`
|
||||
- compatibility wrapper that re-exports or calls `runUsageTui()` from `src/tui/usage`
|
||||
|
||||
## Design choices
|
||||
### 1. Keep CLI path compatibility
|
||||
Do not delete the CLI file outright. Turn it into a tiny wrapper:
|
||||
- minimal import from `../tui/usage/index.ts`
|
||||
- export `runUsageTui()`
|
||||
|
||||
This avoids breaking any existing imports or scripts.
|
||||
|
||||
### 2. Separate pure logic from side effects
|
||||
Keep these pure where possible:
|
||||
- aggregation helpers
|
||||
- formatting helpers
|
||||
- render helpers that return strings
|
||||
- scroll clamping logic
|
||||
|
||||
Keep side effects isolated in the app layer:
|
||||
- reading terminal size
|
||||
- writing to stdout
|
||||
- raw mode setup
|
||||
- signal handling
|
||||
- interval scheduling
|
||||
|
||||
### 3. Avoid over-engineering
|
||||
This should be a pragmatic refactor, not a framework:
|
||||
- no unnecessary classes
|
||||
- keep function-based design
|
||||
- only extract modules around clear responsibility boundaries
|
||||
|
||||
### 4. Preserve behavior first
|
||||
No UX changes unless needed to support the extraction.
|
||||
That means:
|
||||
- same tabs
|
||||
- same keybindings
|
||||
- same output format
|
||||
- same fetch cadence
|
||||
- same search/scroll behavior
|
||||
|
||||
## Implementation steps
|
||||
1. Create `src/tui/usage/`.
|
||||
2. Extract types/constants first.
|
||||
3. Extract terminal helpers.
|
||||
4. Extract data fetching + aggregation helpers.
|
||||
5. Extract state/search/scroll logic.
|
||||
6. Extract rendering helpers + tab renderers.
|
||||
7. Build `app.ts` using the extracted modules.
|
||||
8. Replace `src/cli/usage-tui.ts` with a thin wrapper.
|
||||
9. Run a TypeScript/bun check and fix imports.
|
||||
10. Smoke-test keyboard handling and rendering behavior.
|
||||
|
||||
## Risks
|
||||
- circular imports between render/state/constants
|
||||
- broken relative import paths during extraction
|
||||
- subtle behavior regressions in scroll/search state
|
||||
- terminal escape handling differences if helpers are split carelessly
|
||||
|
||||
## Validation checklist
|
||||
- `src/cli/usage-tui.ts` still exposes `runUsageTui()`
|
||||
- TUI starts from the same CLI path
|
||||
- scroll still works for long content
|
||||
- vim keys still work
|
||||
- arrow keys still work
|
||||
- tab switching still resets scroll
|
||||
- search mode still works
|
||||
- cleanup still restores cursor and alternate screen
|
||||
|
||||
## Non-goals
|
||||
- redesigning the UI
|
||||
- changing tab contents
|
||||
- introducing tests unless needed for safety
|
||||
- adding new features unrelated to the refactor
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { renderBox } from "./src/tui/usage/render.ts";
|
||||
import { stripAnsi } from "./src/tui/usage/terminal.ts";
|
||||
|
||||
const w = 40;
|
||||
const testBox = renderBox(["Hello World", "Line 2"], w, "Title");
|
||||
console.log(testBox);
|
||||
const lines = testBox.split("\n");
|
||||
lines.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`));
|
||||
|
||||
console.log("---");
|
||||
|
||||
const testBoxNoTitle = renderBox(["Hello World", "Line 2"], w);
|
||||
console.log(testBoxNoTitle);
|
||||
const lines2 = testBoxNoTitle.split("\n");
|
||||
lines2.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`));
|
||||
@@ -1,17 +0,0 @@
|
||||
import { renderBox } from "./src/tui/usage/render.ts";
|
||||
|
||||
const width = 80;
|
||||
const halfWidth1 = Math.floor(width / 2);
|
||||
const halfWidth2 = width - halfWidth1;
|
||||
|
||||
const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"];
|
||||
const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"];
|
||||
|
||||
const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats");
|
||||
const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats");
|
||||
|
||||
const leftLines = leftBoxStr.split("\n");
|
||||
const rightLines = rightBoxStr.split("\n");
|
||||
|
||||
const combinedContent = leftLines.map((l, i) => l + (rightLines[i] || " ".repeat(halfWidth2))).join("\n");
|
||||
console.log(combinedContent);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { renderBox } from "./src/tui/usage/render.ts";
|
||||
|
||||
const width = 80;
|
||||
const halfWidth1 = Math.floor(width / 2);
|
||||
const halfWidth2 = width - halfWidth1;
|
||||
|
||||
const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"];
|
||||
const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"];
|
||||
|
||||
const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats");
|
||||
const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats");
|
||||
|
||||
const leftLines = leftBoxStr.split("\n");
|
||||
const rightLines = rightBoxStr.split("\n");
|
||||
|
||||
const maxLines = Math.max(leftLines.length, rightLines.length);
|
||||
const combinedLines: string[] = [];
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const l = leftLines[i] || " ".repeat(Math.floor(width / 2));
|
||||
const r = rightLines[i] || " ".repeat(Math.ceil(width / 2));
|
||||
combinedLines.push(l + r);
|
||||
}
|
||||
console.log(combinedLines.join("\n"));
|
||||
@@ -1,223 +0,0 @@
|
||||
# Report: why `POST /v1/messages` on port 8008 returns chat-completions chunks while port 8009 preserves Messages API format
|
||||
|
||||
Date: 2026-03-28
|
||||
|
||||
## Summary
|
||||
|
||||
I tested the same request against both local daemons using `scripts/test-direct-local.ts` semantics (`POST /v1/messages`, `stream: true`).
|
||||
|
||||
- `localhost:8009` preserves the Anthropic/OpenAI Messages-style stream as expected:
|
||||
- `message_start`
|
||||
- `content_block_start`
|
||||
- `content_block_delta`
|
||||
- `message_delta`
|
||||
- `message_stop`
|
||||
- `localhost:8008` returns OpenAI chat-completions streaming chunks instead:
|
||||
- `object: "chat.completion.chunk"`
|
||||
- `choices[].delta`
|
||||
|
||||
This is **not** because the SDK itself is incapable of preserving `/v1/messages`.
|
||||
It happens because the two daemons call the SDK differently.
|
||||
|
||||
---
|
||||
|
||||
## What I tested
|
||||
|
||||
### 8008 (`../routstrd/`)
|
||||
|
||||
Listener:
|
||||
- `../routstrd/src/daemon/index.ts`
|
||||
- request handler in `../routstrd/src/daemon/http/index.ts`
|
||||
|
||||
Observed result for `POST http://localhost:8008/v1/messages`:
|
||||
- response `content-type: text/event-stream`
|
||||
- SSE payload is OpenAI chat-completions chunks (`chat.completion.chunk`)
|
||||
|
||||
### 8009 (`routstr-chat/scripts/routstr-daemon.ts`)
|
||||
|
||||
Listener:
|
||||
- `scripts/routstr-daemon.ts`
|
||||
|
||||
Observed result for `POST http://localhost:8009/v1/messages`:
|
||||
- response `content-type: text/event-stream`
|
||||
- SSE payload preserves Messages API events (`message_start`, `content_block_delta`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Direct cause
|
||||
|
||||
### 8009 forwards the incoming path to the SDK
|
||||
|
||||
In `routstr-chat/scripts/routstr-daemon.ts`, the request is routed with:
|
||||
|
||||
```ts
|
||||
await routeRequestsToNodeResponse({
|
||||
modelId,
|
||||
requestBody,
|
||||
path: url.pathname,
|
||||
headers: forwardedHeaders,
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Because it passes:
|
||||
|
||||
```ts
|
||||
path: url.pathname
|
||||
```
|
||||
|
||||
an incoming request to `/v1/messages` stays `/v1/messages` all the way through the SDK and upstream provider routing.
|
||||
|
||||
### 8008 does **not** forward the incoming path
|
||||
|
||||
In `../routstrd/src/daemon/http/index.ts`, the request is routed with:
|
||||
|
||||
```ts
|
||||
const response = await routeRequests({
|
||||
modelId,
|
||||
requestBody,
|
||||
forcedProvider,
|
||||
headers: incomingHeaders,
|
||||
walletAdapter: deps.walletAdapter,
|
||||
storageAdapter: deps.storageAdapter,
|
||||
providerRegistry: deps.providerRegistry,
|
||||
discoveryAdapter: deps.discoveryAdapter,
|
||||
modelManager: deps.modelManager,
|
||||
debugLevel: "DEBUG",
|
||||
mode: deps.mode,
|
||||
usageTrackingDriver: deps.usageTrackingDriver,
|
||||
sdkStore: deps.store,
|
||||
});
|
||||
```
|
||||
|
||||
Notice: **no `path` is passed**.
|
||||
|
||||
In the SDK, `routeRequests()` defaults the path to:
|
||||
|
||||
```ts
|
||||
path = "/v1/chat/completions"
|
||||
```
|
||||
|
||||
from:
|
||||
- `routstr-chat/sdk/routeRequests.ts`
|
||||
|
||||
So even when the client calls:
|
||||
|
||||
```http
|
||||
POST /v1/messages
|
||||
```
|
||||
|
||||
on port 8008, the daemon internally re-routes it as:
|
||||
|
||||
```http
|
||||
POST /v1/chat/completions
|
||||
```
|
||||
|
||||
That is why the upstream/provider response is converted into chat-completions format.
|
||||
|
||||
---
|
||||
|
||||
## Why the behavior differs even though both are built on the same SDK
|
||||
|
||||
Both daemons use the same SDK primitives, but:
|
||||
|
||||
- **8009** uses `routeRequestsToNodeResponse(...)` and explicitly passes `path: url.pathname`
|
||||
- **8008** uses `routeRequests(...)` and relies on the SDK default path, which is `/v1/chat/completions`
|
||||
|
||||
So the format difference is caused by **daemon integration code**, not by a provider-specific quirk and not by an unavoidable SDK conversion.
|
||||
|
||||
---
|
||||
|
||||
## Important implementation detail
|
||||
|
||||
The SDK helper itself documents this default:
|
||||
|
||||
- `routstr-chat/sdk/routeRequests.ts`
|
||||
|
||||
```ts
|
||||
/** Optional: API path (defaults to /v1/chat/completions) */
|
||||
```
|
||||
|
||||
and in `resolveRouteRequestContext(...)`:
|
||||
|
||||
```ts
|
||||
path = "/v1/chat/completions"
|
||||
```
|
||||
|
||||
Therefore any caller that omits `path` will get chat-completions semantics by default.
|
||||
|
||||
---
|
||||
|
||||
## Evidence from runtime tests
|
||||
|
||||
### Port 8008
|
||||
|
||||
Observed streamed chunks included:
|
||||
|
||||
- `object: "chat.completion.chunk"`
|
||||
- `choices[0].delta.content`
|
||||
- final `[DONE]`
|
||||
|
||||
### Port 8009
|
||||
|
||||
Observed streamed events included:
|
||||
|
||||
- `event: message_start`
|
||||
- `event: content_block_start`
|
||||
- `event: content_block_delta`
|
||||
- `event: message_delta`
|
||||
- `event: message_stop`
|
||||
- final `[DONE]`
|
||||
|
||||
This matches the code-path difference above.
|
||||
|
||||
---
|
||||
|
||||
## Recommended fix
|
||||
|
||||
In `../routstrd/src/daemon/http/index.ts`, pass the incoming path through to the SDK:
|
||||
|
||||
```ts
|
||||
const response = await routeRequests({
|
||||
modelId,
|
||||
requestBody,
|
||||
path: url.pathname,
|
||||
forcedProvider,
|
||||
headers: incomingHeaders,
|
||||
walletAdapter: deps.walletAdapter,
|
||||
storageAdapter: deps.storageAdapter,
|
||||
providerRegistry: deps.providerRegistry,
|
||||
discoveryAdapter: deps.discoveryAdapter,
|
||||
modelManager: deps.modelManager,
|
||||
debugLevel: "DEBUG",
|
||||
mode: deps.mode,
|
||||
usageTrackingDriver: deps.usageTrackingDriver,
|
||||
sdkStore: deps.store,
|
||||
});
|
||||
```
|
||||
|
||||
This should make `POST /v1/messages` on port 8008 preserve the Messages API format, matching port 8009.
|
||||
|
||||
---
|
||||
|
||||
## Secondary note
|
||||
|
||||
Port 8008 currently uses `routeRequests(...)` and then manually streams the returned response body to `res`.
|
||||
Port 8009 uses `routeRequestsToNodeResponse(...)` directly.
|
||||
|
||||
That difference is probably not the root cause here.
|
||||
The root cause is specifically that **8008 drops the incoming request path and falls back to the SDK default of `/v1/chat/completions`**.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The reason `localhost:8008/v1/messages` appears to "convert to chat completions" is:
|
||||
|
||||
1. `../routstrd` receives `/v1/messages`
|
||||
2. its handler calls `routeRequests(...)` without `path`
|
||||
3. the SDK defaults `path` to `/v1/chat/completions`
|
||||
4. the upstream request is therefore made against chat completions
|
||||
5. the stream returned is chat-completions SSE, not Messages API SSE
|
||||
|
||||
Port 8009 works because it explicitly forwards `url.pathname` into the SDK call.
|
||||
Reference in New Issue
Block a user