mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-31 15:56:14 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0206ff01a0 | ||
|
|
5620564643 |
@@ -51,26 +51,14 @@ curl https://api.routstr.com/v1/chat/completions \
|
||||
|
||||
## Quick Start (Docker)
|
||||
|
||||
If you are a node runner, start a Routstr Core instance using Docker Compose:
|
||||
If you are a node runner, start a Routstr Core instance and configure upstream access in the dashboard.
|
||||
|
||||
1. **Prepare your `.env`**:
|
||||
```bash
|
||||
ADMIN_PASSWORD=mysecretpassword
|
||||
NAME="My AI Node"
|
||||
DESCRIPTION="Fast access to models"
|
||||
NSEC=yournsec
|
||||
RECEIVE_LN_ADDRESS=yourname@wallet.com
|
||||
```
|
||||
|
||||
2. **Start the services**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. **Configure**:
|
||||
Open [http://localhost:8000/admin/](http://localhost:8000/admin/) to connect your AI providers and set pricing.
|
||||
|
||||
For full instructions, see the **[Provider Quick Start Guide](https://docs.routstr.com/provider/quickstart/)**.
|
||||
```bash
|
||||
docker run -d \
|
||||
--name routstr-proxy \
|
||||
-p 8000:8000 \
|
||||
ghcr.io/routstr/proxy:latest
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -6,7 +6,16 @@ Production deployment guide for Routstr Provider nodes.
|
||||
|
||||
For production, use Docker Compose with persistent storage and optional Tor support.
|
||||
|
||||
Use the included `compose.yml` for a flexible setup that handles both the UI and the node execution. This is useful for development or when you want to manage Tor as a separate service.
|
||||
### Unified Setup (All-in-one)
|
||||
To build and run the node with the UI integrated in a single container using the multi-stage build:
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.full -t routstr-full .
|
||||
docker run -d -p 8000:8000 --env-file .env routstr-full
|
||||
```
|
||||
|
||||
### Advanced Setup (Separated UI & Node)
|
||||
Use the included `compose.yml` for a more flexible setup that separates the UI build process from the node execution. This is useful for development or when you want to manage Tor as a separate service.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
@@ -175,16 +184,20 @@ docker compose up -d
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Using Docker Compose
|
||||
The easiest way to build everything from source:
|
||||
### Unified Image (UI + Node)
|
||||
The easiest way to build everything from source into a single production-ready image:
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
docker build -f Dockerfile.full -t routstr-full .
|
||||
```
|
||||
|
||||
### Individual Components
|
||||
If you prefer building the node only (requires manual UI build first):
|
||||
If you prefer building them separately or using Docker Compose:
|
||||
|
||||
```bash
|
||||
# Build using compose
|
||||
docker compose build
|
||||
|
||||
# Or build the node only (requires manual UI build first)
|
||||
docker build -t routstr-node .
|
||||
```
|
||||
|
||||
+24
-12
@@ -35,7 +35,6 @@ ADMIN_PASSWORD=mysecretpassword
|
||||
# Node Identity
|
||||
NAME="My AI Node"
|
||||
DESCRIPTION="Fast access to models"
|
||||
NSEC=yournsec
|
||||
|
||||
# Lightning Payouts
|
||||
RECEIVE_LN_ADDRESS=yourname@wallet.com
|
||||
@@ -44,10 +43,32 @@ RECEIVE_LN_ADDRESS=yourname@wallet.com
|
||||
|
||||
## 2. Start the Node
|
||||
|
||||
The recommended way to run Routstr is using Docker Compose, which handles the node, the UI, and optional services like Tor.
|
||||
You can run the pre-built image directly:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker run -d \
|
||||
--name routstr \
|
||||
-p 8000:8000 \
|
||||
--env-file .env \
|
||||
-v routstr-data:/app/data \
|
||||
ghcr.io/routstr/proxy:latest
|
||||
```
|
||||
|
||||
*Note: The pre-built image does not contain the UI. For the all-in-one experience with the Admin Dashboard, use the Build from Source instructions below.*
|
||||
|
||||
### Build from Source (Recommended)
|
||||
|
||||
If you want to build the node and UI yourself from source, use the unified Dockerfile:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/routstr/routstr-core.git
|
||||
cd routstr-core
|
||||
# Edit your .env with ADMIN_PASSWORD and API keys
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
|
||||
docker build -f Dockerfile.full -t routstr-local .
|
||||
docker run -d -p 8000:8000 --env-file .env --name routstr routstr-local
|
||||
```
|
||||
|
||||
Verify it's running:
|
||||
@@ -56,15 +77,6 @@ Verify it's running:
|
||||
curl http://localhost:8000/v1/info
|
||||
```
|
||||
|
||||
### Build from Source (Optional)
|
||||
|
||||
If you've cloned the repository and want to build the images yourself:
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configure via Dashboard
|
||||
|
||||
@@ -182,11 +182,16 @@ def _row_to_model(
|
||||
)
|
||||
|
||||
if apply_provider_fee:
|
||||
(
|
||||
parsed_pricing.max_prompt_cost,
|
||||
parsed_pricing.max_completion_cost,
|
||||
parsed_pricing.max_cost,
|
||||
) = _calculate_usd_max_costs(model)
|
||||
max_prompt, max_completion, max_cost = _calculate_usd_max_costs(model)
|
||||
parsed_pricing = Pricing(
|
||||
**{
|
||||
**parsed_pricing.dict(),
|
||||
"max_prompt_cost": max_prompt,
|
||||
"max_completion_cost": max_completion,
|
||||
"max_cost": max_cost,
|
||||
}
|
||||
)
|
||||
model = Model(**{**model.dict(), "pricing": parsed_pricing, "sats_pricing": None})
|
||||
|
||||
try:
|
||||
sats_to_usd = sats_usd_price()
|
||||
|
||||
+19
-62
@@ -677,9 +677,7 @@ class BaseUpstreamProvider:
|
||||
response_json["usage"]["cost_sats"] = (
|
||||
cost_data.get("total_msats", 0) // 1000
|
||||
)
|
||||
response_json["usage"]["remaining_balance_msats"] = (
|
||||
remaining_balance_msats
|
||||
)
|
||||
response_json["usage"]["remaining_balance_msats"] = remaining_balance_msats
|
||||
|
||||
# Keep detailed cost
|
||||
response_json["metadata"] = response_json.get("metadata", {})
|
||||
@@ -755,11 +753,7 @@ class BaseUpstreamProvider:
|
||||
raise
|
||||
|
||||
async def handle_streaming_responses_completion(
|
||||
self,
|
||||
response: httpx.Response,
|
||||
key: ApiKey,
|
||||
max_cost_for_model: int,
|
||||
requested_model: str | None = None,
|
||||
self, response: httpx.Response, key: ApiKey, max_cost_for_model: int, requested_model: str | None = None
|
||||
) -> StreamingResponse:
|
||||
"""Handle streaming Responses API responses with token usage tracking and cost adjustment.
|
||||
|
||||
@@ -1006,9 +1000,7 @@ class BaseUpstreamProvider:
|
||||
response_json["usage"]["cost_sats"] = (
|
||||
cost_data.get("total_msats", 0) // 1000
|
||||
)
|
||||
response_json["usage"]["remaining_balance_msats"] = (
|
||||
remaining_balance_msats
|
||||
)
|
||||
response_json["usage"]["remaining_balance_msats"] = remaining_balance_msats
|
||||
|
||||
# Keep detailed cost
|
||||
response_json["metadata"] = response_json.get("metadata", {})
|
||||
@@ -1317,9 +1309,7 @@ class BaseUpstreamProvider:
|
||||
path = self.normalize_request_path(path, model_obj)
|
||||
url = self.build_request_url(path, model_obj)
|
||||
|
||||
original_model_id = (
|
||||
(model_obj.forwarded_model_id or model_obj.id) if model_obj else None
|
||||
)
|
||||
original_model_id = (model_obj.forwarded_model_id or model_obj.id) if model_obj else None
|
||||
|
||||
transformed_body = self.prepare_request_body(request_body, model_obj)
|
||||
|
||||
@@ -1479,10 +1469,7 @@ class BaseUpstreamProvider:
|
||||
background_tasks.add_task(response.aclose)
|
||||
background_tasks.add_task(client.aclose)
|
||||
result = await self.handle_streaming_chat_completion(
|
||||
response,
|
||||
key,
|
||||
max_cost_for_model,
|
||||
background_tasks,
|
||||
response, key, max_cost_for_model, background_tasks,
|
||||
requested_model=original_model_id,
|
||||
)
|
||||
result.background = background_tasks
|
||||
@@ -1492,10 +1479,7 @@ class BaseUpstreamProvider:
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return await self.handle_non_streaming_chat_completion(
|
||||
response,
|
||||
key,
|
||||
session,
|
||||
max_cost_for_model,
|
||||
response, key, session, max_cost_for_model,
|
||||
requested_model=original_model_id,
|
||||
)
|
||||
finally:
|
||||
@@ -1611,9 +1595,7 @@ class BaseUpstreamProvider:
|
||||
path = self.normalize_request_path(path, model_obj)
|
||||
url = self.build_request_url(path, model_obj)
|
||||
|
||||
original_model_id = (
|
||||
(model_obj.forwarded_model_id or model_obj.id) if model_obj else None
|
||||
)
|
||||
original_model_id = (model_obj.forwarded_model_id or model_obj.id) if model_obj else None
|
||||
|
||||
transformed_body = self.prepare_responses_request_body(request_body, model_obj)
|
||||
|
||||
@@ -1701,9 +1683,7 @@ class BaseUpstreamProvider:
|
||||
|
||||
if is_streaming and response.status_code == 200:
|
||||
result = await self.handle_streaming_responses_completion(
|
||||
response,
|
||||
key,
|
||||
max_cost_for_model,
|
||||
response, key, max_cost_for_model,
|
||||
requested_model=original_model_id,
|
||||
)
|
||||
background_tasks = BackgroundTasks()
|
||||
@@ -1715,10 +1695,7 @@ class BaseUpstreamProvider:
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return await self.handle_non_streaming_responses_completion(
|
||||
response,
|
||||
key,
|
||||
session,
|
||||
max_cost_for_model,
|
||||
response, key, session, max_cost_for_model,
|
||||
requested_model=original_model_id,
|
||||
)
|
||||
finally:
|
||||
@@ -2145,10 +2122,7 @@ class BaseUpstreamProvider:
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
refund_amount, unit, mint, payment_token_hash,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
@@ -2190,9 +2164,7 @@ class BaseUpstreamProvider:
|
||||
try:
|
||||
data_json = json.loads(line[6:])
|
||||
if "usage" in data_json and data_json["usage"]:
|
||||
data_json["usage"]["cost_sats"] = (
|
||||
cost_data.total_msats // 1000
|
||||
)
|
||||
data_json["usage"]["cost_sats"] = cost_data.total_msats // 1000
|
||||
lines[i] = "data: " + json.dumps(data_json)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -2293,10 +2265,7 @@ class BaseUpstreamProvider:
|
||||
|
||||
if refund_amount > 0:
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
refund_amount, unit, mint, payment_token_hash,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
@@ -2351,6 +2320,7 @@ class BaseUpstreamProvider:
|
||||
extra={
|
||||
"original_amount": amount,
|
||||
"refund_amount": emergency_refund,
|
||||
"deduction": 60,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2525,10 +2495,7 @@ class BaseUpstreamProvider:
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
amount - 60, unit, mint, payment_token_hash,
|
||||
request_id=getattr(request.state, "request_id", None),
|
||||
)
|
||||
|
||||
@@ -2820,10 +2787,7 @@ class BaseUpstreamProvider:
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
amount - 60, unit, mint, payment_token_hash,
|
||||
request_id=getattr(request.state, "request_id", None),
|
||||
)
|
||||
|
||||
@@ -3087,10 +3051,7 @@ class BaseUpstreamProvider:
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
refund_amount, unit, mint, payment_token_hash,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
@@ -3132,9 +3093,7 @@ class BaseUpstreamProvider:
|
||||
try:
|
||||
data_json = json.loads(line[6:])
|
||||
if "usage" in data_json and data_json["usage"]:
|
||||
data_json["usage"]["cost_sats"] = (
|
||||
cost_data.total_msats // 1000
|
||||
)
|
||||
data_json["usage"]["cost_sats"] = cost_data.total_msats // 1000
|
||||
lines[i] = "data: " + json.dumps(data_json)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -3223,10 +3182,7 @@ class BaseUpstreamProvider:
|
||||
|
||||
if refund_amount > 0:
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
payment_token_hash,
|
||||
refund_amount, unit, mint, payment_token_hash,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
@@ -3281,6 +3237,7 @@ class BaseUpstreamProvider:
|
||||
extra={
|
||||
"original_amount": amount,
|
||||
"refund_amount": emergency_refund,
|
||||
"deduction": 60,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -535,7 +535,7 @@ async def periodic_refund_sweep() -> None:
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if "already spent" in error_msg:
|
||||
refund.collected = True
|
||||
refund.swept = True
|
||||
session.add(refund)
|
||||
logger.info(
|
||||
"Refund already spent (client collected), marking swept",
|
||||
|
||||
Reference in New Issue
Block a user