Merge pull request #124 from Routstr/fix/strict-provider-pin

chore(deps): require @routstr/sdk ^0.4.9 for strict provider pin
This commit is contained in:
redshift
2026-10-04 14:41:20 +00:00
committed by GitHub
4 changed files with 42 additions and 6 deletions
+23 -1
View File
@@ -125,10 +125,11 @@ routstrd start --host 0.0.0.0
Only expose the daemon behind appropriate network controls. Only expose the daemon behind appropriate network controls.
With specific provider: Pin all requests to one provider (no cross-provider failover):
```sh ```sh
routstrd start --provider https://your-provider.com routstrd start --provider https://your-provider.com
``` ```
See [Provider pinning](#provider-pinning) for the request-level header/query.
### CLI Commands ### CLI Commands
@@ -233,6 +234,23 @@ Any unmatched `POST` path is proxied to the selected provider with the incoming
path preserved, so `POST /v1/messages` (Anthropic Messages API) and path preserved, so `POST /v1/messages` (Anthropic Messages API) and
`POST /v1/responses` (OpenAI Responses API) work in their own formats too. `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: Request body:
```json ```json
{ {
@@ -292,6 +310,10 @@ choose and pin an advertised model path for `deepseek-v4.1-flash` requests.
Explicit `x-routstr-model-path` request headers work independently of this Explicit `x-routstr-model-path` request headers work independently of this
setting and take precedence. Restart the daemon after changing `autoModelPath`. 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.
### Environment Variables ### Environment Variables
- `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`) - `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`)
+16 -2
View File
@@ -49,7 +49,7 @@ Start the background daemon process.
|--------|-------------| |--------|-------------|
| `--port <port>` | Port to listen on (default: 8008) | | `--port <port>` | Port to listen on (default: 8008) |
| `--host <host>` | Bind address (default: 127.0.0.1) | | `--host <host>` | Bind address (default: 127.0.0.1) |
| `-p, --provider <provider>` | Default provider to use | | `-p, --provider <provider>` | Pin all requests to this provider (no cross-provider failover) |
### `routstrd daemon` ### `routstrd daemon`
@@ -479,6 +479,20 @@ The incoming request path is forwarded to the provider, so the Anthropic
Messages API (`POST /v1/messages`) and the OpenAI Responses API Messages API (`POST /v1/messages`) and the OpenAI Responses API
(`POST /v1/responses`) are proxied in their own formats as well. (`POST /v1/responses`) are proxied in their own formats as well.
#### Provider pinning
A provider can be pinned with any of:
- request header `x-routstr-provider: <provider-url>`
- query parameter `?provider=<provider-url>` (takes precedence over the header)
- config `provider` / `routstrd start --provider <provider-url>`
All three are **strict pins**: the request is routed only to that provider. If
it answers with an error (including a 400/422 upstream rejection), 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. Without
a pin, the router falls over to the next-cheapest eligible provider as usual.
## Configuration ## Configuration
Config file: `~/.routstrd/config.json` Config file: `~/.routstrd/config.json`
@@ -487,7 +501,7 @@ Config file: `~/.routstrd/config.json`
|-------|------|---------|-------------| |-------|------|---------|-------------|
| `port` | number | 8008 | Daemon HTTP port | | `port` | number | 8008 | Daemon HTTP port |
| `host` | string | `"127.0.0.1"` | Bind address | | `host` | string | `"127.0.0.1"` | Bind address |
| `provider` | string\|null | null | Default provider URL | | `provider` | string\|null | null | Pinned provider URL; all requests go only to this node (no cross-provider failover) |
| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) | | `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) |
| `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit | | `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit |
| `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) | | `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) |
+2 -2
View File
@@ -8,7 +8,7 @@
"@cashu/cashu-ts": "^4.3.0", "@cashu/cashu-ts": "^4.3.0",
"@cashu/coco-core": "^1.0.1", "@cashu/coco-core": "^1.0.1",
"@cashu/coco-sqlite-bun": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1",
"@routstr/sdk": "^0.4.8", "@routstr/sdk": "^0.4.9",
"@scure/bip39": "^2.2.0", "@scure/bip39": "^2.2.0",
"applesauce-core": "^5.1.0", "applesauce-core": "^5.1.0",
"applesauce-relay": "^5.1.0", "applesauce-relay": "^5.1.0",
@@ -98,7 +98,7 @@
"@panva/hpke-noble": ["@panva/hpke-noble@1.1.3", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-zPG7MR9x7QE7+KdYsKBO9H0vp3AdYt9/4AT3ab7T7W6SL0fdRqhgNRu8q4OGTJNLeKpdbkkRb6LhBDaA9+9xWQ=="], "@panva/hpke-noble": ["@panva/hpke-noble@1.1.3", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-zPG7MR9x7QE7+KdYsKBO9H0vp3AdYt9/4AT3ab7T7W6SL0fdRqhgNRu8q4OGTJNLeKpdbkkRb6LhBDaA9+9xWQ=="],
"@routstr/sdk": ["@routstr/sdk@0.4.8", "", { "dependencies": { "@cashu/cashu-ts": "^4.11.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.3.2", "rxjs": "^7.8.1", "tinfoil": "^1.2.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-ThcV9OM4vang6V4L61bIwQCsP3yvWWoHv3MPmgLeR0PUbot3UiWl7tmRAGe66CzGDy9UUd1z/ACSjUtmpb6GfQ=="], "@routstr/sdk": ["@routstr/sdk@0.4.9", "", { "dependencies": { "@cashu/cashu-ts": "^4.11.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.3.2", "rxjs": "^7.8.1", "tinfoil": "^1.2.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-kmqgpL6HpwRoritwR5s42B8h6fC6FzOjJgFzi1ath0v+tC4ppRWxeOloe3IaoHAovmh2GosRbN1rFPpb1wIXSw=="],
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
+1 -1
View File
@@ -46,7 +46,7 @@
"@cashu/cashu-ts": "^4.3.0", "@cashu/cashu-ts": "^4.3.0",
"@cashu/coco-core": "^1.0.1", "@cashu/coco-core": "^1.0.1",
"@cashu/coco-sqlite-bun": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1",
"@routstr/sdk": "^0.4.8", "@routstr/sdk": "^0.4.9",
"@scure/bip39": "^2.2.0", "@scure/bip39": "^2.2.0",
"applesauce-core": "^5.1.0", "applesauce-core": "^5.1.0",
"applesauce-relay": "^5.1.0", "applesauce-relay": "^5.1.0",