mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
docs: Add comprehensive documentation for Routstr Core project
Co-authored-by: db2002dominic <db2002dominic@gmail.com>
This commit is contained in:
co-authored by
db2002dominic
parent
73e701895d
commit
d4c1389249
+35
-32
@@ -1,40 +1,43 @@
|
|||||||
# NAME = "Your Routstr Proxy Name"
|
# Core Configuration
|
||||||
# DESCRIPTION = "A short Description"
|
UPSTREAM_BASE_URL=https://api.openai.com/v1
|
||||||
|
UPSTREAM_API_KEY=your-upstream-api-key
|
||||||
|
ADMIN_PASSWORD=secure-admin-password
|
||||||
|
|
||||||
# Any openai-compatible api endpoint
|
# Database
|
||||||
UPSTREAM_BASE_URL="https://api.openai.com/v1"
|
DATABASE_URL=sqlite+aiosqlite:///keys.db
|
||||||
UPSTREAM_API_KEY="sk-21212121212121212121212121212121"
|
|
||||||
# UPSTREAM_PROVIDER_FEE=1 # 1 = no fees, 1.05 = 5% fees
|
|
||||||
|
|
||||||
# Lightning address used to receive funds
|
# Node Information
|
||||||
|
NAME=My Routstr Node
|
||||||
|
DESCRIPTION=Fast AI API access with Bitcoin payments
|
||||||
|
NPUB=npub1...
|
||||||
|
HTTP_URL=https://api.mynode.com
|
||||||
|
ONION_URL=http://mynode.onion
|
||||||
|
|
||||||
# When your cashu balance reaches this number of sats, send the funds to RECEIVE_LN_ADDRESS.
|
# Cashu Configuration
|
||||||
# RECEIVE_LN_ADDRESS="user@minibits.cash"
|
CASHU_MINTS=https://mint.minibits.cash/Bitcoin
|
||||||
#MINIMUM_PAYOUT = "100"
|
RECEIVE_LN_ADDRESS=
|
||||||
|
|
||||||
# If set to true, pricing is loaded from the file specified by MODELS_PATH
|
# Pricing Configuration
|
||||||
# Defaults to "models.json" and falls back to "models.example.json" if missing
|
MODEL_BASED_PRICING=true
|
||||||
# MODEL_BASED_PRICING = "true"
|
COST_PER_REQUEST=1
|
||||||
# MODELS_PATH="models.json"
|
COST_PER_1K_INPUT_TOKENS=0
|
||||||
|
COST_PER_1K_OUTPUT_TOKENS=0
|
||||||
|
EXCHANGE_FEE=1.005
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.05
|
||||||
|
|
||||||
# Costs in Sats, if MODEL_BASED_PRICING is set to false
|
# Network Configuration
|
||||||
# COST_PER_REQUEST="10"
|
CORS_ORIGINS=*
|
||||||
# COST_PER_1K_INPUT_TOKENS = "0"
|
TOR_PROXY_URL=socks5://127.0.0.1:9050
|
||||||
# COST_PER_1K_OUTPUT_TOKENS = "0"
|
|
||||||
# EXCHANGE_FEE = "1.005" # 0.5 % currency exchange fee
|
|
||||||
|
|
||||||
# password used to log into admin interface
|
# Logging
|
||||||
# ADMIN_PASSWORD=""
|
LOG_LEVEL=INFO
|
||||||
|
ENABLE_CONSOLE_LOGGING=true
|
||||||
|
|
||||||
# Public Endpoint
|
# Model Management
|
||||||
# HTTP_URL="https://your.domain.com"
|
MODELS_PATH=models.json
|
||||||
|
BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
SOURCE=
|
||||||
|
|
||||||
# Tor Endpoint (copy from docker logs)
|
# Optional Features
|
||||||
# ONION_URL=".onion"
|
PREPAID_API_KEY=
|
||||||
|
PREPAID_BALANCE=0
|
||||||
# RELAYS="wss://relay.routstr.com,wss://relay.nostr.band"
|
|
||||||
# CASHU_MINTS="https://mint.minibits.cash/Bitcoin,https://mint.cubabitcoin.org"
|
|
||||||
|
|
||||||
# Development
|
|
||||||
# DEBUG=TRUE
|
|
||||||
# LOG_LEVEL=TRACE
|
|
||||||
@@ -244,3 +244,20 @@ profile:
|
|||||||
@echo "🔥 Running with profiling..."
|
@echo "🔥 Running with profiling..."
|
||||||
$(PYTHON) -m cProfile -o profile.stats -m pytest tests/integration/test_performance_load.py::TestPerformanceBaseline -v
|
$(PYTHON) -m cProfile -o profile.stats -m pytest tests/integration/test_performance_load.py::TestPerformanceBaseline -v
|
||||||
@echo "Profile saved to profile.stats. Use '$(PYTHON) -m pstats profile.stats' to analyze."
|
@echo "Profile saved to profile.stats. Use '$(PYTHON) -m pstats profile.stats' to analyze."
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs-build:
|
||||||
|
@echo "📚 Building documentation..."
|
||||||
|
mkdocs build
|
||||||
|
|
||||||
|
docs-serve:
|
||||||
|
@echo "📚 Serving documentation at http://localhost:8001..."
|
||||||
|
mkdocs serve -a localhost:8001
|
||||||
|
|
||||||
|
docs-deploy:
|
||||||
|
@echo "📚 Deploying documentation to GitHub Pages..."
|
||||||
|
mkdocs gh-deploy --force
|
||||||
|
|
||||||
|
docs-install:
|
||||||
|
@echo "📚 Installing documentation dependencies..."
|
||||||
|
pip install -r docs/requirements.txt
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build script for MkDocs documentation
|
||||||
|
|
||||||
|
echo "Building Routstr Core documentation..."
|
||||||
|
|
||||||
|
# Check if mkdocs is installed
|
||||||
|
if ! command -v mkdocs &> /dev/null; then
|
||||||
|
echo "MkDocs not found. Installing dependencies..."
|
||||||
|
pip install -r docs/requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the documentation
|
||||||
|
echo "Building docs..."
|
||||||
|
mkdocs build
|
||||||
|
|
||||||
|
# Serve locally for preview (optional)
|
||||||
|
if [ "$1" = "serve" ]; then
|
||||||
|
echo "Starting documentation server at http://localhost:8001"
|
||||||
|
mkdocs serve -a localhost:8001
|
||||||
|
elif [ "$1" = "deploy" ]; then
|
||||||
|
echo "Deploying to GitHub Pages..."
|
||||||
|
mkdocs gh-deploy --force
|
||||||
|
else
|
||||||
|
echo "Documentation built successfully in ./site/"
|
||||||
|
echo "Run './build-docs.sh serve' to preview locally"
|
||||||
|
echo "Run './build-docs.sh deploy' to deploy to GitHub Pages"
|
||||||
|
fi
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
# Routstr Core Documentation
|
||||||
|
|
||||||
|
This directory contains the comprehensive documentation for Routstr Core, built with MkDocs.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── index.md # Home page
|
||||||
|
├── getting-started/ # Quick start guides
|
||||||
|
│ ├── overview.md # Project overview
|
||||||
|
│ ├── quickstart.md # Quick setup guide
|
||||||
|
│ ├── docker.md # Docker deployment
|
||||||
|
│ └── configuration.md # Configuration options
|
||||||
|
├── user-guide/ # User documentation
|
||||||
|
│ ├── introduction.md # User guide intro
|
||||||
|
│ ├── payment-flow.md # Payment process
|
||||||
|
│ ├── using-api.md # API usage examples
|
||||||
|
│ ├── admin-dashboard.md # Admin interface
|
||||||
|
│ └── models-pricing.md # Pricing details
|
||||||
|
├── contributing/ # Developer documentation
|
||||||
|
│ ├── setup.md # Development setup
|
||||||
|
│ ├── architecture.md # System architecture
|
||||||
|
│ ├── code-structure.md # Codebase organization
|
||||||
|
│ ├── testing.md # Testing guide
|
||||||
|
│ ├── database.md # Database design
|
||||||
|
│ └── guidelines.md # Contribution guidelines
|
||||||
|
├── api/ # API reference
|
||||||
|
│ ├── overview.md # API overview
|
||||||
|
│ ├── authentication.md # Auth details
|
||||||
|
│ ├── endpoints.md # Endpoint reference
|
||||||
|
│ └── errors.md # Error handling
|
||||||
|
└── advanced/ # Advanced topics
|
||||||
|
├── tor.md # Tor support
|
||||||
|
├── nostr.md # Nostr discovery
|
||||||
|
├── custom-pricing.md # Pricing strategies
|
||||||
|
└── migrations.md # Database migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building Documentation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Install MkDocs and dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r docs/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using the Makefile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docs-install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
Serve documentation locally with live reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdocs serve
|
||||||
|
# Or
|
||||||
|
make docs-serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit http://localhost:8001 to view the documentation.
|
||||||
|
|
||||||
|
### Building Static Site
|
||||||
|
|
||||||
|
Build the static documentation site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdocs build
|
||||||
|
# Or
|
||||||
|
make docs-build
|
||||||
|
```
|
||||||
|
|
||||||
|
The built site will be in the `site/` directory.
|
||||||
|
|
||||||
|
### Deploying to GitHub Pages
|
||||||
|
|
||||||
|
Deploy documentation to GitHub Pages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdocs gh-deploy
|
||||||
|
# Or
|
||||||
|
make docs-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Documentation
|
||||||
|
|
||||||
|
### Style Guide
|
||||||
|
|
||||||
|
- Use clear, concise language
|
||||||
|
- Include code examples
|
||||||
|
- Add diagrams where helpful
|
||||||
|
- Keep sections focused
|
||||||
|
- Test all code examples
|
||||||
|
|
||||||
|
### Markdown Extensions
|
||||||
|
|
||||||
|
Available extensions:
|
||||||
|
|
||||||
|
- **Admonition** - Notes, warnings, tips
|
||||||
|
- **Code blocks** - Syntax highlighting
|
||||||
|
- **Mermaid** - Diagrams and flowcharts
|
||||||
|
- **Tables** - Data presentation
|
||||||
|
- **Task lists** - Checklists
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Admonitions
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
!!! note
|
||||||
|
This is a note.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
This is a warning.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
This is a tip.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mermaid Diagrams
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Client] --> B[Routstr]
|
||||||
|
B --> C[Provider]
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code Blocks
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
```python
|
||||||
|
def example():
|
||||||
|
return "Hello, Routstr!"
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing to Docs
|
||||||
|
|
||||||
|
1. Edit markdown files in the appropriate directory
|
||||||
|
2. Test locally with `mkdocs serve`
|
||||||
|
3. Submit PR with changes
|
||||||
|
4. Documentation auto-deploys on merge
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [MkDocs Documentation](https://www.mkdocs.org/)
|
||||||
|
- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
|
||||||
|
- [Mermaid Diagrams](https://mermaid-js.github.io/mermaid/)
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
# Custom Pricing
|
||||||
|
|
||||||
|
This guide covers advanced pricing strategies and customization options for Routstr Core.
|
||||||
|
|
||||||
|
## Pricing Models Overview
|
||||||
|
|
||||||
|
Routstr supports three pricing models:
|
||||||
|
|
||||||
|
1. **Fixed Pricing** - Simple per-request fee
|
||||||
|
2. **Token-Based Pricing** - Charge per input/output token
|
||||||
|
3. **Model-Based Pricing** - Dynamic pricing from models.json
|
||||||
|
|
||||||
|
## Model-Based Pricing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Enable model-based pricing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MODEL_BASED_PRICING=true
|
||||||
|
MODELS_PATH=/app/config/models.json
|
||||||
|
EXCHANGE_FEE=1.005 # 0.5% exchange fee
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.05 # 5% provider margin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Models File
|
||||||
|
|
||||||
|
Create a `models.json` with your pricing:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-4",
|
||||||
|
"name": "GPT-4",
|
||||||
|
"description": "Advanced reasoning model",
|
||||||
|
"context_length": 8192,
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.03", // USD per 1K tokens
|
||||||
|
"completion": "0.06", // USD per 1K tokens
|
||||||
|
"request": "0.0001", // Fixed per-request fee
|
||||||
|
"image": "0", // For multimodal models
|
||||||
|
"web_search": "0.005", // Additional features
|
||||||
|
"internal_reasoning": "0.01"
|
||||||
|
},
|
||||||
|
"supported_features": [
|
||||||
|
"function_calling",
|
||||||
|
"vision",
|
||||||
|
"json_mode"
|
||||||
|
],
|
||||||
|
"deprecation_date": null,
|
||||||
|
"replacement_model": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom-model",
|
||||||
|
"name": "Custom Fine-tuned Model",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.001",
|
||||||
|
"completion": "0.002",
|
||||||
|
"request": "0.00005"
|
||||||
|
},
|
||||||
|
"minimum_charge": "0.0001", // Minimum charge per request
|
||||||
|
"volume_discounts": {
|
||||||
|
"1000": 0.95, // 5% off after 1K requests
|
||||||
|
"10000": 0.90, // 10% off after 10K requests
|
||||||
|
"100000": 0.85 // 15% off after 100K requests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_pricing": {
|
||||||
|
"prompt": "0.002",
|
||||||
|
"completion": "0.002",
|
||||||
|
"request": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Price Updates
|
||||||
|
|
||||||
|
Automatically fetch prices from providers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# scripts/update_prices.py
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def fetch_openrouter_models():
|
||||||
|
"""Fetch current model pricing from OpenRouter."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(
|
||||||
|
"https://openrouter.ai/api/v1/models"
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def update_models_json():
|
||||||
|
"""Update local models.json with latest prices."""
|
||||||
|
data = await fetch_openrouter_models()
|
||||||
|
|
||||||
|
models = []
|
||||||
|
for model in data['data']:
|
||||||
|
models.append({
|
||||||
|
"id": model['id'],
|
||||||
|
"name": model['name'],
|
||||||
|
"pricing": {
|
||||||
|
"prompt": model['pricing']['prompt'],
|
||||||
|
"completion": model['pricing']['completion'],
|
||||||
|
"request": model['pricing'].get('request', '0')
|
||||||
|
},
|
||||||
|
"context_length": model.get('context_length', 4096)
|
||||||
|
})
|
||||||
|
|
||||||
|
with open('models.json', 'w') as f:
|
||||||
|
json.dump({"models": models}, f, indent=2)
|
||||||
|
|
||||||
|
# Run periodically
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(update_models_json())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token-Based Pricing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Set up token-based pricing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MODEL_BASED_PRICING=false
|
||||||
|
COST_PER_REQUEST=1 # 1 sat base fee
|
||||||
|
COST_PER_1K_INPUT_TOKENS=5 # 5 sats per 1K input
|
||||||
|
COST_PER_1K_OUTPUT_TOKENS=15 # 15 sats per 1K output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Token Counting
|
||||||
|
|
||||||
|
Override default token counting:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from tiktoken import encoding_for_model
|
||||||
|
|
||||||
|
class CustomTokenCounter:
|
||||||
|
def __init__(self):
|
||||||
|
self.encodings = {}
|
||||||
|
|
||||||
|
def count_tokens(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
model: str
|
||||||
|
) -> int:
|
||||||
|
"""Custom token counting logic."""
|
||||||
|
# Cache encodings
|
||||||
|
if model not in self.encodings:
|
||||||
|
try:
|
||||||
|
self.encodings[model] = encoding_for_model(model)
|
||||||
|
except:
|
||||||
|
# Fallback encoding
|
||||||
|
self.encodings[model] = encoding_for_model("gpt-3.5-turbo")
|
||||||
|
|
||||||
|
encoding = self.encodings[model]
|
||||||
|
|
||||||
|
# Special handling for certain content
|
||||||
|
if text.startswith("```"):
|
||||||
|
# Code blocks might need special handling
|
||||||
|
tokens = encoding.encode(text)
|
||||||
|
return len(tokens) * 1.1 # 10% markup for code
|
||||||
|
|
||||||
|
return len(encoding.encode(text))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Pricing Strategies
|
||||||
|
|
||||||
|
### Time-Based Pricing
|
||||||
|
|
||||||
|
Implement peak/off-peak pricing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
class TimeBased PricingStrategy:
|
||||||
|
def __init__(self):
|
||||||
|
self.timezone = pytz.timezone('US/Eastern')
|
||||||
|
self.peak_hours = [(9, 17)] # 9 AM - 5 PM
|
||||||
|
self.peak_multiplier = 1.5
|
||||||
|
self.weekend_discount = 0.8
|
||||||
|
|
||||||
|
def get_price_multiplier(self) -> float:
|
||||||
|
"""Calculate price multiplier based on time."""
|
||||||
|
now = datetime.now(self.timezone)
|
||||||
|
|
||||||
|
# Weekend discount
|
||||||
|
if now.weekday() >= 5: # Saturday or Sunday
|
||||||
|
return self.weekend_discount
|
||||||
|
|
||||||
|
# Peak hours surcharge
|
||||||
|
hour = now.hour
|
||||||
|
for start, end in self.peak_hours:
|
||||||
|
if start <= hour < end:
|
||||||
|
return self.peak_multiplier
|
||||||
|
|
||||||
|
# Off-peak standard pricing
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def apply_to_cost(self, base_cost: int) -> int:
|
||||||
|
"""Apply time-based pricing to cost."""
|
||||||
|
multiplier = self.get_price_multiplier()
|
||||||
|
return int(base_cost * multiplier)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Discounts
|
||||||
|
|
||||||
|
Implement tiered pricing based on usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VolumeDiscountStrategy:
|
||||||
|
def __init__(self):
|
||||||
|
self.tiers = [
|
||||||
|
(0, 1.0), # 0-999: Full price
|
||||||
|
(1000, 0.95), # 1K-9.9K: 5% off
|
||||||
|
(10000, 0.90), # 10K-99.9K: 10% off
|
||||||
|
(100000, 0.85), # 100K+: 15% off
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_discount_rate(
|
||||||
|
self,
|
||||||
|
api_key_id: int,
|
||||||
|
session: AsyncSession
|
||||||
|
) -> float:
|
||||||
|
"""Get discount rate based on usage volume."""
|
||||||
|
# Count requests in current month
|
||||||
|
start_of_month = datetime.now().replace(
|
||||||
|
day=1, hour=0, minute=0, second=0
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.count(Transaction.id))
|
||||||
|
.where(Transaction.api_key_id == api_key_id)
|
||||||
|
.where(Transaction.timestamp >= start_of_month)
|
||||||
|
.where(Transaction.type == TransactionType.USAGE)
|
||||||
|
)
|
||||||
|
request_count = result.scalar()
|
||||||
|
|
||||||
|
# Find applicable tier
|
||||||
|
for threshold, rate in reversed(self.tiers):
|
||||||
|
if request_count >= threshold:
|
||||||
|
return rate
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model-Specific Surcharges
|
||||||
|
|
||||||
|
Add custom fees for specific models:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ModelSurchargeStrategy:
|
||||||
|
def __init__(self):
|
||||||
|
self.surcharges = {
|
||||||
|
"gpt-4-turbo": 1.1, # 10% premium
|
||||||
|
"claude-3-opus": 1.15, # 15% premium
|
||||||
|
"dall-e-3-hd": 1.25, # 25% premium for HD
|
||||||
|
}
|
||||||
|
|
||||||
|
self.discounts = {
|
||||||
|
"gpt-3.5-turbo": 0.95, # 5% discount
|
||||||
|
"deprecated-model": 0.8, # 20% discount
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_model_multiplier(self, model: str) -> float:
|
||||||
|
"""Get price multiplier for model."""
|
||||||
|
if model in self.surcharges:
|
||||||
|
return self.surcharges[model]
|
||||||
|
elif model in self.discounts:
|
||||||
|
return self.discounts[model]
|
||||||
|
return 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geographic Pricing
|
||||||
|
|
||||||
|
Adjust pricing based on client location:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import geoip2.database
|
||||||
|
|
||||||
|
class GeographicPricingStrategy:
|
||||||
|
def __init__(self):
|
||||||
|
self.reader = geoip2.database.Reader('GeoLite2-Country.mmdb')
|
||||||
|
self.country_multipliers = {
|
||||||
|
'US': 1.0,
|
||||||
|
'GB': 1.0,
|
||||||
|
'DE': 1.0,
|
||||||
|
'IN': 0.7, # 30% discount
|
||||||
|
'BR': 0.8, # 20% discount
|
||||||
|
'NG': 0.6, # 40% discount
|
||||||
|
}
|
||||||
|
self.default_multiplier = 0.9
|
||||||
|
|
||||||
|
def get_country_multiplier(self, ip_address: str) -> float:
|
||||||
|
"""Get price multiplier based on country."""
|
||||||
|
try:
|
||||||
|
response = self.reader.country(ip_address)
|
||||||
|
country_code = response.country.iso_code
|
||||||
|
return self.country_multipliers.get(
|
||||||
|
country_code,
|
||||||
|
self.default_multiplier
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return 1.0 # Default pricing if lookup fails
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Calculation Pipeline
|
||||||
|
|
||||||
|
### Implementing Custom Calculator
|
||||||
|
|
||||||
|
```python
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class CostCalculator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
async def calculate(
|
||||||
|
self,
|
||||||
|
request_data: dict,
|
||||||
|
usage_data: dict,
|
||||||
|
context: dict
|
||||||
|
) -> CostResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CompositeCostCalculator(CostCalculator):
|
||||||
|
"""Combine multiple pricing strategies."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.strategies = [
|
||||||
|
BaseCostCalculator(),
|
||||||
|
TimeBasedPricingStrategy(),
|
||||||
|
VolumeDiscountStrategy(),
|
||||||
|
ModelSurchargeStrategy(),
|
||||||
|
GeographicPricingStrategy()
|
||||||
|
]
|
||||||
|
|
||||||
|
async def calculate(
|
||||||
|
self,
|
||||||
|
request_data: dict,
|
||||||
|
usage_data: dict,
|
||||||
|
context: dict
|
||||||
|
) -> CostResult:
|
||||||
|
# Start with base cost
|
||||||
|
base_cost = await self.strategies[0].calculate(
|
||||||
|
request_data, usage_data, context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply each strategy
|
||||||
|
final_cost = base_cost.total_msats
|
||||||
|
breakdown = {"base": base_cost.total_msats}
|
||||||
|
|
||||||
|
for strategy in self.strategies[1:]:
|
||||||
|
multiplier = await strategy.get_multiplier(context)
|
||||||
|
adjustment = final_cost * (multiplier - 1)
|
||||||
|
final_cost += adjustment
|
||||||
|
breakdown[strategy.__class__.__name__] = adjustment
|
||||||
|
|
||||||
|
return CostResult(
|
||||||
|
total_msats=int(final_cost),
|
||||||
|
breakdown=breakdown
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with Routstr
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In routstr/payment/cost_calculation.py
|
||||||
|
async def calculate_request_cost(
|
||||||
|
model: str,
|
||||||
|
prompt_tokens: int,
|
||||||
|
completion_tokens: int,
|
||||||
|
request_type: str,
|
||||||
|
context: dict
|
||||||
|
) -> CostData:
|
||||||
|
"""Enhanced cost calculation with custom strategies."""
|
||||||
|
|
||||||
|
# Use custom calculator if configured
|
||||||
|
if os.getenv("USE_CUSTOM_PRICING", "false").lower() == "true":
|
||||||
|
calculator = CompositeCostCalculator()
|
||||||
|
result = await calculator.calculate(
|
||||||
|
request_data={
|
||||||
|
"model": model,
|
||||||
|
"type": request_type
|
||||||
|
},
|
||||||
|
usage_data={
|
||||||
|
"prompt_tokens": prompt_tokens,
|
||||||
|
"completion_tokens": completion_tokens
|
||||||
|
},
|
||||||
|
context=context
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Fall back to standard calculation
|
||||||
|
return standard_calculate_cost(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring Pricing
|
||||||
|
|
||||||
|
### Price Analytics
|
||||||
|
|
||||||
|
Track pricing effectiveness:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PricingAnalytics:
|
||||||
|
async def analyze_pricing(
|
||||||
|
self,
|
||||||
|
start_date: datetime,
|
||||||
|
end_date: datetime
|
||||||
|
):
|
||||||
|
"""Analyze pricing performance."""
|
||||||
|
# Average cost per request by model
|
||||||
|
model_costs = await self.get_average_costs_by_model(
|
||||||
|
start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revenue by pricing strategy
|
||||||
|
strategy_revenue = await self.get_revenue_by_strategy(
|
||||||
|
start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
# Price elasticity
|
||||||
|
elasticity = await self.calculate_price_elasticity()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model_costs": model_costs,
|
||||||
|
"strategy_revenue": strategy_revenue,
|
||||||
|
"price_elasticity": elasticity,
|
||||||
|
"recommendations": self.generate_recommendations(
|
||||||
|
model_costs, elasticity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### A/B Testing Prices
|
||||||
|
|
||||||
|
Test different pricing strategies:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PricingExperiment:
|
||||||
|
def __init__(self):
|
||||||
|
self.experiments = {
|
||||||
|
"exp_001": {
|
||||||
|
"name": "10% discount test",
|
||||||
|
"group_a": {"multiplier": 1.0},
|
||||||
|
"group_b": {"multiplier": 0.9},
|
||||||
|
"allocation": 0.5 # 50/50 split
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def assign_group(self, api_key_id: int) -> str:
|
||||||
|
"""Assign API key to experiment group."""
|
||||||
|
# Consistent assignment based on key ID
|
||||||
|
import hashlib
|
||||||
|
hash_value = int(hashlib.md5(
|
||||||
|
str(api_key_id).encode()
|
||||||
|
).hexdigest()[:8], 16)
|
||||||
|
|
||||||
|
return "group_b" if (hash_value % 100) < 50 else "group_a"
|
||||||
|
|
||||||
|
def get_experiment_multiplier(
|
||||||
|
self,
|
||||||
|
api_key_id: int,
|
||||||
|
experiment_id: str
|
||||||
|
) -> float:
|
||||||
|
"""Get price multiplier for experiment."""
|
||||||
|
experiment = self.experiments.get(experiment_id)
|
||||||
|
if not experiment:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
group = self.assign_group(api_key_id)
|
||||||
|
return experiment[group]["multiplier"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Enterprise Pricing
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-4-enterprise",
|
||||||
|
"name": "GPT-4 Enterprise",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.02",
|
||||||
|
"completion": "0.04"
|
||||||
|
},
|
||||||
|
"minimum_commitment": "1000", // $1000/month minimum
|
||||||
|
"sla": {
|
||||||
|
"uptime": "99.9%",
|
||||||
|
"support_response": "1 hour",
|
||||||
|
"dedicated_capacity": true
|
||||||
|
},
|
||||||
|
"volume_discounts": {
|
||||||
|
"10000": 0.95,
|
||||||
|
"100000": 0.90,
|
||||||
|
"1000000": 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enterprise_features": {
|
||||||
|
"priority_queue": true,
|
||||||
|
"custom_models": true,
|
||||||
|
"audit_logs": true,
|
||||||
|
"sso": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Budget-Friendly Options
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-3.5-turbo-budget",
|
||||||
|
"name": "GPT-3.5 Turbo Budget",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.0005",
|
||||||
|
"completion": "0.001"
|
||||||
|
},
|
||||||
|
"restrictions": {
|
||||||
|
"max_tokens_per_request": 1000,
|
||||||
|
"requests_per_minute": 10,
|
||||||
|
"peak_hours_blocked": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prepaid_packages": [
|
||||||
|
{
|
||||||
|
"name": "Starter Pack",
|
||||||
|
"price_usd": 10,
|
||||||
|
"tokens_included": 10000000,
|
||||||
|
"expires_days": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Price Calculation Issues
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Debug pricing
|
||||||
|
async def debug_price_calculation(
|
||||||
|
model: str,
|
||||||
|
tokens: dict,
|
||||||
|
api_key_id: int
|
||||||
|
):
|
||||||
|
"""Debug price calculation step by step."""
|
||||||
|
print(f"Model: {model}")
|
||||||
|
print(f"Tokens: {tokens}")
|
||||||
|
|
||||||
|
# Base price
|
||||||
|
base_price = get_model_price(model)
|
||||||
|
print(f"Base price: {base_price}")
|
||||||
|
|
||||||
|
# Token cost
|
||||||
|
token_cost = calculate_token_cost(base_price, tokens)
|
||||||
|
print(f"Token cost: {token_cost}")
|
||||||
|
|
||||||
|
# Strategies
|
||||||
|
strategies = get_active_strategies()
|
||||||
|
for strategy in strategies:
|
||||||
|
multiplier = await strategy.get_multiplier(api_key_id)
|
||||||
|
print(f"{strategy.name}: {multiplier}x")
|
||||||
|
|
||||||
|
# Final cost
|
||||||
|
final_cost = apply_all_strategies(token_cost, api_key_id)
|
||||||
|
print(f"Final cost: {final_cost} msats")
|
||||||
|
|
||||||
|
return final_cost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Prices Not Updating**
|
||||||
|
- Check `MODELS_PATH` is correct
|
||||||
|
- Verify file permissions
|
||||||
|
- Check background task logs
|
||||||
|
|
||||||
|
2. **Wrong Currency Conversion**
|
||||||
|
- Verify BTC/USD rate source
|
||||||
|
- Check `EXCHANGE_FEE` setting
|
||||||
|
- Monitor rate update frequency
|
||||||
|
|
||||||
|
3. **Discounts Not Applied**
|
||||||
|
- Verify strategy configuration
|
||||||
|
- Check API key metadata
|
||||||
|
- Review transaction history
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Transparent Pricing**
|
||||||
|
- Publish pricing clearly
|
||||||
|
- Show cost breakdowns
|
||||||
|
- Notify of price changes
|
||||||
|
|
||||||
|
2. **Fair Pricing**
|
||||||
|
- Regular competitive analysis
|
||||||
|
- Consider user feedback
|
||||||
|
- Offer budget options
|
||||||
|
|
||||||
|
3. **Performance**
|
||||||
|
- Cache price calculations
|
||||||
|
- Optimize database queries
|
||||||
|
- Monitor calculation time
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Migrations](migrations.md) - Database migration guide
|
||||||
|
- [API Endpoints](../api/endpoints.md) - Pricing endpoints
|
||||||
|
- [Monitoring](../user-guide/admin-dashboard.md) - Track pricing metrics
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
# Database Migrations
|
||||||
|
|
||||||
|
This guide covers database schema management using Alembic migrations in Routstr Core.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Routstr uses Alembic for database migrations with these features:
|
||||||
|
- **Automatic migrations** on startup
|
||||||
|
- **Version control** for schema changes
|
||||||
|
- **Rollback capability** for safety
|
||||||
|
- **Support for multiple databases** (SQLite, PostgreSQL)
|
||||||
|
|
||||||
|
## Automatic Migrations
|
||||||
|
|
||||||
|
### Startup Behavior
|
||||||
|
|
||||||
|
Migrations run automatically when Routstr starts:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In routstr/core/main.py
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
|
logger.info("Running database migrations")
|
||||||
|
run_migrations() # Automatic migration
|
||||||
|
await init_db() # Initialize connection pool
|
||||||
|
# ... rest of startup
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- ✅ Database is always up-to-date
|
||||||
|
- ✅ No manual migration steps in production
|
||||||
|
- ✅ Zero-downtime deployments
|
||||||
|
- ✅ Backwards compatibility
|
||||||
|
|
||||||
|
### Migration Safety
|
||||||
|
|
||||||
|
Migrations are designed to be safe:
|
||||||
|
- Idempotent (can run multiple times)
|
||||||
|
- Non-destructive by default
|
||||||
|
- Tested before release
|
||||||
|
- Reversible when possible
|
||||||
|
|
||||||
|
## Creating Migrations
|
||||||
|
|
||||||
|
### Auto-generating from Models
|
||||||
|
|
||||||
|
After modifying SQLModel classes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate migration from model changes
|
||||||
|
make db-migrate
|
||||||
|
|
||||||
|
# You'll be prompted for a description
|
||||||
|
Enter migration message: Add user preferences table
|
||||||
|
|
||||||
|
# Review generated file
|
||||||
|
cat migrations/versions/xxxx_add_user_preferences_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Migrations
|
||||||
|
|
||||||
|
For complex changes, create manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create empty migration
|
||||||
|
alembic revision -m "Complex data transformation"
|
||||||
|
|
||||||
|
# Edit the generated file
|
||||||
|
vim migrations/versions/xxxx_complex_data_transformation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Template
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Add user preferences table
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: f6e5d4c3b2a1
|
||||||
|
Create Date: 2024-01-15 10:30:00.123456
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
|
||||||
|
# revision identifiers
|
||||||
|
revision = 'a1b2c3d4e5f6'
|
||||||
|
down_revision = 'f6e5d4c3b2a1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Apply migration."""
|
||||||
|
# Create new table
|
||||||
|
op.create_table(
|
||||||
|
'userpreferences',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('api_key_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('theme', sa.String(), nullable=True),
|
||||||
|
sa.Column('notifications_enabled', sa.Boolean(), default=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.ForeignKeyConstraint(['api_key_id'], ['apikey.id'], )
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create index
|
||||||
|
op.create_index(
|
||||||
|
'ix_userpreferences_api_key_id',
|
||||||
|
'userpreferences',
|
||||||
|
['api_key_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Revert migration."""
|
||||||
|
op.drop_index('ix_userpreferences_api_key_id', table_name='userpreferences')
|
||||||
|
op.drop_table('userpreferences')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Migration Patterns
|
||||||
|
|
||||||
|
### Adding Columns
|
||||||
|
|
||||||
|
Add column with default value:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Add nullable column first
|
||||||
|
op.add_column(
|
||||||
|
'apikey',
|
||||||
|
sa.Column('last_rotation', sa.DateTime(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate existing rows
|
||||||
|
connection = op.get_bind()
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE apikey SET last_rotation = created_at WHERE last_rotation IS NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make non-nullable if needed
|
||||||
|
op.alter_column('apikey', 'last_rotation', nullable=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renaming Columns
|
||||||
|
|
||||||
|
Safe column rename:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# SQLite doesn't support ALTER COLUMN, so we need a workaround
|
||||||
|
with op.batch_alter_table('apikey') as batch_op:
|
||||||
|
batch_op.alter_column('old_name', new_column_name='new_name')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Indexes
|
||||||
|
|
||||||
|
Performance-improving indexes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Single column index
|
||||||
|
op.create_index(
|
||||||
|
'ix_transaction_timestamp',
|
||||||
|
'transaction',
|
||||||
|
['timestamp']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Composite index
|
||||||
|
op.create_index(
|
||||||
|
'ix_transaction_key_time',
|
||||||
|
'transaction',
|
||||||
|
['api_key_id', 'timestamp']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Partial index (PostgreSQL only)
|
||||||
|
op.create_index(
|
||||||
|
'ix_apikey_active',
|
||||||
|
'apikey',
|
||||||
|
['balance'],
|
||||||
|
postgresql_where='balance > 0'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Migrations
|
||||||
|
|
||||||
|
Transform existing data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Add new column
|
||||||
|
op.add_column(
|
||||||
|
'apikey',
|
||||||
|
sa.Column('key_type', sa.String(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migrate data
|
||||||
|
connection = op.get_bind()
|
||||||
|
result = connection.execute('SELECT id, metadata FROM apikey')
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
key_type = 'premium' if row.metadata.get('premium') else 'standard'
|
||||||
|
connection.execute(
|
||||||
|
f"UPDATE apikey SET key_type = '{key_type}' WHERE id = {row.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make column non-nullable
|
||||||
|
op.alter_column('apikey', 'key_type', nullable=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enum Types
|
||||||
|
|
||||||
|
Add enum column:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class KeyStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
SUSPENDED = "suspended"
|
||||||
|
EXPIRED = "expired"
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Create enum type (PostgreSQL)
|
||||||
|
key_status_enum = sa.Enum(KeyStatus, name='keystatus')
|
||||||
|
key_status_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
|
||||||
|
# Add column
|
||||||
|
op.add_column(
|
||||||
|
'apikey',
|
||||||
|
sa.Column(
|
||||||
|
'status',
|
||||||
|
key_status_enum,
|
||||||
|
nullable=False,
|
||||||
|
server_default='active'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database-Specific Considerations
|
||||||
|
|
||||||
|
### SQLite Limitations
|
||||||
|
|
||||||
|
SQLite has limitations requiring workarounds:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# SQLite doesn't support ALTER COLUMN directly
|
||||||
|
# Use batch_alter_table for compatibility
|
||||||
|
with op.batch_alter_table('apikey') as batch_op:
|
||||||
|
batch_op.alter_column(
|
||||||
|
'balance',
|
||||||
|
type_=sa.BigInteger(), # Change from Integer
|
||||||
|
existing_type=sa.Integer()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL Features
|
||||||
|
|
||||||
|
Leverage PostgreSQL-specific features:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Use JSONB for better performance
|
||||||
|
op.add_column(
|
||||||
|
'apikey',
|
||||||
|
sa.Column('metadata', sa.JSON().with_variant(
|
||||||
|
sa.dialects.postgresql.JSONB(), 'postgresql'
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add GIN index for JSONB queries
|
||||||
|
op.create_index(
|
||||||
|
'ix_apikey_metadata',
|
||||||
|
'apikey',
|
||||||
|
['metadata'],
|
||||||
|
postgresql_using='gin'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add check constraint
|
||||||
|
op.create_check_constraint(
|
||||||
|
'ck_apikey_balance_positive',
|
||||||
|
'apikey',
|
||||||
|
'balance >= 0'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Commands
|
||||||
|
|
||||||
|
### Running Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all pending migrations
|
||||||
|
make db-upgrade
|
||||||
|
|
||||||
|
# Upgrade to specific revision
|
||||||
|
alembic upgrade a1b2c3d4e5f6
|
||||||
|
|
||||||
|
# Upgrade one revision
|
||||||
|
alembic upgrade +1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show current revision
|
||||||
|
make db-current
|
||||||
|
# Output: a1b2c3d4e5f6 (head)
|
||||||
|
|
||||||
|
# Show migration history
|
||||||
|
make db-history
|
||||||
|
# Output:
|
||||||
|
# a1b2c3d4e5f6 -> b2c3d4e5f6a7 (head), Add user preferences
|
||||||
|
# f6e5d4c3b2a1 -> a1b2c3d4e5f6, Add indexes
|
||||||
|
# e5d4c3b2a1f6 -> f6e5d4c3b2a1, Initial schema
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rolling Back
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rollback one migration
|
||||||
|
make db-downgrade
|
||||||
|
|
||||||
|
# Rollback to specific revision
|
||||||
|
alembic downgrade f6e5d4c3b2a1
|
||||||
|
|
||||||
|
# Rollback all (dangerous!)
|
||||||
|
alembic downgrade base
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Migrations
|
||||||
|
|
||||||
|
### Unit Testing
|
||||||
|
|
||||||
|
Test migrations in isolation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from alembic import command
|
||||||
|
from alembic.config import Config
|
||||||
|
from sqlalchemy import create_engine, inspect
|
||||||
|
|
||||||
|
def test_migration_add_user_preferences():
|
||||||
|
"""Test user preferences migration."""
|
||||||
|
# Create test database
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
|
||||||
|
# Run migrations up to previous version
|
||||||
|
alembic_cfg = Config("alembic.ini")
|
||||||
|
alembic_cfg.set_main_option("sqlalchemy.url", str(engine.url))
|
||||||
|
command.upgrade(alembic_cfg, "f6e5d4c3b2a1")
|
||||||
|
|
||||||
|
# Verify state before migration
|
||||||
|
inspector = inspect(engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
assert "userpreferences" not in tables
|
||||||
|
|
||||||
|
# Run target migration
|
||||||
|
command.upgrade(alembic_cfg, "a1b2c3d4e5f6")
|
||||||
|
|
||||||
|
# Verify state after migration
|
||||||
|
inspector = inspect(engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
assert "userpreferences" in tables
|
||||||
|
|
||||||
|
# Check columns
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('userpreferences')}
|
||||||
|
assert columns == {'id', 'api_key_id', 'theme', 'notifications_enabled', 'created_at'}
|
||||||
|
|
||||||
|
# Test downgrade
|
||||||
|
command.downgrade(alembic_cfg, "f6e5d4c3b2a1")
|
||||||
|
inspector = inspect(engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
assert "userpreferences" not in tables
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Testing
|
||||||
|
|
||||||
|
Test with real data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_migration_with_data():
|
||||||
|
"""Test migration preserves existing data."""
|
||||||
|
# Setup test database with data
|
||||||
|
async with test_engine.begin() as conn:
|
||||||
|
# Insert test data
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO apikey (key_hash, balance) VALUES ('test', 1000)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
run_migrations()
|
||||||
|
|
||||||
|
# Verify data integrity
|
||||||
|
async with test_engine.connect() as conn:
|
||||||
|
result = await conn.execute("SELECT * FROM apikey WHERE key_hash = 'test'")
|
||||||
|
row = result.first()
|
||||||
|
assert row.balance == 1000
|
||||||
|
assert row.key_type == 'standard' # New column with default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Zero-Downtime Migrations
|
||||||
|
|
||||||
|
Strategy for seamless updates:
|
||||||
|
|
||||||
|
1. **Make migrations backwards compatible**
|
||||||
|
```python
|
||||||
|
# Good: Add nullable column
|
||||||
|
op.add_column('apikey', sa.Column('new_field', sa.String(), nullable=True))
|
||||||
|
|
||||||
|
# Bad: Drop column immediately
|
||||||
|
# op.drop_column('apikey', 'old_field')
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy in phases**
|
||||||
|
```bash
|
||||||
|
# Phase 1: Deploy code that works with both schemas
|
||||||
|
# Phase 2: Run migration
|
||||||
|
# Phase 3: Deploy code that requires new schema
|
||||||
|
# Phase 4: Clean up deprecated columns
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use feature flags**
|
||||||
|
```python
|
||||||
|
if feature_enabled('use_new_schema'):
|
||||||
|
# Use new column
|
||||||
|
query = select(APIKey.new_field)
|
||||||
|
else:
|
||||||
|
# Use old column
|
||||||
|
query = select(APIKey.old_field)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Monitoring
|
||||||
|
|
||||||
|
Track migration execution:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to migration
|
||||||
|
def upgrade():
|
||||||
|
start_time = time.time()
|
||||||
|
logger.info(f"Starting migration {revision}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Migration logic here
|
||||||
|
op.create_table(...)
|
||||||
|
|
||||||
|
duration = time.time() - start_time
|
||||||
|
logger.info(f"Migration {revision} completed in {duration:.2f}s")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Migration {revision} failed: {e}")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Before Migration
|
||||||
|
|
||||||
|
Always backup before major changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# backup_before_migration.sh
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
if [[ "$DATABASE_URL" == *"sqlite"* ]]; then
|
||||||
|
cp database.db "backup_$(date +%Y%m%d_%H%M%S).db"
|
||||||
|
else
|
||||||
|
pg_dump $DATABASE_URL > "backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
alembic current
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Migration Conflicts**
|
||||||
|
```bash
|
||||||
|
# Multiple heads detected
|
||||||
|
alembic heads
|
||||||
|
# a1b2c3d4e5f6 (head)
|
||||||
|
# b2c3d4e5f6a7 (head)
|
||||||
|
|
||||||
|
# Merge heads
|
||||||
|
alembic merge -m "Merge migrations" a1b2c3 b2c3d4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Failed Migration**
|
||||||
|
```python
|
||||||
|
# Add rollback logic
|
||||||
|
def upgrade():
|
||||||
|
try:
|
||||||
|
op.create_table(...)
|
||||||
|
except Exception as e:
|
||||||
|
# Clean up partial changes
|
||||||
|
op.drop_table('partial_table', checkfirst=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Ensure clean rollback
|
||||||
|
op.drop_table('new_table', checkfirst=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lock Timeout**
|
||||||
|
```python
|
||||||
|
# Add timeout handling
|
||||||
|
def upgrade():
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# Set timeout (PostgreSQL)
|
||||||
|
connection.execute("SET lock_timeout = '10s'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
op.add_column(...)
|
||||||
|
except OperationalError as e:
|
||||||
|
if 'lock timeout' in str(e):
|
||||||
|
logger.error("Migration failed due to lock timeout")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery Procedures
|
||||||
|
|
||||||
|
If migration fails in production:
|
||||||
|
|
||||||
|
1. **Check current state**
|
||||||
|
```bash
|
||||||
|
alembic current
|
||||||
|
alembic history
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Manual rollback if needed**
|
||||||
|
```sql
|
||||||
|
-- Check migration table
|
||||||
|
SELECT * FROM alembic_version;
|
||||||
|
|
||||||
|
-- Force version if necessary
|
||||||
|
UPDATE alembic_version SET version_num = 'previous_version';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Fix and retry**
|
||||||
|
```bash
|
||||||
|
# Fix migration file
|
||||||
|
vim migrations/versions/problematic_migration.py
|
||||||
|
|
||||||
|
# Retry
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Migration Guidelines
|
||||||
|
|
||||||
|
1. **Keep migrations small and focused**
|
||||||
|
- One logical change per migration
|
||||||
|
- Easier to review and rollback
|
||||||
|
|
||||||
|
2. **Test migrations thoroughly**
|
||||||
|
- Test upgrade and downgrade
|
||||||
|
- Test with production-like data
|
||||||
|
- Test database-specific features
|
||||||
|
|
||||||
|
3. **Document breaking changes**
|
||||||
|
```python
|
||||||
|
"""BREAKING: Change balance column type
|
||||||
|
|
||||||
|
This migration requires application update.
|
||||||
|
Deploy order:
|
||||||
|
1. Update application to handle both int and bigint
|
||||||
|
2. Run this migration
|
||||||
|
3. Update application to use only bigint
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Make migrations idempotent**
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Check if column exists
|
||||||
|
inspector = inspect(op.get_bind())
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('apikey')]
|
||||||
|
|
||||||
|
if 'new_column' not in columns:
|
||||||
|
op.add_column(
|
||||||
|
'apikey',
|
||||||
|
sa.Column('new_column', sa.String())
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
1. **Add indexes concurrently (PostgreSQL)**
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Create index without locking table
|
||||||
|
op.create_index(
|
||||||
|
'ix_large_table_column',
|
||||||
|
'large_table',
|
||||||
|
['column'],
|
||||||
|
postgresql_concurrently=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Batch large updates**
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# Process in batches
|
||||||
|
batch_size = 1000
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result = connection.execute(
|
||||||
|
f"UPDATE apikey SET processed = true "
|
||||||
|
f"WHERE id IN (SELECT id FROM apikey WHERE processed = false LIMIT {batch_size})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.rowcount == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
offset += batch_size
|
||||||
|
time.sleep(0.1) # Prevent overload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Database Guide](../contributing/database.md) - Database design details
|
||||||
|
- [Testing Guide](../contributing/testing.md) - Testing migrations
|
||||||
|
- [Deployment](../getting-started/docker.md) - Production deployment
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
# Nostr Discovery
|
||||||
|
|
||||||
|
Routstr Core integrates with Nostr (Notes and Other Stuff Transmitted by Relays) for decentralized provider discovery. This enables users to find Routstr nodes without relying on centralized directories.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Nostr integration provides:
|
||||||
|
- **Decentralized Discovery**: Find providers through relay network
|
||||||
|
- **Cryptographic Identity**: Providers identified by public keys
|
||||||
|
- **Real-time Updates**: Live provider status and pricing
|
||||||
|
- **Censorship Resistance**: No central point of control
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Routstr Node] --> B[Nostr Relay]
|
||||||
|
B --> C[Nostr Relay]
|
||||||
|
B --> D[Nostr Relay]
|
||||||
|
|
||||||
|
E[User Client] --> B
|
||||||
|
E --> C
|
||||||
|
E --> D
|
||||||
|
|
||||||
|
B --> F[Provider List]
|
||||||
|
C --> F
|
||||||
|
D --> F
|
||||||
|
```
|
||||||
|
|
||||||
|
Providers announce themselves by publishing signed events to Nostr relays. Clients can query these relays to discover available providers.
|
||||||
|
|
||||||
|
## Provider Configuration
|
||||||
|
|
||||||
|
### Setting Up Nostr Identity
|
||||||
|
|
||||||
|
1. **Generate Nostr Keys**
|
||||||
|
```bash
|
||||||
|
# Using nostril or similar tool
|
||||||
|
nostril --generate-keypair
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# Private key (nsec): nsec1abc...
|
||||||
|
# Public key (npub): npub1xyz...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Environment**
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
NPUB=npub1xyz... # Your public key
|
||||||
|
NSEC=nsec1abc... # Your private key (keep secret!)
|
||||||
|
NAME=Lightning AI Gateway
|
||||||
|
DESCRIPTION=Fast and reliable AI API with Bitcoin payments
|
||||||
|
HTTP_URL=https://api.lightning-ai.com
|
||||||
|
ONION_URL=http://lightningai.onion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publishing to Nostr
|
||||||
|
|
||||||
|
Routstr automatically publishes provider information to configured relays:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Published event structure (NIP-89)
|
||||||
|
{
|
||||||
|
"kind": 31990, # Application handler event
|
||||||
|
"pubkey": "your_public_key",
|
||||||
|
"content": {
|
||||||
|
"name": "Lightning AI Gateway",
|
||||||
|
"description": "Fast and reliable AI API",
|
||||||
|
"endpoints": {
|
||||||
|
"http": "https://api.lightning-ai.com",
|
||||||
|
"onion": "http://lightningai.onion"
|
||||||
|
},
|
||||||
|
"models": ["gpt-3.5-turbo", "gpt-4", "claude-3"],
|
||||||
|
"pricing": {
|
||||||
|
"gpt-3.5-turbo": {
|
||||||
|
"prompt_sats_per_1k": 3,
|
||||||
|
"completion_sats_per_1k": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cashu_mints": [
|
||||||
|
"https://mint.minibits.cash/Bitcoin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
["d", "routstr"],
|
||||||
|
["t", "ai-api"],
|
||||||
|
["t", "bitcoin"],
|
||||||
|
["p", "payment-proxy"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relay Configuration
|
||||||
|
|
||||||
|
Configure which relays to publish to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Default relays
|
||||||
|
DEFAULT_RELAYS = [
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://nostr.mom",
|
||||||
|
"wss://nos.lol"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Custom relay configuration
|
||||||
|
NOSTR_RELAYS=wss://relay1.com,wss://relay2.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Discovery
|
||||||
|
|
||||||
|
### Using the Discovery Endpoint
|
||||||
|
|
||||||
|
Find providers through the API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /v1/providers
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"name": "Lightning AI Gateway",
|
||||||
|
"npub": "npub1xyz...",
|
||||||
|
"description": "Fast and reliable AI API",
|
||||||
|
"endpoints": {
|
||||||
|
"http": "https://api.lightning-ai.com",
|
||||||
|
"onion": "http://lightningai.onion"
|
||||||
|
},
|
||||||
|
"models": ["gpt-3.5-turbo", "gpt-4"],
|
||||||
|
"pricing": {
|
||||||
|
"gpt-3.5-turbo": {
|
||||||
|
"prompt_sats_per_1k": 3,
|
||||||
|
"completion_sats_per_1k": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"last_seen": "2024-01-01T12:00:00Z",
|
||||||
|
"reliability_score": 0.99
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Nostr Queries
|
||||||
|
|
||||||
|
Query Nostr relays directly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
def discover_providers(relay_url: str):
|
||||||
|
"""Discover Routstr providers from Nostr relay."""
|
||||||
|
ws = websocket.create_connection(relay_url)
|
||||||
|
|
||||||
|
# Subscribe to provider events
|
||||||
|
subscription = {
|
||||||
|
"kinds": [31990],
|
||||||
|
"tags": {
|
||||||
|
"d": ["routstr"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(json.dumps(["REQ", "sub1", subscription]))
|
||||||
|
|
||||||
|
providers = []
|
||||||
|
while True:
|
||||||
|
response = json.loads(ws.recv())
|
||||||
|
if response[0] == "EVENT":
|
||||||
|
event = response[2]
|
||||||
|
providers.append(parse_provider_event(event))
|
||||||
|
elif response[0] == "EOSE": # End of stored events
|
||||||
|
break
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
return providers
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SimplePool } from 'nostr-tools';
|
||||||
|
|
||||||
|
async function discoverProviders(): Promise<Provider[]> {
|
||||||
|
const pool = new SimplePool();
|
||||||
|
const relays = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.nostr.band'
|
||||||
|
];
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
kinds: [31990],
|
||||||
|
'#d': ['routstr']
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = await pool.list(relays, [filter]);
|
||||||
|
|
||||||
|
return events.map(event => ({
|
||||||
|
name: event.content.name,
|
||||||
|
npub: nip19.npubEncode(event.pubkey),
|
||||||
|
url: event.content.endpoints.http,
|
||||||
|
models: event.content.models,
|
||||||
|
pricing: event.content.pricing
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provider Ranking
|
||||||
|
|
||||||
|
### Reliability Scoring
|
||||||
|
|
||||||
|
Providers are ranked based on:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ProviderScore:
|
||||||
|
def calculate(self, provider: Provider) -> float:
|
||||||
|
score = 1.0
|
||||||
|
|
||||||
|
# Uptime (based on recent checks)
|
||||||
|
uptime_ratio = provider.successful_pings / provider.total_pings
|
||||||
|
score *= uptime_ratio
|
||||||
|
|
||||||
|
# Response time
|
||||||
|
if provider.avg_response_time < 500: # ms
|
||||||
|
score *= 1.0
|
||||||
|
elif provider.avg_response_time < 1000:
|
||||||
|
score *= 0.9
|
||||||
|
else:
|
||||||
|
score *= 0.7
|
||||||
|
|
||||||
|
# Model availability
|
||||||
|
model_score = len(provider.models) / 10 # Max 10 models
|
||||||
|
score *= min(1.0, 0.5 + model_score * 0.5)
|
||||||
|
|
||||||
|
# Price competitiveness
|
||||||
|
if provider.is_cheapest_for_any_model():
|
||||||
|
score *= 1.1
|
||||||
|
|
||||||
|
return min(1.0, score)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider Selection
|
||||||
|
|
||||||
|
Choose optimal provider:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def select_provider(
|
||||||
|
providers: list[Provider],
|
||||||
|
model: str,
|
||||||
|
requirements: dict
|
||||||
|
) -> Provider:
|
||||||
|
"""Select best provider for requirements."""
|
||||||
|
|
||||||
|
# Filter by model availability
|
||||||
|
candidates = [p for p in providers if model in p.models]
|
||||||
|
|
||||||
|
# Filter by requirements
|
||||||
|
if requirements.get('tor_required'):
|
||||||
|
candidates = [p for p in candidates if p.onion_url]
|
||||||
|
|
||||||
|
if requirements.get('max_price_per_1k'):
|
||||||
|
max_price = requirements['max_price_per_1k']
|
||||||
|
candidates = [
|
||||||
|
p for p in candidates
|
||||||
|
if p.pricing[model]['prompt_sats_per_1k'] <= max_price
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort by score
|
||||||
|
candidates.sort(key=lambda p: p.reliability_score, reverse=True)
|
||||||
|
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing Updates
|
||||||
|
|
||||||
|
### Automatic Updates
|
||||||
|
|
||||||
|
Routstr publishes updates when:
|
||||||
|
- Node starts up
|
||||||
|
- Configuration changes
|
||||||
|
- Models are added/removed
|
||||||
|
- Pricing updates
|
||||||
|
|
||||||
|
### Manual Publishing
|
||||||
|
|
||||||
|
Force publish current state:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def publish_provider_info():
|
||||||
|
"""Manually publish provider information."""
|
||||||
|
event = create_provider_event(
|
||||||
|
name=os.getenv("NAME"),
|
||||||
|
description=os.getenv("DESCRIPTION"),
|
||||||
|
models=get_available_models(),
|
||||||
|
pricing=get_current_pricing()
|
||||||
|
)
|
||||||
|
|
||||||
|
await publish_to_relays(event, NOSTR_RELAYS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Lifecycle
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Publish every 6 hours
|
||||||
|
@periodic_task(hours=6)
|
||||||
|
async def update_nostr_presence():
|
||||||
|
"""Keep provider information fresh."""
|
||||||
|
try:
|
||||||
|
await publish_provider_info()
|
||||||
|
logger.info("Updated Nostr presence")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update Nostr: {e}")
|
||||||
|
|
||||||
|
# Delete on shutdown
|
||||||
|
async def remove_nostr_presence():
|
||||||
|
"""Remove provider from discovery."""
|
||||||
|
deletion_event = create_deletion_event()
|
||||||
|
await publish_to_relays(deletion_event, NOSTR_RELAYS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Key Management
|
||||||
|
|
||||||
|
1. **Secure Storage**
|
||||||
|
```python
|
||||||
|
# Never log private keys
|
||||||
|
SENSITIVE_VARS = ['NSEC', 'ADMIN_PASSWORD']
|
||||||
|
|
||||||
|
def sanitize_env(env_dict: dict) -> dict:
|
||||||
|
return {
|
||||||
|
k: '***' if k in SENSITIVE_VARS else v
|
||||||
|
for k, v in env_dict.items()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Key Rotation**
|
||||||
|
```bash
|
||||||
|
# Generate new keys
|
||||||
|
nostril --generate-keypair
|
||||||
|
|
||||||
|
# Update configuration
|
||||||
|
# Publish transition event
|
||||||
|
# Update all references
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Validation
|
||||||
|
|
||||||
|
Verify provider events:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_provider_event(event: dict) -> bool:
|
||||||
|
"""Validate provider announcement."""
|
||||||
|
# Check signature
|
||||||
|
if not verify_signature(event):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required = ['name', 'endpoints', 'models', 'pricing']
|
||||||
|
content = json.loads(event['content'])
|
||||||
|
if not all(field in content for field in required):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify endpoints are reachable
|
||||||
|
if not await check_endpoints(content['endpoints']):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relay Security
|
||||||
|
|
||||||
|
Choose relays carefully:
|
||||||
|
|
||||||
|
```python
|
||||||
|
TRUSTED_RELAYS = {
|
||||||
|
'wss://relay.damus.io': {
|
||||||
|
'operator': 'Damus',
|
||||||
|
'reputation': 'high',
|
||||||
|
'filters_spam': True
|
||||||
|
},
|
||||||
|
'wss://relay.nostr.band': {
|
||||||
|
'operator': 'Nostr.Band',
|
||||||
|
'reputation': 'high',
|
||||||
|
'paid_tier': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Multi-Relay Broadcasting
|
||||||
|
|
||||||
|
Ensure wide distribution:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def broadcast_to_relays(event: dict, relays: list[str]):
|
||||||
|
"""Broadcast event to multiple relays."""
|
||||||
|
tasks = []
|
||||||
|
for relay in relays:
|
||||||
|
task = asyncio.create_task(
|
||||||
|
publish_to_relay(event, relay)
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
successful = sum(1 for r in results if not isinstance(r, Exception))
|
||||||
|
logger.info(f"Published to {successful}/{len(relays)} relays")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider Metadata
|
||||||
|
|
||||||
|
Extended metadata in events:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 31990,
|
||||||
|
"content": {
|
||||||
|
"name": "Lightning AI",
|
||||||
|
"description": "Enterprise AI API",
|
||||||
|
"metadata": {
|
||||||
|
"established": "2024-01-01",
|
||||||
|
"total_requests": 1000000,
|
||||||
|
"average_response_ms": 250,
|
||||||
|
"supported_features": [
|
||||||
|
"streaming",
|
||||||
|
"function_calling",
|
||||||
|
"vision",
|
||||||
|
"embeddings"
|
||||||
|
],
|
||||||
|
"certifications": ["SOC2", "GDPR"],
|
||||||
|
"contact": {
|
||||||
|
"nostr": "npub1contact...",
|
||||||
|
"email": "support@lightning-ai.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discovery Filters
|
||||||
|
|
||||||
|
Advanced filtering options:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Find providers with specific features
|
||||||
|
GET /v1/providers?features=streaming,vision&max_price=5&min_reliability=0.95
|
||||||
|
|
||||||
|
# Response includes filtered results
|
||||||
|
{
|
||||||
|
"providers": [...],
|
||||||
|
"filters_applied": {
|
||||||
|
"features": ["streaming", "vision"],
|
||||||
|
"max_price_sats_per_1k": 5,
|
||||||
|
"min_reliability": 0.95
|
||||||
|
},
|
||||||
|
"total_providers": 50,
|
||||||
|
"matching_providers": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Discovery Metrics
|
||||||
|
|
||||||
|
Track discovery performance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DiscoveryMetrics:
|
||||||
|
def __init__(self):
|
||||||
|
self.relay_health = {}
|
||||||
|
self.provider_count = 0
|
||||||
|
self.query_latency = []
|
||||||
|
|
||||||
|
async def check_relay_health(self, relay_url: str):
|
||||||
|
"""Monitor relay connectivity."""
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
await connect_to_relay(relay_url)
|
||||||
|
latency = time.time() - start
|
||||||
|
self.relay_health[relay_url] = {
|
||||||
|
'status': 'healthy',
|
||||||
|
'latency_ms': latency * 1000
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self.relay_health[relay_url] = {
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider Monitoring
|
||||||
|
|
||||||
|
```python
|
||||||
|
@periodic_task(minutes=5)
|
||||||
|
async def monitor_providers():
|
||||||
|
"""Check provider health."""
|
||||||
|
providers = await discover_providers()
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
try:
|
||||||
|
# Test endpoint
|
||||||
|
response = await test_provider_endpoint(provider.http_url)
|
||||||
|
|
||||||
|
# Update metrics
|
||||||
|
await update_provider_metrics(
|
||||||
|
provider.npub,
|
||||||
|
success=response.status_code == 200,
|
||||||
|
response_time=response.elapsed
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Provider {provider.name} check failed: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### No Providers Found
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Debug discovery issues
|
||||||
|
async def debug_discovery():
|
||||||
|
"""Diagnose discovery problems."""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check relay connectivity
|
||||||
|
for relay in NOSTR_RELAYS:
|
||||||
|
if not await can_connect_to_relay(relay):
|
||||||
|
issues.append(f"Cannot connect to {relay}")
|
||||||
|
|
||||||
|
# Check event publishing
|
||||||
|
if not await verify_own_events_visible():
|
||||||
|
issues.append("Own events not visible on relays")
|
||||||
|
|
||||||
|
# Check filters
|
||||||
|
if len(await get_all_provider_events()) == 0:
|
||||||
|
issues.append("No provider events on any relay")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relay Connection Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test relay connection
|
||||||
|
wscat -c wss://relay.damus.io
|
||||||
|
|
||||||
|
# Send subscription
|
||||||
|
["REQ","test",{"kinds":[31990],"#d":["routstr"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### For Providers
|
||||||
|
|
||||||
|
1. **Consistent Identity**
|
||||||
|
- Use same npub across services
|
||||||
|
- Maintain profile metadata
|
||||||
|
- Verify identity on multiple platforms
|
||||||
|
|
||||||
|
2. **Regular Updates**
|
||||||
|
- Publish status every few hours
|
||||||
|
- Update pricing promptly
|
||||||
|
- Remove stale information
|
||||||
|
|
||||||
|
3. **Relay Diversity**
|
||||||
|
- Publish to 5+ relays
|
||||||
|
- Include regional relays
|
||||||
|
- Monitor relay health
|
||||||
|
|
||||||
|
### For Clients
|
||||||
|
|
||||||
|
1. **Verify Providers**
|
||||||
|
- Check multiple relays
|
||||||
|
- Verify endpoints work
|
||||||
|
- Monitor reliability over time
|
||||||
|
|
||||||
|
2. **Cache Discovery**
|
||||||
|
- Cache provider list
|
||||||
|
- Refresh periodically
|
||||||
|
- Handle stale data gracefully
|
||||||
|
|
||||||
|
3. **Fallback Options**
|
||||||
|
- Keep backup providers
|
||||||
|
- Handle discovery failures
|
||||||
|
- Support manual configuration
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Tor Support](tor.md) - Anonymous provider access
|
||||||
|
- [Custom Pricing](custom-pricing.md) - Dynamic pricing strategies
|
||||||
|
- [API Reference](../api/endpoints.md) - Discovery API details
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
# Tor Support
|
||||||
|
|
||||||
|
Routstr Core includes built-in support for Tor hidden services, enabling anonymous access to your API and enhanced privacy for users.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Tor support provides:
|
||||||
|
- **Anonymous Access**: Hidden service (.onion) address
|
||||||
|
- **Enhanced Privacy**: No IP address logging
|
||||||
|
- **Censorship Resistance**: Accessible from restricted networks
|
||||||
|
- **Optional Usage**: Regular HTTP/HTTPS access remains available
|
||||||
|
|
||||||
|
## Docker Setup
|
||||||
|
|
||||||
|
### Using Docker Compose
|
||||||
|
|
||||||
|
The included `compose.yml` automatically sets up Tor:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
routstr:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- TOR_PROXY_URL=socks5://tor:9050
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
|
||||||
|
tor:
|
||||||
|
image: ghcr.io/hundehausen/tor-hidden-service:latest
|
||||||
|
volumes:
|
||||||
|
- tor-data:/var/lib/tor
|
||||||
|
environment:
|
||||||
|
- HS_ROUTER=routstr:8000:80
|
||||||
|
depends_on:
|
||||||
|
- routstr
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tor-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
Start with:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Your Onion Address
|
||||||
|
|
||||||
|
After starting, retrieve your hidden service address:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View Tor logs
|
||||||
|
docker compose logs tor
|
||||||
|
|
||||||
|
# Or directly from the hostname file
|
||||||
|
docker exec tor cat /var/lib/tor/hidden_service/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
Your onion address will look like:
|
||||||
|
```
|
||||||
|
roustrjfsdgfiueghsklchg.onion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Tor Setup
|
||||||
|
|
||||||
|
### Install Tor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install tor
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
brew install tor
|
||||||
|
|
||||||
|
# Start Tor
|
||||||
|
sudo systemctl start tor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure Hidden Service
|
||||||
|
|
||||||
|
Edit `/etc/tor/torrc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hidden service configuration
|
||||||
|
HiddenServiceDir /var/lib/tor/routstr/
|
||||||
|
HiddenServicePort 80 127.0.0.1:8000
|
||||||
|
|
||||||
|
# Optional: Restrict to v3 addresses
|
||||||
|
HiddenServiceVersion 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Tor:
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart tor
|
||||||
|
```
|
||||||
|
|
||||||
|
Get onion address:
|
||||||
|
```bash
|
||||||
|
sudo cat /var/lib/tor/routstr/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Configuration
|
||||||
|
|
||||||
|
### Using Tor with Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
# Configure SOCKS proxy
|
||||||
|
proxies = {
|
||||||
|
"http://": "socks5://127.0.0.1:9050",
|
||||||
|
"https://": "socks5://127.0.0.1:9050"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create client with Tor
|
||||||
|
http_client = httpx.Client(proxies=proxies)
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="http://roustrjfsdgfiueghsklchg.onion/v1",
|
||||||
|
http_client=http_client
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use normally
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": "Hello via Tor!"}]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Tor with cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install torify
|
||||||
|
sudo apt-get install torsocks
|
||||||
|
|
||||||
|
# Make request through Tor
|
||||||
|
torify curl http://roustrjfsdgfiueghsklchg.onion/v1/models
|
||||||
|
|
||||||
|
# Or with explicit proxy
|
||||||
|
curl --socks5 127.0.0.1:9050 http://roustrjfsdgfiueghsklchg.onion/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/Node.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
// Create SOCKS agent
|
||||||
|
const agent = new SocksProxyAgent('socks5://127.0.0.1:9050');
|
||||||
|
|
||||||
|
// Configure OpenAI client
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: 'rstr_your_key',
|
||||||
|
baseURL: 'http://roustrjfsdgfiueghsklchg.onion/v1',
|
||||||
|
httpAgent: agent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use normally
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [{ role: 'user', content: 'Hello via Tor!' }]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure Tor proxy for outgoing connections:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
TOR_PROXY_URL=socks5://tor:9050 # Docker
|
||||||
|
# or
|
||||||
|
TOR_PROXY_URL=socks5://127.0.0.1:9050 # Local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publishing Onion Address
|
||||||
|
|
||||||
|
Make your onion address discoverable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
ONION_URL=http://roustrjfsdgfiueghsklchg.onion
|
||||||
|
```
|
||||||
|
|
||||||
|
This will be included in:
|
||||||
|
- `/v1/info` endpoint
|
||||||
|
- Nostr announcements
|
||||||
|
- Admin dashboard
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Hidden Service Security
|
||||||
|
|
||||||
|
1. **Keep Private Key Secure**
|
||||||
|
```bash
|
||||||
|
# Backup hidden service keys
|
||||||
|
sudo tar -czf tor-keys-backup.tar.gz /var/lib/tor/routstr/
|
||||||
|
|
||||||
|
# Restore to maintain same address
|
||||||
|
sudo tar -xzf tor-keys-backup.tar.gz -C /
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Access Control**
|
||||||
|
```bash
|
||||||
|
# Restrict to authenticated clients
|
||||||
|
HiddenServiceAuthorizeClient stealth client1,client2
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rate Limiting**
|
||||||
|
```bash
|
||||||
|
# In torrc
|
||||||
|
HiddenServiceMaxStreams 100
|
||||||
|
HiddenServiceMaxStreamsCloseCircuit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operational Security
|
||||||
|
|
||||||
|
1. **Separate Tor Instance**
|
||||||
|
```yaml
|
||||||
|
# Use dedicated Tor container
|
||||||
|
tor:
|
||||||
|
image: ghcr.io/hundehausen/tor-hidden-service:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- tor_network
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Monitor Tor Health**
|
||||||
|
```python
|
||||||
|
async def check_tor_connection():
|
||||||
|
"""Verify Tor connectivity."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
proxies={"all://": TOR_PROXY_URL}
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
"https://check.torproject.org/api/ip"
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
return data.get("IsTor", False)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Logging Considerations**
|
||||||
|
```python
|
||||||
|
# Don't log .onion addresses with IPs
|
||||||
|
def sanitize_logs(message: str) -> str:
|
||||||
|
# Remove IP addresses when .onion is present
|
||||||
|
if ".onion" in message:
|
||||||
|
message = re.sub(r'\d+\.\d+\.\d+\.\d+', '[IP]', message)
|
||||||
|
return message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Connection Pooling
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Reuse Tor circuits
|
||||||
|
class TorConnectionPool:
|
||||||
|
def __init__(self, proxy_url: str):
|
||||||
|
self.proxy_url = proxy_url
|
||||||
|
self._clients = []
|
||||||
|
|
||||||
|
async def get_client(self) -> httpx.AsyncClient:
|
||||||
|
if not self._clients:
|
||||||
|
client = httpx.AsyncClient(
|
||||||
|
proxies={"all://": self.proxy_url},
|
||||||
|
timeout=httpx.Timeout(30.0),
|
||||||
|
limits=httpx.Limits(
|
||||||
|
max_keepalive_connections=5,
|
||||||
|
max_connections=10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._clients.append(client)
|
||||||
|
return self._clients[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Circuit Management
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Rotate Tor circuits periodically
|
||||||
|
async def rotate_tor_circuit():
|
||||||
|
"""Signal Tor to create new circuit."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Tor control port (requires configuration)
|
||||||
|
response = await client.post(
|
||||||
|
"http://localhost:9051",
|
||||||
|
data="AUTHENTICATE\r\nSIGNAL NEWNYM\r\n"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Strategies
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Cache responses for Tor users
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_cached_response(
|
||||||
|
endpoint: str,
|
||||||
|
params_hash: str
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""Cache frequently accessed data."""
|
||||||
|
# Longer cache for Tor users due to latency
|
||||||
|
return cache.get(f"tor:{endpoint}:{params_hash}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Tor Metrics
|
||||||
|
|
||||||
|
Track Tor-specific metrics:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TorMetrics:
|
||||||
|
def __init__(self):
|
||||||
|
self.tor_requests = 0
|
||||||
|
self.tor_errors = 0
|
||||||
|
self.circuit_builds = 0
|
||||||
|
self.average_latency = 0
|
||||||
|
|
||||||
|
async def record_request(
|
||||||
|
self,
|
||||||
|
duration: float,
|
||||||
|
success: bool
|
||||||
|
):
|
||||||
|
self.tor_requests += 1
|
||||||
|
if not success:
|
||||||
|
self.tor_errors += 1
|
||||||
|
|
||||||
|
# Update average latency
|
||||||
|
self.average_latency = (
|
||||||
|
(self.average_latency * (self.tor_requests - 1) + duration)
|
||||||
|
/ self.tor_requests
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.get("/health/tor")
|
||||||
|
async def tor_health():
|
||||||
|
"""Check Tor service health."""
|
||||||
|
checks = {
|
||||||
|
"tor_proxy": await check_tor_proxy(),
|
||||||
|
"hidden_service": await check_hidden_service(),
|
||||||
|
"circuit_established": await check_circuit()
|
||||||
|
}
|
||||||
|
|
||||||
|
status = "healthy" if all(checks.values()) else "unhealthy"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"checks": checks,
|
||||||
|
"metrics": {
|
||||||
|
"tor_requests_total": metrics.tor_requests,
|
||||||
|
"tor_error_rate": metrics.tor_errors / max(metrics.tor_requests, 1),
|
||||||
|
"average_latency_ms": metrics.average_latency * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Hidden Service Not Accessible**
|
||||||
|
```bash
|
||||||
|
# Check Tor logs
|
||||||
|
docker compose logs tor
|
||||||
|
# or
|
||||||
|
sudo journalctl -u tor
|
||||||
|
|
||||||
|
# Verify service is running
|
||||||
|
sudo systemctl status tor
|
||||||
|
|
||||||
|
# Test locally
|
||||||
|
curl --socks5 127.0.0.1:9050 http://your-onion.onion/v1/info
|
||||||
|
```
|
||||||
|
|
||||||
|
**Slow Connection**
|
||||||
|
- Tor adds 3+ hops of latency
|
||||||
|
- Use connection pooling
|
||||||
|
- Implement aggressive caching
|
||||||
|
- Consider increasing timeouts
|
||||||
|
|
||||||
|
**Connection Errors**
|
||||||
|
```python
|
||||||
|
# Implement Tor-specific retry logic
|
||||||
|
async def tor_retry(func, max_retries=5):
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return await func()
|
||||||
|
except httpx.ProxyError:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
# Exponential backoff for circuit building
|
||||||
|
await asyncio.sleep(2 ** attempt)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
Enable Tor debug logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In torrc
|
||||||
|
Log debug file /var/log/tor/debug.log
|
||||||
|
|
||||||
|
# Monitor in real-time
|
||||||
|
tail -f /var/log/tor/debug.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### For Operators
|
||||||
|
|
||||||
|
1. **Backup Hidden Service Keys**
|
||||||
|
- Store securely offline
|
||||||
|
- Enables service recovery
|
||||||
|
- Maintains same .onion address
|
||||||
|
|
||||||
|
2. **Monitor Tor Health**
|
||||||
|
- Check circuit establishment
|
||||||
|
- Track request latency
|
||||||
|
- Alert on failures
|
||||||
|
|
||||||
|
3. **Separate Concerns**
|
||||||
|
- Run Tor in separate container
|
||||||
|
- Isolate from main application
|
||||||
|
- Use internal networks
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
|
||||||
|
1. **Verify Onion Addresses**
|
||||||
|
- Check against multiple sources
|
||||||
|
- Bookmark verified addresses
|
||||||
|
- Watch for phishing
|
||||||
|
|
||||||
|
2. **Handle Higher Latency**
|
||||||
|
- Increase client timeouts
|
||||||
|
- Implement retries
|
||||||
|
- Use connection pooling
|
||||||
|
|
||||||
|
3. **Enhance Privacy**
|
||||||
|
- Use Tor Browser for web access
|
||||||
|
- Avoid mixing Tor/clearnet
|
||||||
|
- Don't include identifying info
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Multi-Hop Onion Services
|
||||||
|
|
||||||
|
For extra security, chain multiple Tor instances:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# compose.yml
|
||||||
|
services:
|
||||||
|
tor-entry:
|
||||||
|
image: tor:latest
|
||||||
|
command: tor -f /etc/tor/torrc.entry
|
||||||
|
|
||||||
|
tor-middle:
|
||||||
|
image: tor:latest
|
||||||
|
command: tor -f /etc/tor/torrc.middle
|
||||||
|
|
||||||
|
tor-exit:
|
||||||
|
image: tor:latest
|
||||||
|
command: tor -f /etc/tor/torrc.exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Onion Service Authentication
|
||||||
|
|
||||||
|
Require client authorization:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate client auth
|
||||||
|
openssl rand -base64 32 > client_auth_key
|
||||||
|
|
||||||
|
# In torrc
|
||||||
|
HiddenServiceDir /var/lib/tor/routstr/
|
||||||
|
HiddenServicePort 80 127.0.0.1:8000
|
||||||
|
HiddenServiceAuthorizeClient stealth payments
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load Balancing
|
||||||
|
|
||||||
|
Distribute load across multiple instances:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Onion service nginx config
|
||||||
|
upstream routstr_backends {
|
||||||
|
server routstr1:8000;
|
||||||
|
server routstr2:8000;
|
||||||
|
server routstr3:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://routstr_backends;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Nostr Discovery](nostr.md) - Announce your onion service
|
||||||
|
- [Security Guide](../contributing/guidelines.md#security) - Security best practices
|
||||||
|
- [Docker Setup](../getting-started/docker.md) - Container configuration
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
Routstr uses API key authentication for all protected endpoints. This guide covers how to create, use, and manage API keys.
|
||||||
|
|
||||||
|
## API Key Creation
|
||||||
|
|
||||||
|
### From eCash Token
|
||||||
|
|
||||||
|
Create an API key by depositing an eCash token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/create
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vbWlu...",
|
||||||
|
"name": "Production Key",
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z",
|
||||||
|
"refund_npub": "npub1abcdef..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Parameters:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `cashu_token` | string | Yes | Base64-encoded Cashu token |
|
||||||
|
| `name` | string | No | Friendly name for the key |
|
||||||
|
| `expires_at` | string | No | ISO 8601 expiration timestamp |
|
||||||
|
| `refund_npub` | string | No | Nostr pubkey for refunds |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p",
|
||||||
|
"balance": 10000,
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z",
|
||||||
|
"key_id": "key_123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Lightning Invoice (Coming Soon)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/create/lightning
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"amount_sats": 10000,
|
||||||
|
"name": "Lightning Key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes Lightning invoice for payment.
|
||||||
|
|
||||||
|
## Using API Keys
|
||||||
|
|
||||||
|
### Header Authentication
|
||||||
|
|
||||||
|
Include the API key in the Authorization header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://your-node.com/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"Hello"}]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameter (Not Recommended)
|
||||||
|
|
||||||
|
For tools that don't support headers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /v1/models?api_key=rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Warning**: Query parameters may be logged. Use headers when possible.
|
||||||
|
|
||||||
|
## Key Management
|
||||||
|
|
||||||
|
### Check Balance
|
||||||
|
|
||||||
|
Get current balance and usage statistics:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /v1/wallet/balance
|
||||||
|
Authorization: Bearer rstr_your_api_key
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"balance": 8546,
|
||||||
|
"total_deposited": 10000,
|
||||||
|
"total_spent": 1454,
|
||||||
|
"last_used": "2024-01-01T12:34:56Z",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"expires_at": null,
|
||||||
|
"key_info": {
|
||||||
|
"name": "Production Key",
|
||||||
|
"key_id": "key_123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top Up Balance
|
||||||
|
|
||||||
|
Add funds to existing key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/topup
|
||||||
|
Authorization: Bearer rstr_your_api_key
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"old_balance": 8546,
|
||||||
|
"added_amount": 5000,
|
||||||
|
"new_balance": 13546,
|
||||||
|
"transaction_id": "txn_789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Transactions
|
||||||
|
|
||||||
|
View transaction history:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /v1/wallet/transactions?limit=10
|
||||||
|
Authorization: Bearer rstr_your_api_key
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"id": "txn_123",
|
||||||
|
"type": "usage",
|
||||||
|
"amount": -154,
|
||||||
|
"balance_after": 8546,
|
||||||
|
"description": "gpt-3.5-turbo: 50 prompt + 150 completion tokens",
|
||||||
|
"timestamp": "2024-01-01T12:34:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "txn_122",
|
||||||
|
"type": "deposit",
|
||||||
|
"amount": 10000,
|
||||||
|
"balance_after": 10000,
|
||||||
|
"description": "Initial deposit",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### API Key Storage
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Store keys in environment variables
|
||||||
|
- Use secret management systems
|
||||||
|
- Encrypt keys at rest
|
||||||
|
- Implement key rotation
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
- Commit keys to version control
|
||||||
|
- Share keys between environments
|
||||||
|
- Log keys in plain text
|
||||||
|
- Expose keys in client-side code
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env file
|
||||||
|
ROUTSTR_API_KEY=rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p
|
||||||
|
ROUTSTR_BASE_URL=https://your-node.com/v1
|
||||||
|
|
||||||
|
# Usage in code
|
||||||
|
import os
|
||||||
|
api_key = os.getenv("ROUTSTR_API_KEY")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rotation
|
||||||
|
|
||||||
|
Regularly rotate API keys:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Create new key
|
||||||
|
new_key = create_api_key(balance=old_key_balance)
|
||||||
|
|
||||||
|
# 2. Update applications
|
||||||
|
update_environment_variable("ROUTSTR_API_KEY", new_key)
|
||||||
|
|
||||||
|
# 3. Test new key
|
||||||
|
test_api_connection(new_key)
|
||||||
|
|
||||||
|
# 4. Withdraw old key balance
|
||||||
|
withdraw_balance(old_key)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Errors
|
||||||
|
|
||||||
|
### Invalid API Key
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_failed",
|
||||||
|
"message": "Invalid API key",
|
||||||
|
"code": "invalid_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Code:** 401
|
||||||
|
|
||||||
|
**Common Causes:**
|
||||||
|
- Typo in API key
|
||||||
|
- Key doesn't exist
|
||||||
|
- Key has been deleted
|
||||||
|
|
||||||
|
### Expired API Key
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_failed",
|
||||||
|
"message": "API key has expired",
|
||||||
|
"code": "key_expired",
|
||||||
|
"details": {
|
||||||
|
"expired_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Code:** 401
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
- Create a new API key
|
||||||
|
- Contact admin if refund address was set
|
||||||
|
|
||||||
|
### Insufficient Balance
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "insufficient_balance",
|
||||||
|
"message": "Insufficient balance for request",
|
||||||
|
"code": "payment_required",
|
||||||
|
"details": {
|
||||||
|
"balance": 100,
|
||||||
|
"required": 154,
|
||||||
|
"shortfall": 54
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Code:** 402
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
- Top up the API key balance
|
||||||
|
- Use a more economical model
|
||||||
|
- Optimize request parameters
|
||||||
|
|
||||||
|
## Advanced Authentication
|
||||||
|
|
||||||
|
### Per-Request Tokens (Coming Soon)
|
||||||
|
|
||||||
|
Pay per request without maintaining a balance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://your-node.com/v1/chat/completions \
|
||||||
|
-H "X-Cashu: cashuAeyJ0b2tlbiI6W3..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"gpt-3.5-turbo","messages":[...]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes change:
|
||||||
|
```
|
||||||
|
X-Cashu: cashuAeyJjaGFuZ2UiOlt7...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Key Authentication
|
||||||
|
|
||||||
|
Use multiple keys for different purposes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Production key for main app
|
||||||
|
PROD_KEY = os.getenv("ROUTSTR_PROD_KEY")
|
||||||
|
|
||||||
|
# Development key for testing
|
||||||
|
DEV_KEY = os.getenv("ROUTSTR_DEV_KEY")
|
||||||
|
|
||||||
|
# Analytics key with restricted permissions
|
||||||
|
ANALYTICS_KEY = os.getenv("ROUTSTR_ANALYTICS_KEY")
|
||||||
|
|
||||||
|
# Choose key based on environment
|
||||||
|
api_key = PROD_KEY if is_production() else DEV_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delegated Authentication
|
||||||
|
|
||||||
|
Create sub-keys with limited permissions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/create/subkey
|
||||||
|
Authorization: Bearer rstr_parent_key
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Limited Subkey",
|
||||||
|
"balance_limit": 1000,
|
||||||
|
"allowed_models": ["gpt-3.5-turbo"],
|
||||||
|
"expires_in_hours": 24
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
Rate limits are applied per API key:
|
||||||
|
|
||||||
|
### Default Limits
|
||||||
|
|
||||||
|
| Metric | Limit | Window |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| Requests | 1000 | 1 minute |
|
||||||
|
| Tokens | 1,000,000 | 1 hour |
|
||||||
|
| Concurrent | 10 | - |
|
||||||
|
|
||||||
|
### Rate Limit Headers
|
||||||
|
|
||||||
|
```
|
||||||
|
X-RateLimit-Limit: 1000
|
||||||
|
X-RateLimit-Remaining: 999
|
||||||
|
X-RateLimit-Reset: 1640995200
|
||||||
|
X-RateLimit-Type: requests_per_minute
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Rate Limits
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def make_request_with_retry(
|
||||||
|
client,
|
||||||
|
max_retries: int = 3
|
||||||
|
) -> Optional[Response]:
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(...)
|
||||||
|
return response
|
||||||
|
except RateLimitError as e:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
# Extract retry-after from error
|
||||||
|
retry_after = e.retry_after or 60
|
||||||
|
print(f"Rate limited. Waiting {retry_after}s...")
|
||||||
|
time.sleep(retry_after)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
## IP Whitelisting
|
||||||
|
|
||||||
|
Restrict API key usage by IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/update
|
||||||
|
Authorization: Bearer rstr_your_api_key
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"allowed_ips": [
|
||||||
|
"192.168.1.100",
|
||||||
|
"10.0.0.0/24"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Usage Alerts
|
||||||
|
|
||||||
|
Set up usage notifications:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/alerts
|
||||||
|
Authorization: Bearer rstr_your_api_key
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"low_balance_threshold": 1000,
|
||||||
|
"daily_spend_limit": 5000,
|
||||||
|
"webhook_url": "https://your-app.com/webhook"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit Logging
|
||||||
|
|
||||||
|
All API key usage is logged:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2024-01-01T12:34:56Z",
|
||||||
|
"api_key_id": "key_123456",
|
||||||
|
"endpoint": "/v1/chat/completions",
|
||||||
|
"method": "POST",
|
||||||
|
"ip_address": "192.168.1.100",
|
||||||
|
"user_agent": "OpenAI-Python/1.0",
|
||||||
|
"cost_sats": 154,
|
||||||
|
"response_status": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Endpoints](endpoints.md) - Complete endpoint reference
|
||||||
|
- [Errors](errors.md) - Error handling guide
|
||||||
|
- [Using the API](../user-guide/using-api.md) - Integration examples
|
||||||
@@ -0,0 +1,622 @@
|
|||||||
|
# API Endpoints
|
||||||
|
|
||||||
|
Complete reference for all available endpoints in Routstr Core.
|
||||||
|
|
||||||
|
## Chat Completions
|
||||||
|
|
||||||
|
### Create Chat Completion
|
||||||
|
|
||||||
|
Generate a model response for a conversation.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a helpful assistant."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Hello!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 150,
|
||||||
|
"stream": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `model` | string | Yes | - | Model ID to use |
|
||||||
|
| `messages` | array | Yes | - | Conversation messages |
|
||||||
|
| `temperature` | number | No | 1.0 | Sampling temperature (0-2) |
|
||||||
|
| `max_tokens` | integer | No | Unlimited | Maximum tokens to generate |
|
||||||
|
| `stream` | boolean | No | false | Stream response |
|
||||||
|
| `top_p` | number | No | 1.0 | Nucleus sampling |
|
||||||
|
| `n` | integer | No | 1 | Number of completions |
|
||||||
|
| `stop` | string/array | No | null | Stop sequences |
|
||||||
|
| `presence_penalty` | number | No | 0 | Presence penalty (-2 to 2) |
|
||||||
|
| `frequency_penalty` | number | No | 0 | Frequency penalty (-2 to 2) |
|
||||||
|
| `logit_bias` | object | No | null | Token bias |
|
||||||
|
| `user` | string | No | null | End-user identifier |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "chatcmpl-123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1677652288,
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"choices": [{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hello! How can I assist you today?"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 9,
|
||||||
|
"completion_tokens": 10,
|
||||||
|
"total_tokens": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Response
|
||||||
|
|
||||||
|
When `stream: true`:
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Completions
|
||||||
|
|
||||||
|
### Create Completion
|
||||||
|
|
||||||
|
Generate text completion (legacy endpoint).
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "gpt-3.5-turbo-instruct",
|
||||||
|
"prompt": "Once upon a time",
|
||||||
|
"max_tokens": 50,
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cmpl-123",
|
||||||
|
"object": "text_completion",
|
||||||
|
"created": 1677652288,
|
||||||
|
"model": "gpt-3.5-turbo-instruct",
|
||||||
|
"choices": [{
|
||||||
|
"text": " in a faraway land, there lived a brave knight...",
|
||||||
|
"index": 0,
|
||||||
|
"logprobs": null,
|
||||||
|
"finish_reason": "length"
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 4,
|
||||||
|
"completion_tokens": 50,
|
||||||
|
"total_tokens": 54
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Embeddings
|
||||||
|
|
||||||
|
### Create Embeddings
|
||||||
|
|
||||||
|
Generate vector representations of text.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/embeddings
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "text-embedding-3-small",
|
||||||
|
"input": "The quick brown fox jumps over the lazy dog",
|
||||||
|
"encoding_format": "float"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `model` | string | Yes | - | Embedding model ID |
|
||||||
|
| `input` | string/array | Yes | - | Text(s) to embed |
|
||||||
|
| `encoding_format` | string | No | "float" | Format: "float" or "base64" |
|
||||||
|
| `dimensions` | integer | No | Model default | Output dimensions |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [{
|
||||||
|
"object": "embedding",
|
||||||
|
"index": 0,
|
||||||
|
"embedding": [0.0023064255, -0.009327292, ...]
|
||||||
|
}],
|
||||||
|
"model": "text-embedding-3-small",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 9,
|
||||||
|
"total_tokens": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
### Create Image
|
||||||
|
|
||||||
|
Generate images from text prompts.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/images/generations
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "dall-e-3",
|
||||||
|
"prompt": "A white siamese cat wearing a space helmet",
|
||||||
|
"n": 1,
|
||||||
|
"size": "1024x1024",
|
||||||
|
"quality": "standard"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `model` | string | Yes | - | Model: dall-e-2, dall-e-3 |
|
||||||
|
| `prompt` | string | Yes | - | Text description |
|
||||||
|
| `n` | integer | No | 1 | Number of images |
|
||||||
|
| `size` | string | No | "1024x1024" | Image dimensions |
|
||||||
|
| `quality` | string | No | "standard" | Quality: standard, hd |
|
||||||
|
| `style` | string | No | "vivid" | Style: vivid, natural |
|
||||||
|
| `response_format` | string | No | "url" | Format: url, b64_json |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created": 1677652288,
|
||||||
|
"data": [{
|
||||||
|
"url": "https://generated-image-url.com/image.png",
|
||||||
|
"revised_prompt": "A white Siamese cat wearing a detailed space helmet..."
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
### Create Transcription
|
||||||
|
|
||||||
|
Convert audio to text.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/audio/transcriptions
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Form Data:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `file` | file | Yes | Audio file (mp3, mp4, mpeg, mpga, m4a, wav, webm) |
|
||||||
|
| `model` | string | Yes | Model ID (whisper-1) |
|
||||||
|
| `language` | string | No | Language code (ISO-639-1) |
|
||||||
|
| `prompt` | string | No | Context prompt |
|
||||||
|
| `response_format` | string | No | Format: json, text, srt, verbose_json, vtt |
|
||||||
|
| `temperature` | number | No | Sampling temperature |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Hello, this is the transcribed audio content."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Translation
|
||||||
|
|
||||||
|
Translate audio to English.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/audio/translations
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Same parameters as transcription, but always translates to English.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
### List Models
|
||||||
|
|
||||||
|
Get available models and pricing.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "gpt-3.5-turbo",
|
||||||
|
"object": "model",
|
||||||
|
"created": 1677652288,
|
||||||
|
"owned_by": "openai",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": 0.0015,
|
||||||
|
"completion": 0.002,
|
||||||
|
"prompt_sats_per_1k": 3,
|
||||||
|
"completion_sats_per_1k": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-4",
|
||||||
|
"object": "model",
|
||||||
|
"created": 1677652288,
|
||||||
|
"owned_by": "openai",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": 0.03,
|
||||||
|
"completion": 0.06,
|
||||||
|
"prompt_sats_per_1k": 60,
|
||||||
|
"completion_sats_per_1k": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Model
|
||||||
|
|
||||||
|
Get details for a specific model.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/models/{model_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "gpt-3.5-turbo",
|
||||||
|
"object": "model",
|
||||||
|
"created": 1677652288,
|
||||||
|
"owned_by": "openai",
|
||||||
|
"permission": [{
|
||||||
|
"allow_create_engine": false,
|
||||||
|
"allow_sampling": true,
|
||||||
|
"allow_logprobs": true,
|
||||||
|
"allow_search_indices": false,
|
||||||
|
"allow_view": true,
|
||||||
|
"allow_fine_tuning": false
|
||||||
|
}],
|
||||||
|
"root": "gpt-3.5-turbo",
|
||||||
|
"parent": null,
|
||||||
|
"pricing": {
|
||||||
|
"prompt": 0.0015,
|
||||||
|
"completion": 0.002,
|
||||||
|
"image": 0,
|
||||||
|
"request": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wallet Management
|
||||||
|
|
||||||
|
### Create API Key
|
||||||
|
|
||||||
|
Create a new API key with eCash deposit.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/wallet/create
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3...",
|
||||||
|
"name": "My API Key",
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z",
|
||||||
|
"refund_npub": "npub1..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p",
|
||||||
|
"balance": 10000,
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Balance
|
||||||
|
|
||||||
|
Get current balance and usage stats.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/wallet/balance
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"balance": 8546,
|
||||||
|
"total_deposited": 10000,
|
||||||
|
"total_spent": 1454,
|
||||||
|
"last_used": "2024-01-01T12:34:56Z",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top Up Balance
|
||||||
|
|
||||||
|
Add funds to existing API key.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/wallet/topup
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"old_balance": 8546,
|
||||||
|
"added_amount": 5000,
|
||||||
|
"new_balance": 13546
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Withdraw Balance
|
||||||
|
|
||||||
|
Generate eCash token from balance.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/wallet/withdraw
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"amount": 5000,
|
||||||
|
"mint_url": "https://mint.minibits.cash/Bitcoin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3...",
|
||||||
|
"amount": 5000,
|
||||||
|
"mint_url": "https://mint.minibits.cash/Bitcoin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Information
|
||||||
|
|
||||||
|
### Get Node Info
|
||||||
|
|
||||||
|
Get public information about the Routstr node.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/info
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Lightning AI Gateway",
|
||||||
|
"description": "Fast AI API access with Bitcoin payments",
|
||||||
|
"version": "0.1.1b",
|
||||||
|
"npub": "npub1abc...",
|
||||||
|
"mints": [
|
||||||
|
"https://mint.minibits.cash/Bitcoin",
|
||||||
|
"https://testnut.cashu.space"
|
||||||
|
],
|
||||||
|
"http_url": "https://api.lightning-ai.com",
|
||||||
|
"onion_url": "http://lightningai.onion",
|
||||||
|
"models": {
|
||||||
|
"gpt-3.5-turbo": {
|
||||||
|
"name": "GPT-3.5 Turbo",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": 0.0015,
|
||||||
|
"completion": 0.002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
### List Providers
|
||||||
|
|
||||||
|
Discover Routstr providers from Nostr relays.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/providers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `relay` | string | Specific relay URL |
|
||||||
|
| `limit` | integer | Maximum results |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"name": "Fast AI Node",
|
||||||
|
"npub": "npub1xyz...",
|
||||||
|
"url": "https://fast-ai.com",
|
||||||
|
"description": "Low latency AI API access",
|
||||||
|
"models": ["gpt-3.5-turbo", "gpt-4"],
|
||||||
|
"pricing": {
|
||||||
|
"gpt-3.5-turbo": {
|
||||||
|
"prompt_sats_per_1k": 3,
|
||||||
|
"completion_sats_per_1k": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin Endpoints
|
||||||
|
|
||||||
|
### Admin Dashboard
|
||||||
|
|
||||||
|
Access the web-based admin interface.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires password authentication via web form.
|
||||||
|
|
||||||
|
### Admin API
|
||||||
|
|
||||||
|
Protected endpoints for node management.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/api/withdraw
|
||||||
|
X-Admin-Password: {admin_password}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "rstr_123...",
|
||||||
|
"amount": 5000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health & Status
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
Monitor service health.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "0.1.1b",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z",
|
||||||
|
"checks": {
|
||||||
|
"database": "ok",
|
||||||
|
"upstream": "ok",
|
||||||
|
"mint": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
Get service metrics.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns Prometheus-compatible metrics.
|
||||||
|
|
||||||
|
## Deprecated Endpoints
|
||||||
|
|
||||||
|
### Legacy Balance Check
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/balance
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Deprecated**: Use `/v1/wallet/balance` instead.
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
All endpoints are subject to rate limiting:
|
||||||
|
|
||||||
|
| Endpoint Type | Limit | Window |
|
||||||
|
|---------------|-------|--------|
|
||||||
|
| AI Generation | 100/min | 1 minute |
|
||||||
|
| Wallet Operations | 10/min | 1 minute |
|
||||||
|
| Info/Discovery | 60/min | 1 minute |
|
||||||
|
|
||||||
|
Rate limit information is included in response headers.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Errors](errors.md) - Error handling reference
|
||||||
|
- [Authentication](authentication.md) - Auth details
|
||||||
|
- [Examples](../user-guide/using-api.md) - Code examples
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
# Error Handling
|
||||||
|
|
||||||
|
This guide covers error responses, codes, and handling strategies for the Routstr API.
|
||||||
|
|
||||||
|
## Error Response Format
|
||||||
|
|
||||||
|
All errors follow a consistent JSON structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "error_type",
|
||||||
|
"message": "Human-readable error message",
|
||||||
|
"code": "error_code",
|
||||||
|
"details": {
|
||||||
|
"additional": "context-specific information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Status Codes
|
||||||
|
|
||||||
|
| Status | Meaning | Common Causes |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| 400 | Bad Request | Invalid parameters, malformed JSON |
|
||||||
|
| 401 | Unauthorized | Invalid or missing API key |
|
||||||
|
| 402 | Payment Required | Insufficient balance |
|
||||||
|
| 403 | Forbidden | Access denied to resource |
|
||||||
|
| 404 | Not Found | Endpoint or resource doesn't exist |
|
||||||
|
| 422 | Unprocessable Entity | Validation errors |
|
||||||
|
| 429 | Too Many Requests | Rate limit exceeded |
|
||||||
|
| 500 | Internal Server Error | Server-side error |
|
||||||
|
| 502 | Bad Gateway | Upstream API error |
|
||||||
|
| 503 | Service Unavailable | Temporary outage |
|
||||||
|
|
||||||
|
## Error Types
|
||||||
|
|
||||||
|
### Authentication Errors
|
||||||
|
|
||||||
|
#### Invalid API Key
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_failed",
|
||||||
|
"message": "Invalid API key provided",
|
||||||
|
"code": "invalid_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 401
|
||||||
|
**Resolution:** Check API key format and validity
|
||||||
|
|
||||||
|
#### Expired API Key
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_failed",
|
||||||
|
"message": "API key has expired",
|
||||||
|
"code": "key_expired",
|
||||||
|
"details": {
|
||||||
|
"expired_at": "2024-01-01T00:00:00Z",
|
||||||
|
"refund_available": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 401
|
||||||
|
**Resolution:** Create new API key or contact admin for refund
|
||||||
|
|
||||||
|
#### Missing Authorization
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "authentication_failed",
|
||||||
|
"message": "Authorization header required",
|
||||||
|
"code": "missing_auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 401
|
||||||
|
**Resolution:** Include `Authorization: Bearer {api_key}` header
|
||||||
|
|
||||||
|
### Payment Errors
|
||||||
|
|
||||||
|
#### Insufficient Balance
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "insufficient_balance",
|
||||||
|
"message": "Insufficient balance for request",
|
||||||
|
"code": "payment_required",
|
||||||
|
"details": {
|
||||||
|
"balance": 100,
|
||||||
|
"required": 154,
|
||||||
|
"shortfall": 54,
|
||||||
|
"estimated_tokens": {
|
||||||
|
"prompt": 50,
|
||||||
|
"completion": 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 402
|
||||||
|
**Resolution:** Top up API key balance
|
||||||
|
|
||||||
|
#### Invalid Token
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "payment_error",
|
||||||
|
"message": "Invalid Cashu token",
|
||||||
|
"code": "invalid_token",
|
||||||
|
"details": {
|
||||||
|
"reason": "Token already spent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 400
|
||||||
|
**Resolution:** Use a valid, unspent token
|
||||||
|
|
||||||
|
#### Mint Unavailable
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "payment_error",
|
||||||
|
"message": "Cannot connect to Cashu mint",
|
||||||
|
"code": "mint_unavailable",
|
||||||
|
"details": {
|
||||||
|
"mint_url": "https://mint.example.com",
|
||||||
|
"retry_after": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 503
|
||||||
|
**Resolution:** Try again later or use different mint
|
||||||
|
|
||||||
|
### Validation Errors
|
||||||
|
|
||||||
|
#### Invalid Parameters
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "invalid_request",
|
||||||
|
"message": "Invalid request parameters",
|
||||||
|
"code": "validation_error",
|
||||||
|
"details": {
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"field": "temperature",
|
||||||
|
"message": "Must be between 0 and 2",
|
||||||
|
"value": 3.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "model",
|
||||||
|
"message": "Model 'gpt-5' not found",
|
||||||
|
"value": "gpt-5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 422
|
||||||
|
**Resolution:** Fix parameter values
|
||||||
|
|
||||||
|
#### Missing Required Fields
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "invalid_request",
|
||||||
|
"message": "Missing required fields",
|
||||||
|
"code": "missing_fields",
|
||||||
|
"details": {
|
||||||
|
"missing": ["model", "messages"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 400
|
||||||
|
**Resolution:** Include all required fields
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
#### Rate Limit Exceeded
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "rate_limit_exceeded",
|
||||||
|
"message": "Too many requests",
|
||||||
|
"code": "rate_limit",
|
||||||
|
"details": {
|
||||||
|
"limit": 100,
|
||||||
|
"window": "1 minute",
|
||||||
|
"retry_after": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 429
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
X-RateLimit-Limit: 100
|
||||||
|
X-RateLimit-Remaining: 0
|
||||||
|
X-RateLimit-Reset: 1640995200
|
||||||
|
Retry-After: 45
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution:** Wait for retry_after seconds
|
||||||
|
|
||||||
|
### Upstream Errors
|
||||||
|
|
||||||
|
#### Model Overloaded
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "upstream_error",
|
||||||
|
"message": "Model is currently overloaded",
|
||||||
|
"code": "model_overloaded",
|
||||||
|
"details": {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"retry_after": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 503
|
||||||
|
**Resolution:** Retry request after delay
|
||||||
|
|
||||||
|
#### Upstream Timeout
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "upstream_error",
|
||||||
|
"message": "Request to upstream API timed out",
|
||||||
|
"code": "upstream_timeout",
|
||||||
|
"details": {
|
||||||
|
"timeout": 30,
|
||||||
|
"endpoint": "chat/completions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 504
|
||||||
|
**Resolution:** Retry with shorter prompt or max_tokens
|
||||||
|
|
||||||
|
### Content Policy
|
||||||
|
|
||||||
|
#### Content Filtered
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "content_policy_violation",
|
||||||
|
"message": "Content filtered due to policy violation",
|
||||||
|
"code": "content_filtered",
|
||||||
|
"details": {
|
||||||
|
"reason": "harmful_content",
|
||||||
|
"categories": ["violence", "hate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** 400
|
||||||
|
**Resolution:** Modify prompt to comply with policies
|
||||||
|
|
||||||
|
## Error Handling Best Practices
|
||||||
|
|
||||||
|
### Retry Logic
|
||||||
|
|
||||||
|
Implement exponential backoff with jitter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
def retry_with_backoff(
|
||||||
|
func: Callable,
|
||||||
|
max_retries: int = 3,
|
||||||
|
base_delay: float = 1.0,
|
||||||
|
max_delay: float = 60.0
|
||||||
|
) -> Optional[Any]:
|
||||||
|
"""Retry function with exponential backoff."""
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Check if error is retryable
|
||||||
|
if hasattr(e, 'status_code'):
|
||||||
|
if e.status_code in [429, 502, 503, 504]:
|
||||||
|
# Calculate delay with jitter
|
||||||
|
delay = min(
|
||||||
|
base_delay * (2 ** attempt) + random.uniform(0, 1),
|
||||||
|
max_delay
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use retry_after if provided
|
||||||
|
if hasattr(e, 'retry_after'):
|
||||||
|
delay = e.retry_after
|
||||||
|
|
||||||
|
time.sleep(delay)
|
||||||
|
else:
|
||||||
|
# Non-retryable error
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Categories
|
||||||
|
|
||||||
|
Group errors for handling:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ErrorHandler:
|
||||||
|
# Errors that should be retried
|
||||||
|
RETRYABLE_ERRORS = {
|
||||||
|
'rate_limit',
|
||||||
|
'upstream_timeout',
|
||||||
|
'model_overloaded',
|
||||||
|
'mint_unavailable'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors requiring user action
|
||||||
|
USER_ACTION_ERRORS = {
|
||||||
|
'insufficient_balance',
|
||||||
|
'invalid_api_key',
|
||||||
|
'key_expired'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors requiring code changes
|
||||||
|
CLIENT_ERRORS = {
|
||||||
|
'validation_error',
|
||||||
|
'missing_fields',
|
||||||
|
'invalid_request'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_error(cls, error_response: dict) -> None:
|
||||||
|
error_code = error_response['error']['code']
|
||||||
|
|
||||||
|
if error_code in cls.RETRYABLE_ERRORS:
|
||||||
|
# Implement retry logic
|
||||||
|
pass
|
||||||
|
elif error_code in cls.USER_ACTION_ERRORS:
|
||||||
|
# Alert user
|
||||||
|
pass
|
||||||
|
elif error_code in cls.CLIENT_ERRORS:
|
||||||
|
# Log for debugging
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Graceful Degradation
|
||||||
|
|
||||||
|
Handle errors without breaking application flow:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def get_ai_response(prompt: str) -> str:
|
||||||
|
"""Get AI response with fallback handling."""
|
||||||
|
try:
|
||||||
|
# Try primary model
|
||||||
|
response = await client.chat.completions.create(
|
||||||
|
model="gpt-4",
|
||||||
|
messages=[{"role": "user", "content": prompt}]
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
except InsufficientBalanceError:
|
||||||
|
# Fall back to cheaper model
|
||||||
|
try:
|
||||||
|
response = await client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=100 # Limit tokens
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fallback failed: {e}")
|
||||||
|
return "Service temporarily unavailable"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
return "An error occurred processing your request"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging Errors
|
||||||
|
|
||||||
|
Structure error logs for debugging:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
def log_api_error(error_response: dict, context: dict) -> None:
|
||||||
|
"""Log API errors with context."""
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
error_data = {
|
||||||
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
|
'error': error_response['error'],
|
||||||
|
'context': {
|
||||||
|
'endpoint': context.get('endpoint'),
|
||||||
|
'api_key_id': context.get('api_key_id'),
|
||||||
|
'request_id': context.get('request_id'),
|
||||||
|
'model': context.get('model')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"API Error",
|
||||||
|
extra={'structured_data': json.dumps(error_data)}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### User-Friendly Messages
|
||||||
|
|
||||||
|
Map technical errors to user messages:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ERROR_MESSAGES = {
|
||||||
|
'insufficient_balance': "Your account balance is too low. Please add funds to continue.",
|
||||||
|
'invalid_api_key': "Invalid API key. Please check your configuration.",
|
||||||
|
'rate_limit': "Too many requests. Please wait a moment and try again.",
|
||||||
|
'model_overloaded': "The AI service is busy. Please try again in a few seconds.",
|
||||||
|
'validation_error': "Invalid request. Please check your input and try again."
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_message(error_code: str) -> str:
|
||||||
|
"""Get user-friendly error message."""
|
||||||
|
return ERROR_MESSAGES.get(
|
||||||
|
error_code,
|
||||||
|
"An unexpected error occurred. Please try again later."
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Scenarios
|
||||||
|
|
||||||
|
### Handling Balance Errors
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def make_request_with_balance_check():
|
||||||
|
try:
|
||||||
|
# Check balance first
|
||||||
|
balance_info = await client.get("/v1/wallet/balance")
|
||||||
|
|
||||||
|
# Estimate cost
|
||||||
|
estimated_cost = calculate_cost(model, prompt_length)
|
||||||
|
|
||||||
|
if balance_info['balance'] < estimated_cost * 1.1: # 10% buffer
|
||||||
|
# Proactively top up
|
||||||
|
await top_up_balance()
|
||||||
|
|
||||||
|
# Make request
|
||||||
|
return await client.chat.completions.create(...)
|
||||||
|
|
||||||
|
except InsufficientBalanceError as e:
|
||||||
|
# Handle insufficient balance
|
||||||
|
shortfall = e.details['shortfall']
|
||||||
|
await top_up_balance(amount=shortfall * 2)
|
||||||
|
# Retry request
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Rate Limits
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class RateLimitTracker:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset_times = {}
|
||||||
|
|
||||||
|
def is_limited(self, endpoint: str) -> bool:
|
||||||
|
reset_time = self.reset_times.get(endpoint)
|
||||||
|
if reset_time and datetime.now() < reset_time:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_limit(self, endpoint: str, reset_timestamp: int):
|
||||||
|
self.reset_times[endpoint] = datetime.fromtimestamp(reset_timestamp)
|
||||||
|
|
||||||
|
def wait_time(self, endpoint: str) -> float:
|
||||||
|
reset_time = self.reset_times.get(endpoint)
|
||||||
|
if reset_time:
|
||||||
|
return max(0, (reset_time - datetime.now()).total_seconds())
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Error Handling
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
async def test_insufficient_balance_handling():
|
||||||
|
# Mock API client
|
||||||
|
mock_client = Mock()
|
||||||
|
mock_client.chat.completions.create.side_effect = InsufficientBalanceError(
|
||||||
|
required=100,
|
||||||
|
available=50
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test error handling
|
||||||
|
handler = ErrorHandler(mock_client)
|
||||||
|
result = await handler.safe_request(
|
||||||
|
model="gpt-4",
|
||||||
|
messages=[{"role": "user", "content": "test"}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify fallback behavior
|
||||||
|
assert result.fallback_used is True
|
||||||
|
assert result.model == "gpt-3.5-turbo"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_real_error_scenarios():
|
||||||
|
# Test with invalid API key
|
||||||
|
invalid_client = OpenAI(
|
||||||
|
api_key="rstr_invalid",
|
||||||
|
base_url=test_url
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AuthenticationError) as exc_info:
|
||||||
|
await invalid_client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": "test"}]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 401
|
||||||
|
assert "invalid_api_key" in str(exc_info.value)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring Errors
|
||||||
|
|
||||||
|
Track error rates and patterns:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ErrorMetrics:
|
||||||
|
def __init__(self):
|
||||||
|
self.error_counts = defaultdict(int)
|
||||||
|
self.error_timestamps = defaultdict(list)
|
||||||
|
|
||||||
|
def record_error(self, error_code: str):
|
||||||
|
self.error_counts[error_code] += 1
|
||||||
|
self.error_timestamps[error_code].append(datetime.now())
|
||||||
|
|
||||||
|
def get_error_rate(self, error_code: str, window_minutes: int = 60) -> float:
|
||||||
|
cutoff = datetime.now() - timedelta(minutes=window_minutes)
|
||||||
|
recent_errors = [
|
||||||
|
ts for ts in self.error_timestamps[error_code]
|
||||||
|
if ts > cutoff
|
||||||
|
]
|
||||||
|
return len(recent_errors) / window_minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Authentication](authentication.md) - Auth error details
|
||||||
|
- [Endpoints](endpoints.md) - Endpoint-specific errors
|
||||||
|
- [Examples](../user-guide/using-api.md) - Error handling examples
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# API Reference Overview
|
||||||
|
|
||||||
|
Routstr Core provides a complete OpenAI-compatible API with additional endpoints for payment management. This reference covers all available endpoints, authentication methods, and response formats.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://your-routstr-node.com/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
All API endpoints are prefixed with `/v1` for versioning.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Routstr uses API keys for authentication. Include your key in the Authorization header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Authorization: Bearer rstr_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Key Format
|
||||||
|
|
||||||
|
- Prefix: `rstr_`
|
||||||
|
- Length: 32 characters
|
||||||
|
- Example: `rstr_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p`
|
||||||
|
|
||||||
|
## Content Types
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
- **Required**: `Content-Type: application/json`
|
||||||
|
- **Encoding**: UTF-8
|
||||||
|
- **Maximum Size**: 10MB (configurable)
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
- **Type**: `application/json` or `text/event-stream` (for streaming)
|
||||||
|
- **Encoding**: UTF-8
|
||||||
|
- **Compression**: gzip (if accepted)
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
Rate limits are applied per API key:
|
||||||
|
|
||||||
|
- **Requests**: 1000 per minute
|
||||||
|
- **Tokens**: 1,000,000 per hour
|
||||||
|
- **Concurrent**: 10 simultaneous requests
|
||||||
|
|
||||||
|
Rate limit headers:
|
||||||
|
```
|
||||||
|
X-RateLimit-Limit: 1000
|
||||||
|
X-RateLimit-Remaining: 999
|
||||||
|
X-RateLimit-Reset: 1640995200
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
All errors follow a consistent format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "insufficient_balance",
|
||||||
|
"message": "Insufficient balance for request",
|
||||||
|
"code": "payment_required",
|
||||||
|
"details": {
|
||||||
|
"required": 154,
|
||||||
|
"available": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
| Type | Status Code | Description |
|
||||||
|
|------|-------------|-------------|
|
||||||
|
| `invalid_request` | 400 | Malformed request |
|
||||||
|
| `authentication_failed` | 401 | Invalid or missing API key |
|
||||||
|
| `insufficient_balance` | 402 | Not enough balance |
|
||||||
|
| `forbidden` | 403 | Access denied |
|
||||||
|
| `not_found` | 404 | Resource not found |
|
||||||
|
| `rate_limit_exceeded` | 429 | Too many requests |
|
||||||
|
| `internal_error` | 500 | Server error |
|
||||||
|
| `upstream_error` | 502 | Upstream API error |
|
||||||
|
|
||||||
|
## Endpoint Categories
|
||||||
|
|
||||||
|
### AI/ML Endpoints
|
||||||
|
|
||||||
|
Standard OpenAI-compatible endpoints:
|
||||||
|
|
||||||
|
- **Chat Completions**: `/v1/chat/completions`
|
||||||
|
- **Completions**: `/v1/completions`
|
||||||
|
- **Embeddings**: `/v1/embeddings`
|
||||||
|
- **Images**: `/v1/images/generations`
|
||||||
|
- **Audio**: `/v1/audio/transcriptions`
|
||||||
|
- **Models**: `/v1/models`
|
||||||
|
|
||||||
|
### Payment Endpoints
|
||||||
|
|
||||||
|
Routstr-specific payment management:
|
||||||
|
|
||||||
|
- **Wallet**: `/v1/wallet/*`
|
||||||
|
- **Balance**: `/v1/balance`
|
||||||
|
- **Node Info**: `/v1/info`
|
||||||
|
|
||||||
|
### Admin Endpoints
|
||||||
|
|
||||||
|
Protected administrative functions:
|
||||||
|
|
||||||
|
- **Dashboard**: `/admin/`
|
||||||
|
- **API Management**: `/admin/api/*`
|
||||||
|
|
||||||
|
## Request Headers
|
||||||
|
|
||||||
|
### Standard Headers
|
||||||
|
|
||||||
|
| Header | Required | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `Authorization` | Yes | Bearer token with API key |
|
||||||
|
| `Content-Type` | Yes | Must be `application/json` |
|
||||||
|
| `Accept` | No | Response format preference |
|
||||||
|
| `Accept-Encoding` | No | Compression support |
|
||||||
|
| `X-Request-ID` | No | Client-provided request ID |
|
||||||
|
|
||||||
|
### Custom Headers
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `X-Routstr-Version` | API version override |
|
||||||
|
| `X-Cashu` | eCash token for per-request payment |
|
||||||
|
| `X-Max-Cost` | Maximum acceptable cost in sats |
|
||||||
|
|
||||||
|
## Response Headers
|
||||||
|
|
||||||
|
### Standard Headers
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `Content-Type` | Response format |
|
||||||
|
| `Content-Length` | Response size |
|
||||||
|
| `X-Routstr-Request-ID` | Unique request identifier |
|
||||||
|
| `X-Routstr-Version` | API version used |
|
||||||
|
|
||||||
|
### Cost Headers
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `X-Routstr-Cost` | Request cost in sats |
|
||||||
|
| `X-Routstr-Balance` | Remaining balance |
|
||||||
|
| `X-Cashu` | Change token (if applicable) |
|
||||||
|
|
||||||
|
## Streaming Responses
|
||||||
|
|
||||||
|
For endpoints supporting streaming, responses use Server-Sent Events:
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"content":" there"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenAPI Specification
|
||||||
|
|
||||||
|
The complete OpenAPI 3.0 specification is available at:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Interactive documentation:
|
||||||
|
```
|
||||||
|
GET /docs # Swagger UI
|
||||||
|
GET /redoc # ReDoc
|
||||||
|
```
|
||||||
|
|
||||||
|
## SDK Support
|
||||||
|
|
||||||
|
Routstr is compatible with official OpenAI SDKs:
|
||||||
|
|
||||||
|
### Python
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="https://your-node.com/v1"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
```javascript
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: 'rstr_your_key',
|
||||||
|
baseURL: 'https://your-node.com/v1'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
```bash
|
||||||
|
curl https://your-node.com/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer rstr_your_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"Hello"}]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook Support
|
||||||
|
|
||||||
|
Configure webhooks for events:
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /v1/webhooks
|
||||||
|
{
|
||||||
|
"url": "https://your-app.com/webhook",
|
||||||
|
"events": ["balance.low", "key.expired"],
|
||||||
|
"secret": "whsec_your_secret"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Events are sent with signature verification:
|
||||||
|
```
|
||||||
|
X-Webhook-Signature: sha256=...
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Versioning
|
||||||
|
|
||||||
|
- Current version: `v1`
|
||||||
|
- Version in URL path: `/v1/endpoint`
|
||||||
|
- Override with header: `X-Routstr-Version: v2`
|
||||||
|
- Deprecation notices: 6 months
|
||||||
|
|
||||||
|
## Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 200 | Success |
|
||||||
|
| 201 | Created |
|
||||||
|
| 204 | No content |
|
||||||
|
| 400 | Bad request |
|
||||||
|
| 401 | Unauthorized |
|
||||||
|
| 402 | Payment required |
|
||||||
|
| 403 | Forbidden |
|
||||||
|
| 404 | Not found |
|
||||||
|
| 429 | Rate limited |
|
||||||
|
| 500 | Server error |
|
||||||
|
| 502 | Upstream error |
|
||||||
|
| 503 | Service unavailable |
|
||||||
|
|
||||||
|
## CORS Support
|
||||||
|
|
||||||
|
CORS is enabled with configurable origins:
|
||||||
|
|
||||||
|
```
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||||
|
Access-Control-Allow-Headers: Authorization, Content-Type
|
||||||
|
Access-Control-Max-Age: 86400
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compression
|
||||||
|
|
||||||
|
Responses are compressed with gzip when:
|
||||||
|
- Client sends `Accept-Encoding: gzip`
|
||||||
|
- Response is larger than 1KB
|
||||||
|
- Content type is compressible
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
List endpoints support pagination:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /v1/transactions?limit=50&offset=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes pagination metadata:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [...],
|
||||||
|
"has_more": true,
|
||||||
|
"total": 500,
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Filtering
|
||||||
|
|
||||||
|
Select specific fields in responses:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /v1/models?fields=id,name,pricing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Requests
|
||||||
|
|
||||||
|
Process multiple operations in one request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /v1/batch
|
||||||
|
{
|
||||||
|
"requests": [
|
||||||
|
{"method": "POST", "endpoint": "/chat/completions", "body": {...}},
|
||||||
|
{"method": "GET", "endpoint": "/models"},
|
||||||
|
{"method": "GET", "endpoint": "/balance"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotency
|
||||||
|
|
||||||
|
Prevent duplicate operations:
|
||||||
|
|
||||||
|
```
|
||||||
|
Idempotency-Key: unique-request-id
|
||||||
|
```
|
||||||
|
|
||||||
|
Keys are stored for 24 hours.
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
Monitor service status:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "0.1.1b",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z",
|
||||||
|
"checks": {
|
||||||
|
"database": "ok",
|
||||||
|
"upstream": "ok",
|
||||||
|
"mint": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Authentication](authentication.md) - Detailed auth guide
|
||||||
|
- [Endpoints](endpoints.md) - Complete endpoint reference
|
||||||
|
- [Errors](errors.md) - Error handling guide
|
||||||
|
- [Examples](../user-guide/using-api.md) - Code examples
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
This document describes the high-level architecture of Routstr Core, helping contributors understand how the system works.
|
||||||
|
|
||||||
|
## System Overview
|
||||||
|
|
||||||
|
Routstr Core is a FastAPI-based reverse proxy that adds Bitcoin micropayments to OpenAI-compatible APIs.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "External Services"
|
||||||
|
Client[API Client]
|
||||||
|
Mint[Cashu Mint]
|
||||||
|
Provider[AI Provider]
|
||||||
|
Nostr[Nostr Relays]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Routstr Core"
|
||||||
|
API[FastAPI Server]
|
||||||
|
Auth[Auth Module]
|
||||||
|
Payment[Payment Module]
|
||||||
|
Proxy[Proxy Module]
|
||||||
|
DB[(SQLite DB)]
|
||||||
|
|
||||||
|
API --> Auth
|
||||||
|
Auth --> Payment
|
||||||
|
Auth --> DB
|
||||||
|
Payment --> Proxy
|
||||||
|
Proxy --> Provider
|
||||||
|
Payment --> Mint
|
||||||
|
API --> Nostr
|
||||||
|
end
|
||||||
|
|
||||||
|
Client --> API
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### FastAPI Application
|
||||||
|
|
||||||
|
The main application is initialized in `routstr/core/main.py`:
|
||||||
|
|
||||||
|
- **Lifespan Management**: Handles startup/shutdown tasks
|
||||||
|
- **Middleware**: CORS, logging, error handling
|
||||||
|
- **Routers**: Modular endpoint organization
|
||||||
|
- **Background Tasks**: Price updates, automatic payouts
|
||||||
|
|
||||||
|
### Authentication System
|
||||||
|
|
||||||
|
Located in `routstr/auth.py`, handles:
|
||||||
|
|
||||||
|
- **API Key Validation**: Hashed key storage and lookup
|
||||||
|
- **Balance Checking**: Ensures sufficient funds
|
||||||
|
- **Rate Limiting**: Optional request throttling
|
||||||
|
- **Token Redemption**: Converts eCash to balance
|
||||||
|
|
||||||
|
### Payment Processing
|
||||||
|
|
||||||
|
The `routstr/payment/` module manages:
|
||||||
|
|
||||||
|
- **Cost Calculation**: Token-based or fixed pricing
|
||||||
|
- **Model Pricing**: Dynamic pricing from models.json
|
||||||
|
- **Currency Conversion**: BTC/USD rate management
|
||||||
|
- **Fee Application**: Exchange and provider fees
|
||||||
|
|
||||||
|
### Request Proxying
|
||||||
|
|
||||||
|
`routstr/proxy.py` handles:
|
||||||
|
|
||||||
|
- **Request Forwarding**: Preserves headers and body
|
||||||
|
- **Response Streaming**: Efficient memory usage
|
||||||
|
- **Usage Tracking**: Counts tokens and costs
|
||||||
|
- **Error Handling**: Graceful upstream failures
|
||||||
|
|
||||||
|
### Database Layer
|
||||||
|
|
||||||
|
Using SQLModel in `routstr/core/db.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Core models
|
||||||
|
APIKey:
|
||||||
|
- id: Primary key
|
||||||
|
- key_hash: Hashed API key
|
||||||
|
- balance: Current balance (msats)
|
||||||
|
- created_at: Timestamp
|
||||||
|
- metadata: JSON field
|
||||||
|
|
||||||
|
Transaction:
|
||||||
|
- id: Primary key
|
||||||
|
- api_key_id: Foreign key
|
||||||
|
- amount: Transaction amount
|
||||||
|
- type: deposit/usage/withdrawal
|
||||||
|
- timestamp: When occurred
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request Flow
|
||||||
|
|
||||||
|
### Standard API Request
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant R as Routstr
|
||||||
|
participant D as Database
|
||||||
|
participant P as AI Provider
|
||||||
|
|
||||||
|
C->>R: API Request + Key
|
||||||
|
R->>D: Validate Key
|
||||||
|
D-->>R: Key Info + Balance
|
||||||
|
R->>R: Check Balance
|
||||||
|
R->>P: Forward Request
|
||||||
|
P-->>R: AI Response
|
||||||
|
R->>D: Deduct Cost
|
||||||
|
R-->>C: Return Response
|
||||||
|
```
|
||||||
|
|
||||||
|
### Payment Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant R as Routstr
|
||||||
|
participant W as Wallet Module
|
||||||
|
participant M as Cashu Mint
|
||||||
|
participant D as Database
|
||||||
|
|
||||||
|
C->>R: Create Key Request + Token
|
||||||
|
R->>W: Validate Token
|
||||||
|
W->>M: Verify with Mint
|
||||||
|
M-->>W: Token Valid
|
||||||
|
W-->>R: Token Amount
|
||||||
|
R->>D: Create Key + Balance
|
||||||
|
R-->>C: Return API Key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
### 1. Async Architecture
|
||||||
|
|
||||||
|
Everything is async for maximum performance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def handle_request(request: Request) -> Response:
|
||||||
|
# Non-blocking database queries
|
||||||
|
api_key = await get_api_key(request.headers["Authorization"])
|
||||||
|
|
||||||
|
# Concurrent operations
|
||||||
|
balance_check, rate_limit = await asyncio.gather(
|
||||||
|
check_balance(api_key),
|
||||||
|
check_rate_limit(api_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stream response without blocking
|
||||||
|
async for chunk in proxy_request(request):
|
||||||
|
yield chunk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Modular Design
|
||||||
|
|
||||||
|
Components are loosely coupled:
|
||||||
|
|
||||||
|
- **Routers**: Separate files for different endpoints
|
||||||
|
- **Dependencies**: Injected via FastAPI's DI system
|
||||||
|
- **Models**: Shared data structures
|
||||||
|
- **Services**: Business logic separated from routes
|
||||||
|
|
||||||
|
### 3. Error Handling
|
||||||
|
|
||||||
|
Graceful degradation and clear error messages:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RoustrError(Exception):
|
||||||
|
"""Base exception with structured error response"""
|
||||||
|
status_code: int = 500
|
||||||
|
error_type: str = "internal_error"
|
||||||
|
|
||||||
|
class InsufficientBalanceError(RoustrError):
|
||||||
|
status_code = 402
|
||||||
|
error_type = "insufficient_balance"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Database Migrations
|
||||||
|
|
||||||
|
Using Alembic for schema management:
|
||||||
|
|
||||||
|
- Auto-migrations on startup
|
||||||
|
- Version control for schema changes
|
||||||
|
- Rollback capability
|
||||||
|
- Zero-downtime updates
|
||||||
|
|
||||||
|
## Security Architecture
|
||||||
|
|
||||||
|
### API Key Security
|
||||||
|
|
||||||
|
- **Storage**: SHA-256 hashed keys
|
||||||
|
- **Generation**: Cryptographically secure random
|
||||||
|
- **Validation**: Constant-time comparison
|
||||||
|
- **Rotation**: Support for key expiry
|
||||||
|
|
||||||
|
### Payment Security
|
||||||
|
|
||||||
|
- **Token Validation**: Cryptographic verification
|
||||||
|
- **Double-Spend Prevention**: Mint verification
|
||||||
|
- **Balance Protection**: Atomic transactions
|
||||||
|
- **Audit Trail**: All transactions logged
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- **HTTPS**: Enforced in production
|
||||||
|
- **CORS**: Configurable origins
|
||||||
|
- **Rate Limiting**: Per-key limits
|
||||||
|
- **Input Validation**: Pydantic models
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Model pricing cache
|
||||||
|
@lru_cache(maxsize=100)
|
||||||
|
def get_model_price(model_id: str) -> ModelPrice:
|
||||||
|
return MODELS.get(model_id)
|
||||||
|
|
||||||
|
# Balance cache with TTL
|
||||||
|
balance_cache = TTLCache(maxsize=1000, ttl=60)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Optimization
|
||||||
|
|
||||||
|
- **Connection Pooling**: Reuse connections
|
||||||
|
- **Indexed Queries**: Key lookups are O(1)
|
||||||
|
- **Batch Operations**: Group updates
|
||||||
|
- **Async I/O**: Non-blocking queries
|
||||||
|
|
||||||
|
### Streaming Responses
|
||||||
|
|
||||||
|
Efficient memory usage for large responses:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(upstream_response):
|
||||||
|
async for chunk in upstream_response.aiter_bytes():
|
||||||
|
# Process chunk without loading full response
|
||||||
|
yield process_chunk(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extension Points
|
||||||
|
|
||||||
|
### Adding New Endpoints
|
||||||
|
|
||||||
|
1. Create router module
|
||||||
|
2. Define Pydantic models
|
||||||
|
3. Implement business logic
|
||||||
|
4. Register with main app
|
||||||
|
5. Add tests
|
||||||
|
|
||||||
|
### Custom Pricing Models
|
||||||
|
|
||||||
|
1. Extend `ModelPrice` class
|
||||||
|
2. Implement calculation logic
|
||||||
|
3. Add to pricing registry
|
||||||
|
4. Update configuration
|
||||||
|
|
||||||
|
### Payment Methods
|
||||||
|
|
||||||
|
1. Create payment handler
|
||||||
|
2. Implement validation
|
||||||
|
3. Add to payment router
|
||||||
|
4. Update balance logic
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- Mock external dependencies
|
||||||
|
- Test business logic in isolation
|
||||||
|
- Fast execution (< 1 second per test)
|
||||||
|
- High coverage target (> 80%)
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- Test component interactions
|
||||||
|
- Use test database
|
||||||
|
- Mock external services
|
||||||
|
- Verify end-to-end flows
|
||||||
|
|
||||||
|
### Performance Tests
|
||||||
|
|
||||||
|
- Load testing with locust
|
||||||
|
- Memory profiling
|
||||||
|
- Database query optimization
|
||||||
|
- Response time benchmarks
|
||||||
|
|
||||||
|
## Monitoring and Observability
|
||||||
|
|
||||||
|
### Structured Logging
|
||||||
|
|
||||||
|
```python
|
||||||
|
logger.info("api_request", extra={
|
||||||
|
"request_id": request_id,
|
||||||
|
"api_key": api_key_id,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"model": model,
|
||||||
|
"tokens": token_count,
|
||||||
|
"cost_sats": cost,
|
||||||
|
"duration_ms": duration
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics Collection
|
||||||
|
|
||||||
|
Key metrics tracked:
|
||||||
|
- Request rate by endpoint
|
||||||
|
- Token usage by model
|
||||||
|
- Balance changes
|
||||||
|
- Error rates
|
||||||
|
- Response times
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"version": __version__,
|
||||||
|
"database": await check_db(),
|
||||||
|
"upstream": await check_upstream(),
|
||||||
|
"mints": await check_mints()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Architecture
|
||||||
|
|
||||||
|
### Container Structure
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Multi-stage build
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
# Install dependencies
|
||||||
|
|
||||||
|
FROM python:3.11-slim
|
||||||
|
# Copy only runtime needs
|
||||||
|
# Run as non-root user
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
- **Development**: Local SQLite, debug logging
|
||||||
|
- **Testing**: In-memory database, mock services
|
||||||
|
- **Production**: Persistent storage, structured logs
|
||||||
|
|
||||||
|
### Scaling Considerations
|
||||||
|
|
||||||
|
- **Horizontal**: Multiple instances behind load balancer
|
||||||
|
- **Vertical**: Async handles high concurrency
|
||||||
|
- **Database**: Consider PostgreSQL for scale
|
||||||
|
- **Caching**: Redis for distributed cache
|
||||||
|
|
||||||
|
## Future Architecture
|
||||||
|
|
||||||
|
### Planned Improvements
|
||||||
|
|
||||||
|
1. **WebSocket Support**: Real-time balance updates
|
||||||
|
2. **Plugin System**: Extensible pricing/auth
|
||||||
|
3. **Multi-Region**: Geographic distribution
|
||||||
|
4. **Event Sourcing**: Complete audit trail
|
||||||
|
|
||||||
|
### Technical Debt
|
||||||
|
|
||||||
|
Areas for improvement:
|
||||||
|
- Database query optimization
|
||||||
|
- Response caching layer
|
||||||
|
- Metric aggregation
|
||||||
|
- API versioning strategy
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Review [Code Structure](code-structure.md) for detailed organization
|
||||||
|
- See [Testing Guide](testing.md) for test architecture
|
||||||
|
- Check [Database Guide](database.md) for schema details
|
||||||
|
- Read [Guidelines](guidelines.md) for coding standards
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
# Code Structure
|
||||||
|
|
||||||
|
This guide provides a detailed overview of Routstr Core's codebase organization and key modules.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
routstr-core/
|
||||||
|
├── routstr/ # Main application package
|
||||||
|
│ ├── __init__.py # Package initialization, loads .env
|
||||||
|
│ ├── auth.py # Authentication and authorization
|
||||||
|
│ ├── balance.py # Balance management endpoints
|
||||||
|
│ ├── discovery.py # Nostr relay discovery
|
||||||
|
│ ├── proxy.py # Request proxying logic
|
||||||
|
│ ├── wallet.py # Cashu wallet operations
|
||||||
|
│ │
|
||||||
|
│ ├── core/ # Core infrastructure
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── admin.py # Admin dashboard and API
|
||||||
|
│ │ ├── db.py # Database models and connection
|
||||||
|
│ │ ├── exceptions.py # Custom exception classes
|
||||||
|
│ │ ├── logging.py # Structured logging setup
|
||||||
|
│ │ ├── main.py # FastAPI app initialization
|
||||||
|
│ │ └── middleware.py # HTTP middleware components
|
||||||
|
│ │
|
||||||
|
│ └── payment/ # Payment processing
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── cost_calculation.py # Usage cost calculation
|
||||||
|
│ ├── helpers.py # Payment utilities
|
||||||
|
│ ├── lnurl.py # Lightning URL support
|
||||||
|
│ ├── models.py # Model pricing management
|
||||||
|
│ ├── price.py # BTC/USD price handling
|
||||||
|
│ └── x_cashu.py # Cashu header protocol
|
||||||
|
│
|
||||||
|
├── tests/ # Test suite
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── conftest.py # Pytest configuration
|
||||||
|
│ ├── unit/ # Unit tests
|
||||||
|
│ └── integration/ # Integration tests
|
||||||
|
│
|
||||||
|
├── migrations/ # Alembic database migrations
|
||||||
|
│ ├── alembic.ini
|
||||||
|
│ ├── env.py
|
||||||
|
│ ├── script.py.mako
|
||||||
|
│ └── versions/ # Migration files
|
||||||
|
│
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
│ └── models_meta.py # Fetch model pricing
|
||||||
|
│
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── logs/ # Application logs (git ignored)
|
||||||
|
│
|
||||||
|
├── .github/ # GitHub Actions workflows
|
||||||
|
├── .env.example # Environment variable template
|
||||||
|
├── .gitignore # Git ignore rules
|
||||||
|
├── .dockerignore # Docker ignore rules
|
||||||
|
├── Dockerfile # Container definition
|
||||||
|
├── Makefile # Development commands
|
||||||
|
├── README.md # Project overview
|
||||||
|
├── alembic.ini # Migration configuration
|
||||||
|
├── compose.yml # Docker Compose setup
|
||||||
|
├── compose.testing.yml # Testing environment
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
└── uv.lock # Locked dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Modules
|
||||||
|
|
||||||
|
### Application Entry Point
|
||||||
|
|
||||||
|
#### `routstr/__init__.py`
|
||||||
|
```python
|
||||||
|
# Loads environment variables
|
||||||
|
import dotenv
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
# Exports FastAPI app
|
||||||
|
from .core.main import app as fastapi_app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routstr/core/main.py`
|
||||||
|
```python
|
||||||
|
# FastAPI application setup
|
||||||
|
app = FastAPI(
|
||||||
|
title="Routstr Node",
|
||||||
|
lifespan=lifespan, # Manages startup/shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
# Middleware registration
|
||||||
|
app.add_middleware(CORSMiddleware, ...)
|
||||||
|
app.add_middleware(LoggingMiddleware)
|
||||||
|
|
||||||
|
# Router inclusion
|
||||||
|
app.include_router(admin_router)
|
||||||
|
app.include_router(balance_router)
|
||||||
|
app.include_router(proxy_router)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Module
|
||||||
|
|
||||||
|
#### `routstr/auth.py`
|
||||||
|
|
||||||
|
Handles API key validation and authorization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class APIKeyAuth:
|
||||||
|
"""FastAPI dependency for API key authentication"""
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> APIKey:
|
||||||
|
# Extract and validate API key
|
||||||
|
# Check balance
|
||||||
|
# Return authenticated key object
|
||||||
|
|
||||||
|
# Usage in routes:
|
||||||
|
@router.get("/protected")
|
||||||
|
async def protected_route(api_key: APIKey = Depends(APIKeyAuth())):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Key functions:
|
||||||
|
- `create_api_key()` - Generate new API keys
|
||||||
|
- `validate_api_key()` - Verify and retrieve key
|
||||||
|
- `check_balance()` - Ensure sufficient funds
|
||||||
|
- `update_last_used()` - Track usage
|
||||||
|
|
||||||
|
### Payment Processing
|
||||||
|
|
||||||
|
#### `routstr/payment/cost_calculation.py`
|
||||||
|
|
||||||
|
Calculates request costs:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_request_cost(
|
||||||
|
model: str,
|
||||||
|
prompt_tokens: int,
|
||||||
|
completion_tokens: int,
|
||||||
|
**kwargs
|
||||||
|
) -> CostData:
|
||||||
|
"""Calculate cost in millisatoshis"""
|
||||||
|
# Model-based or fixed pricing
|
||||||
|
# Token counting
|
||||||
|
# Fee application
|
||||||
|
# Currency conversion
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routstr/payment/models.py`
|
||||||
|
|
||||||
|
Manages model pricing data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ModelPrice:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
pricing: dict[str, float] # USD prices
|
||||||
|
context_length: int
|
||||||
|
|
||||||
|
# Global model registry
|
||||||
|
MODELS: dict[str, ModelPrice] = load_models()
|
||||||
|
|
||||||
|
# Dynamic price updates
|
||||||
|
async def update_sats_pricing():
|
||||||
|
"""Background task to update BTC prices"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routstr/payment/x_cashu.py`
|
||||||
|
|
||||||
|
Implements Cashu payment protocol:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class XCashuHandler:
|
||||||
|
"""Handle x-cashu header payments"""
|
||||||
|
|
||||||
|
async def process_request_payment(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
estimated_cost: int
|
||||||
|
) -> PaymentResult:
|
||||||
|
# Validate token
|
||||||
|
# Check minimum amount
|
||||||
|
# Process payment
|
||||||
|
# Generate change
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Proxying
|
||||||
|
|
||||||
|
#### `routstr/proxy.py`
|
||||||
|
|
||||||
|
Core proxy functionality:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.api_route("/{path:path}", methods=ALL_METHODS)
|
||||||
|
async def proxy_request(
|
||||||
|
request: Request,
|
||||||
|
path: str,
|
||||||
|
api_key: APIKey = Depends(APIKeyAuth())
|
||||||
|
) -> Response:
|
||||||
|
"""Forward requests to upstream provider"""
|
||||||
|
# Build upstream request
|
||||||
|
# Stream response
|
||||||
|
# Track usage
|
||||||
|
# Deduct costs
|
||||||
|
```
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Streaming support
|
||||||
|
- Header preservation
|
||||||
|
- Error handling
|
||||||
|
- Usage tracking
|
||||||
|
|
||||||
|
### Database Layer
|
||||||
|
|
||||||
|
#### `routstr/core/db.py`
|
||||||
|
|
||||||
|
SQLModel definitions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class APIKey(SQLModel, table=True):
|
||||||
|
id: int | None = Field(primary_key=True)
|
||||||
|
key_hash: str = Field(index=True, unique=True)
|
||||||
|
balance: int # millisatoshis
|
||||||
|
total_deposited: int = 0
|
||||||
|
total_spent: int = 0
|
||||||
|
created_at: datetime
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
metadata: dict = Field(default_factory=dict, sa_column=Column(JSON))
|
||||||
|
|
||||||
|
class Transaction(SQLModel, table=True):
|
||||||
|
id: int | None = Field(primary_key=True)
|
||||||
|
api_key_id: int = Field(foreign_key="apikey.id")
|
||||||
|
amount: int # can be negative
|
||||||
|
balance_after: int
|
||||||
|
type: TransactionType
|
||||||
|
description: str
|
||||||
|
timestamp: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Interface
|
||||||
|
|
||||||
|
#### `routstr/core/admin.py`
|
||||||
|
|
||||||
|
Web dashboard and admin API:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@admin_router.get("/admin/")
|
||||||
|
async def admin_dashboard(request: Request):
|
||||||
|
"""Render admin HTML interface"""
|
||||||
|
# Authentication check
|
||||||
|
# Load statistics
|
||||||
|
# Render template
|
||||||
|
|
||||||
|
@admin_router.post("/admin/api/withdraw")
|
||||||
|
async def withdraw_balance(
|
||||||
|
api_key: str,
|
||||||
|
amount: int | None = None
|
||||||
|
) -> WithdrawalResponse:
|
||||||
|
"""Generate eCash token for withdrawal"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- HTML dashboard
|
||||||
|
- API key management
|
||||||
|
- Balance withdrawals
|
||||||
|
- Usage statistics
|
||||||
|
|
||||||
|
### Wallet Integration
|
||||||
|
|
||||||
|
#### `routstr/wallet.py`
|
||||||
|
|
||||||
|
Cashu wallet operations:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WalletManager:
|
||||||
|
"""Manage Cashu wallet instances"""
|
||||||
|
|
||||||
|
async def redeem_token(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
mint_url: str | None = None
|
||||||
|
) -> int:
|
||||||
|
"""Redeem eCash token and return value"""
|
||||||
|
|
||||||
|
async def create_token(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
mint_url: str
|
||||||
|
) -> str:
|
||||||
|
"""Create eCash token for withdrawal"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Modules
|
||||||
|
|
||||||
|
#### `routstr/core/logging.py`
|
||||||
|
|
||||||
|
Structured logging configuration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setup_logging():
|
||||||
|
"""Configure JSON structured logging"""
|
||||||
|
# Set log level
|
||||||
|
# Configure formatters
|
||||||
|
# Add handlers
|
||||||
|
|
||||||
|
class RequestIdMiddleware:
|
||||||
|
"""Add request ID to all logs"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routstr/core/middleware.py`
|
||||||
|
|
||||||
|
HTTP middleware components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LoggingMiddleware:
|
||||||
|
"""Log all HTTP requests/responses"""
|
||||||
|
|
||||||
|
class ErrorHandlingMiddleware:
|
||||||
|
"""Consistent error responses"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routstr/core/exceptions.py`
|
||||||
|
|
||||||
|
Custom exception hierarchy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RoustrError(Exception):
|
||||||
|
"""Base exception with error details"""
|
||||||
|
status_code: int
|
||||||
|
error_type: str
|
||||||
|
detail: str
|
||||||
|
|
||||||
|
class PaymentError(RoustrError):
|
||||||
|
"""Payment-related errors"""
|
||||||
|
|
||||||
|
class UpstreamError(RoustrError):
|
||||||
|
"""Upstream API errors"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
### `pyproject.toml`
|
||||||
|
|
||||||
|
Project metadata and dependencies:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "routstr"
|
||||||
|
version = "0.1.1b"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi[standard]>=0.115",
|
||||||
|
"sqlmodel>=0.0.24",
|
||||||
|
"cashu",
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `alembic.ini`
|
||||||
|
|
||||||
|
Database migration configuration:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[alembic]
|
||||||
|
script_location = migrations
|
||||||
|
prepend_sys_path = .
|
||||||
|
version_path_separator = os
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Makefile`
|
||||||
|
|
||||||
|
Development commands:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Setup commands
|
||||||
|
setup:
|
||||||
|
uv sync
|
||||||
|
uv pip install -e .
|
||||||
|
|
||||||
|
# Development server
|
||||||
|
dev:
|
||||||
|
fastapi dev routstr --host 0.0.0.0
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
test:
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Code quality
|
||||||
|
lint:
|
||||||
|
uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Patterns
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Using FastAPI's DI system:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Define dependency
|
||||||
|
async def get_db() -> AsyncSession:
|
||||||
|
async with async_session() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
# Use in routes
|
||||||
|
@router.get("/items")
|
||||||
|
async def get_items(db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Item))
|
||||||
|
return result.scalars().all()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Context Managers
|
||||||
|
|
||||||
|
For resource management:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url)
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
# Atomic operations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
|
||||||
|
Leveraging Python 3.11+ features:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Union types with |
|
||||||
|
def process(value: str | int) -> dict[str, Any]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Type aliases
|
||||||
|
Balance = int # millisatoshis
|
||||||
|
TokenList = list[dict[str, str]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Consistent error responses:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = await risky_operation()
|
||||||
|
except SpecificError as e:
|
||||||
|
logger.error("Operation failed", exc_info=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": "specific_error",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Module Organization
|
||||||
|
|
||||||
|
1. **Single Responsibility**: Each module has one clear purpose
|
||||||
|
2. **Minimal Imports**: Import only what's needed
|
||||||
|
3. **Circular Dependencies**: Avoid by using dependency injection
|
||||||
|
4. **Public API**: Expose through `__init__.py`
|
||||||
|
|
||||||
|
### Function Design
|
||||||
|
|
||||||
|
1. **Type Hints**: Always include complete type annotations
|
||||||
|
2. **Async First**: Use async/await for I/O operations
|
||||||
|
3. **Error Handling**: Raise specific exceptions
|
||||||
|
4. **Documentation**: Docstrings for public functions
|
||||||
|
|
||||||
|
### Testing Structure
|
||||||
|
|
||||||
|
1. **Mirror Source**: Test structure matches source
|
||||||
|
2. **Fixtures**: Reusable test data in conftest.py
|
||||||
|
3. **Mocking**: Mock external dependencies
|
||||||
|
4. **Coverage**: Aim for >80% coverage
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Review [Testing Guide](testing.md) for test structure
|
||||||
|
- See [Database Guide](database.md) for schema details
|
||||||
|
- Check [Guidelines](guidelines.md) for coding standards
|
||||||
|
- Read [Architecture](architecture.md) for system design
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
# Database Guide
|
||||||
|
|
||||||
|
This guide covers database design, migrations, and best practices for Routstr Core.
|
||||||
|
|
||||||
|
## Database Overview
|
||||||
|
|
||||||
|
Routstr uses:
|
||||||
|
- **SQLite** for local development and single-node deployments
|
||||||
|
- **PostgreSQL** (optional) for production scale
|
||||||
|
- **SQLModel** for ORM with type safety
|
||||||
|
- **Alembic** for schema migrations
|
||||||
|
- **Async SQLAlchemy** for non-blocking I/O
|
||||||
|
|
||||||
|
## Schema Design
|
||||||
|
|
||||||
|
### Core Tables
|
||||||
|
|
||||||
|
#### APIKey Table
|
||||||
|
|
||||||
|
```python
|
||||||
|
class APIKey(SQLModel, table=True):
|
||||||
|
"""API key with balance tracking"""
|
||||||
|
|
||||||
|
# Primary key
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
|
||||||
|
# Key data (indexed for fast lookups)
|
||||||
|
key_hash: str = Field(index=True, unique=True)
|
||||||
|
|
||||||
|
# Balance in millisatoshis (1 sat = 1000 msats)
|
||||||
|
balance: int = Field(default=0)
|
||||||
|
total_deposited: int = Field(default=0)
|
||||||
|
total_spent: int = Field(default=0)
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
last_used_at: datetime | None = None
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
|
||||||
|
# Metadata (JSON field)
|
||||||
|
metadata: dict = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
sa_column=Column(JSON)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
transactions: list["Transaction"] = Relationship(back_populates="api_key")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Transaction Table
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Transaction(SQLModel, table=True):
|
||||||
|
"""Transaction log for audit trail"""
|
||||||
|
|
||||||
|
# Primary key
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
|
||||||
|
# Foreign key to API key
|
||||||
|
api_key_id: int = Field(foreign_key="apikey.id", index=True)
|
||||||
|
|
||||||
|
# Transaction details
|
||||||
|
amount: int # Can be negative for deductions
|
||||||
|
balance_after: int # Balance snapshot
|
||||||
|
type: TransactionType # Enum: deposit, usage, withdrawal
|
||||||
|
description: str
|
||||||
|
|
||||||
|
# Timestamp (indexed for range queries)
|
||||||
|
timestamp: datetime = Field(
|
||||||
|
default_factory=datetime.utcnow,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Request details (optional)
|
||||||
|
request_data: dict | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column(JSON)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
api_key: APIKey = Relationship(back_populates="transactions")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Withdrawal Table
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Withdrawal(SQLModel, table=True):
|
||||||
|
"""Track withdrawal requests"""
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
api_key_id: int = Field(foreign_key="apikey.id")
|
||||||
|
|
||||||
|
# Withdrawal details
|
||||||
|
amount: int
|
||||||
|
token: str # Encrypted eCash token
|
||||||
|
mint_url: str
|
||||||
|
|
||||||
|
# Status tracking
|
||||||
|
status: WithdrawalStatus # pending, completed, failed
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
completed_at: datetime | None = None
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
error_message: str | None = None
|
||||||
|
retry_count: int = Field(default=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Indexes
|
||||||
|
|
||||||
|
Critical indexes for performance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In models
|
||||||
|
key_hash: str = Field(index=True, unique=True) # Fast key lookup
|
||||||
|
timestamp: datetime = Field(index=True) # Range queries
|
||||||
|
|
||||||
|
# Composite indexes (in migrations)
|
||||||
|
Index('idx_transactions_key_time', 'api_key_id', 'timestamp')
|
||||||
|
Index('idx_apikey_expires', 'expires_at').where(expires_at.isnot(None))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
### Creating Migrations
|
||||||
|
|
||||||
|
#### Auto-generate from Model Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After modifying SQLModel classes
|
||||||
|
make db-migrate
|
||||||
|
|
||||||
|
# Enter descriptive message
|
||||||
|
> Add withdrawal status field
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Manual Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create empty migration
|
||||||
|
alembic revision -m "custom migration"
|
||||||
|
|
||||||
|
# Edit the generated file
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration Template
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Add withdrawal status field
|
||||||
|
|
||||||
|
Revision ID: abc123
|
||||||
|
Revises: def456
|
||||||
|
Create Date: 2024-01-01 12:00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
|
||||||
|
# revision identifiers
|
||||||
|
revision = 'abc123'
|
||||||
|
down_revision = 'def456'
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Apply migration"""
|
||||||
|
op.add_column(
|
||||||
|
'withdrawal',
|
||||||
|
sa.Column(
|
||||||
|
'status',
|
||||||
|
sa.String(),
|
||||||
|
nullable=False,
|
||||||
|
server_default='pending'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add index
|
||||||
|
op.create_index(
|
||||||
|
'idx_withdrawal_status',
|
||||||
|
'withdrawal',
|
||||||
|
['status']
|
||||||
|
)
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Revert migration"""
|
||||||
|
op.drop_index('idx_withdrawal_status', 'withdrawal')
|
||||||
|
op.drop_column('withdrawal', 'status')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Migrations
|
||||||
|
|
||||||
|
#### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all migrations
|
||||||
|
make db-upgrade
|
||||||
|
|
||||||
|
# Check current version
|
||||||
|
make db-current
|
||||||
|
|
||||||
|
# Rollback one version
|
||||||
|
make db-downgrade
|
||||||
|
|
||||||
|
# View history
|
||||||
|
make db-history
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production
|
||||||
|
|
||||||
|
Migrations run automatically on startup:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In routstr/core/main.py
|
||||||
|
def run_migrations():
|
||||||
|
"""Run database migrations on startup"""
|
||||||
|
from alembic import command
|
||||||
|
from alembic.config import Config
|
||||||
|
|
||||||
|
alembic_cfg = Config("alembic.ini")
|
||||||
|
command.upgrade(alembic_cfg, "head")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Best Practices
|
||||||
|
|
||||||
|
1. **Always Review Generated Migrations**
|
||||||
|
- Check for data loss
|
||||||
|
- Verify index creation
|
||||||
|
- Test rollback
|
||||||
|
|
||||||
|
2. **Handle Data Migrations**
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Schema change
|
||||||
|
op.add_column('apikey', sa.Column('status', sa.String()))
|
||||||
|
|
||||||
|
# Data migration
|
||||||
|
connection = op.get_bind()
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE apikey SET status = 'active' WHERE expires_at IS NULL"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make Migrations Idempotent**
|
||||||
|
```python
|
||||||
|
def upgrade():
|
||||||
|
# Check if column exists
|
||||||
|
inspector = sa.inspect(op.get_bind())
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('apikey')]
|
||||||
|
|
||||||
|
if 'new_field' not in columns:
|
||||||
|
op.add_column('apikey', sa.Column('new_field', sa.String()))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Operations
|
||||||
|
|
||||||
|
### Connection Management
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Database session factory
|
||||||
|
async_session = sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dependency injection
|
||||||
|
async def get_db() -> AsyncSession:
|
||||||
|
async with async_session() as session:
|
||||||
|
yield session
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Patterns
|
||||||
|
|
||||||
|
#### Basic Queries
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get by primary key
|
||||||
|
api_key = await session.get(APIKey, key_id)
|
||||||
|
|
||||||
|
# Get by unique field
|
||||||
|
result = await session.execute(
|
||||||
|
select(APIKey).where(APIKey.key_hash == hash_value)
|
||||||
|
)
|
||||||
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# Get multiple with filter
|
||||||
|
result = await session.execute(
|
||||||
|
select(APIKey)
|
||||||
|
.where(APIKey.balance > 0)
|
||||||
|
.where(APIKey.expires_at > datetime.utcnow())
|
||||||
|
)
|
||||||
|
active_keys = result.scalars().all()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Joins and Relationships
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Eager loading
|
||||||
|
result = await session.execute(
|
||||||
|
select(APIKey)
|
||||||
|
.options(selectinload(APIKey.transactions))
|
||||||
|
.where(APIKey.id == key_id)
|
||||||
|
)
|
||||||
|
api_key = result.scalar_one()
|
||||||
|
|
||||||
|
# Join query
|
||||||
|
result = await session.execute(
|
||||||
|
select(Transaction)
|
||||||
|
.join(APIKey)
|
||||||
|
.where(APIKey.key_hash == hash_value)
|
||||||
|
.order_by(Transaction.timestamp.desc())
|
||||||
|
.limit(10)
|
||||||
|
)
|
||||||
|
recent_transactions = result.scalars().all()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Aggregations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Sum total spent
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.sum(Transaction.amount))
|
||||||
|
.where(Transaction.api_key_id == key_id)
|
||||||
|
.where(Transaction.type == TransactionType.USAGE)
|
||||||
|
)
|
||||||
|
total_spent = result.scalar() or 0
|
||||||
|
|
||||||
|
# Count active keys
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.count(APIKey.id))
|
||||||
|
.where(APIKey.balance > 0)
|
||||||
|
)
|
||||||
|
active_count = result.scalar()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
#### Atomic Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def transfer_balance(
|
||||||
|
session: AsyncSession,
|
||||||
|
from_key: int,
|
||||||
|
to_key: int,
|
||||||
|
amount: int
|
||||||
|
):
|
||||||
|
"""Atomic balance transfer"""
|
||||||
|
async with session.begin():
|
||||||
|
# Lock rows to prevent race conditions
|
||||||
|
from_api_key = await session.execute(
|
||||||
|
select(APIKey)
|
||||||
|
.where(APIKey.id == from_key)
|
||||||
|
.with_for_update()
|
||||||
|
)
|
||||||
|
from_api_key = from_api_key.scalar_one()
|
||||||
|
|
||||||
|
to_api_key = await session.execute(
|
||||||
|
select(APIKey)
|
||||||
|
.where(APIKey.id == to_key)
|
||||||
|
.with_for_update()
|
||||||
|
)
|
||||||
|
to_api_key = to_api_key.scalar_one()
|
||||||
|
|
||||||
|
# Check balance
|
||||||
|
if from_api_key.balance < amount:
|
||||||
|
raise InsufficientBalanceError()
|
||||||
|
|
||||||
|
# Update balances
|
||||||
|
from_api_key.balance -= amount
|
||||||
|
to_api_key.balance += amount
|
||||||
|
|
||||||
|
# Log transactions
|
||||||
|
session.add(Transaction(
|
||||||
|
api_key_id=from_key,
|
||||||
|
amount=-amount,
|
||||||
|
balance_after=from_api_key.balance,
|
||||||
|
type=TransactionType.TRANSFER_OUT
|
||||||
|
))
|
||||||
|
|
||||||
|
session.add(Transaction(
|
||||||
|
api_key_id=to_key,
|
||||||
|
amount=amount,
|
||||||
|
balance_after=to_api_key.balance,
|
||||||
|
type=TransactionType.TRANSFER_IN
|
||||||
|
))
|
||||||
|
|
||||||
|
# Commit happens automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Optimistic Locking
|
||||||
|
|
||||||
|
```python
|
||||||
|
class APIKey(SQLModel, table=True):
|
||||||
|
# Add version field
|
||||||
|
version: int = Field(default=1)
|
||||||
|
|
||||||
|
async def update_with_version_check(
|
||||||
|
session: AsyncSession,
|
||||||
|
api_key: APIKey,
|
||||||
|
new_balance: int
|
||||||
|
):
|
||||||
|
"""Update with optimistic locking"""
|
||||||
|
result = await session.execute(
|
||||||
|
update(APIKey)
|
||||||
|
.where(APIKey.id == api_key.id)
|
||||||
|
.where(APIKey.version == api_key.version)
|
||||||
|
.values(
|
||||||
|
balance=new_balance,
|
||||||
|
version=APIKey.version + 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.rowcount == 0:
|
||||||
|
raise ConcurrentModificationError()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Query Optimization
|
||||||
|
|
||||||
|
1. **Use Indexes Effectively**
|
||||||
|
```python
|
||||||
|
# Good: Uses index
|
||||||
|
where(APIKey.key_hash == value)
|
||||||
|
|
||||||
|
# Bad: Function prevents index use
|
||||||
|
where(func.lower(APIKey.key_hash) == value.lower())
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Limit Results**
|
||||||
|
```python
|
||||||
|
# Always limit when possible
|
||||||
|
query.limit(100)
|
||||||
|
|
||||||
|
# Use pagination
|
||||||
|
query.offset(page * page_size).limit(page_size)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Select Only Needed Columns**
|
||||||
|
```python
|
||||||
|
# Select specific columns
|
||||||
|
result = await session.execute(
|
||||||
|
select(APIKey.id, APIKey.balance)
|
||||||
|
.where(APIKey.key_hash == hash_value)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Pooling
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Configure connection pool
|
||||||
|
engine = create_async_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
pool_size=20, # Number of connections
|
||||||
|
max_overflow=10, # Extra connections when needed
|
||||||
|
pool_timeout=30, # Wait time for connection
|
||||||
|
pool_recycle=3600, # Recycle connections after 1 hour
|
||||||
|
pool_pre_ping=True, # Check connection health
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Batch insert
|
||||||
|
async def bulk_create_transactions(
|
||||||
|
session: AsyncSession,
|
||||||
|
transactions: list[dict]
|
||||||
|
):
|
||||||
|
"""Efficient bulk insert"""
|
||||||
|
await session.execute(
|
||||||
|
insert(Transaction),
|
||||||
|
transactions
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# Batch update
|
||||||
|
await session.execute(
|
||||||
|
update(APIKey)
|
||||||
|
.where(APIKey.expires_at < datetime.utcnow())
|
||||||
|
.values(active=False)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Database Code
|
||||||
|
|
||||||
|
### Test Database Setup
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_engine():
|
||||||
|
"""Create test database engine"""
|
||||||
|
engine = create_async_engine(
|
||||||
|
"sqlite+aiosqlite:///:memory:",
|
||||||
|
echo=True # Log SQL for debugging
|
||||||
|
)
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
|
|
||||||
|
yield engine
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Queries
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_balance_update(test_session):
|
||||||
|
"""Test atomic balance update"""
|
||||||
|
# Create test data
|
||||||
|
api_key = APIKey(key_hash="test", balance=1000)
|
||||||
|
test_session.add(api_key)
|
||||||
|
await test_session.commit()
|
||||||
|
|
||||||
|
# Test update
|
||||||
|
await deduct_balance(test_session, api_key.id, 100)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
await test_session.refresh(api_key)
|
||||||
|
assert api_key.balance == 900
|
||||||
|
|
||||||
|
# Verify transaction log
|
||||||
|
result = await test_session.execute(
|
||||||
|
select(Transaction)
|
||||||
|
.where(Transaction.api_key_id == api_key.id)
|
||||||
|
)
|
||||||
|
transactions = result.scalars().all()
|
||||||
|
assert len(transactions) == 1
|
||||||
|
assert transactions[0].amount == -100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Migrations
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_migration_upgrade():
|
||||||
|
"""Test migration applies correctly"""
|
||||||
|
# Create database at previous version
|
||||||
|
alembic_cfg = Config("alembic.ini")
|
||||||
|
command.downgrade(alembic_cfg, "-1")
|
||||||
|
|
||||||
|
# Apply migration
|
||||||
|
command.upgrade(alembic_cfg, "+1")
|
||||||
|
|
||||||
|
# Verify schema changes
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
inspector = inspect(engine)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('apikey')]
|
||||||
|
assert 'new_column' in columns
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Maintenance
|
||||||
|
|
||||||
|
### Monitoring Queries
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Slow queries (SQLite)
|
||||||
|
EXPLAIN QUERY PLAN
|
||||||
|
SELECT * FROM apikey WHERE balance > 0;
|
||||||
|
|
||||||
|
-- Table sizes
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
COUNT(*) as row_count
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type='table'
|
||||||
|
GROUP BY name;
|
||||||
|
|
||||||
|
-- Index usage
|
||||||
|
SELECT * FROM sqlite_stat1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup Tasks
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def cleanup_expired_keys(session: AsyncSession):
|
||||||
|
"""Remove expired API keys"""
|
||||||
|
result = await session.execute(
|
||||||
|
delete(APIKey)
|
||||||
|
.where(APIKey.expires_at < datetime.utcnow())
|
||||||
|
.where(APIKey.balance == 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Cleaned up {result.rowcount} expired keys")
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def vacuum_database(session: AsyncSession):
|
||||||
|
"""Optimize database file size (SQLite)"""
|
||||||
|
await session.execute(text("VACUUM"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Strategies
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def backup_database(source_url: str, backup_path: str):
|
||||||
|
"""Create database backup"""
|
||||||
|
if "sqlite" in source_url:
|
||||||
|
# SQLite backup
|
||||||
|
import shutil
|
||||||
|
db_path = source_url.split("///")[1]
|
||||||
|
shutil.copy2(db_path, backup_path)
|
||||||
|
else:
|
||||||
|
# PostgreSQL backup
|
||||||
|
import subprocess
|
||||||
|
subprocess.run([
|
||||||
|
"pg_dump",
|
||||||
|
source_url,
|
||||||
|
"-f", backup_path
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## PostgreSQL Migration
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
# PostgreSQL connection
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/routstr"
|
||||||
|
|
||||||
|
# Additional PostgreSQL-specific settings
|
||||||
|
engine = create_async_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
server_settings={
|
||||||
|
"jit": "off",
|
||||||
|
"statement_timeout": "30s"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL-Specific Features
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Use PostgreSQL arrays
|
||||||
|
from sqlalchemy.dialects.postgresql import ARRAY
|
||||||
|
|
||||||
|
class APIKey(SQLModel, table=True):
|
||||||
|
allowed_models: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
sa_column=Column(ARRAY(String))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use JSONB for better performance
|
||||||
|
metadata: dict = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
sa_column=Column(JSONB)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Full-text search
|
||||||
|
from sqlalchemy.dialects.postgresql import TSVECTOR
|
||||||
|
|
||||||
|
search_vector = Column(TSVECTOR)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### SQL Injection Prevention
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Always use parameterized queries
|
||||||
|
# Good
|
||||||
|
await session.execute(
|
||||||
|
select(APIKey).where(APIKey.key_hash == key_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bad - SQL injection risk
|
||||||
|
await session.execute(
|
||||||
|
text(f"SELECT * FROM apikey WHERE key_hash = '{key_hash}'")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Encryption
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
class EncryptedField(TypeDecorator):
|
||||||
|
"""Encrypt sensitive data at rest"""
|
||||||
|
impl = String
|
||||||
|
|
||||||
|
def __init__(self, key: bytes, *args, **kwargs):
|
||||||
|
self.cipher = Fernet(key)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if value is not None:
|
||||||
|
return self.cipher.encrypt(value.encode()).decode()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
if value is not None:
|
||||||
|
return self.cipher.decrypt(value.encode()).decode()
|
||||||
|
return value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Review [Testing Guide](testing.md) for database testing
|
||||||
|
- Check [Guidelines](guidelines.md) for code standards
|
||||||
|
- See [Architecture](architecture.md) for system design
|
||||||
|
- Read [Setup Guide](setup.md) for development setup
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
# Contribution Guidelines
|
||||||
|
|
||||||
|
Thank you for considering contributing to Routstr Core! This document provides guidelines and standards for contributing to the project.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
### Our Pledge
|
||||||
|
|
||||||
|
We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
|
||||||
|
- Be respectful and inclusive
|
||||||
|
- Accept constructive criticism gracefully
|
||||||
|
- Focus on what's best for the community
|
||||||
|
- Show empathy towards other contributors
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### First Time Contributors
|
||||||
|
|
||||||
|
1. **Find an Issue**
|
||||||
|
- Look for issues labeled `good first issue`
|
||||||
|
- Check `help wanted` labels
|
||||||
|
- Ask in discussions if unsure
|
||||||
|
|
||||||
|
2. **Claim the Issue**
|
||||||
|
- Comment on the issue to claim it
|
||||||
|
- Wait for maintainer acknowledgment
|
||||||
|
- Ask questions if needed
|
||||||
|
|
||||||
|
3. **Fork and Branch**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/routstr-core.git
|
||||||
|
cd routstr-core
|
||||||
|
git checkout -b feat/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
### Python Style Guide
|
||||||
|
|
||||||
|
We follow modern Python practices with strict type checking:
|
||||||
|
|
||||||
|
#### Type Annotations
|
||||||
|
|
||||||
|
Always use complete type hints:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Good - Complete type hints
|
||||||
|
async def process_payment(
|
||||||
|
token: str,
|
||||||
|
amount: int,
|
||||||
|
mint_url: str | None = None
|
||||||
|
) -> PaymentResult:
|
||||||
|
"""Process an eCash payment."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ❌ Bad - Missing or incomplete types
|
||||||
|
async def process_payment(token, amount, mint_url=None):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Modern Python Features
|
||||||
|
|
||||||
|
Use Python 3.11+ syntax:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Good - Modern union types
|
||||||
|
def handle_value(data: str | int | None) -> dict[str, Any]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ❌ Bad - Old-style typing
|
||||||
|
from typing import Union, Dict, Optional
|
||||||
|
def handle_value(data: Optional[Union[str, int]]) -> Dict[str, Any]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
|
||||||
|
Be explicit with exceptions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Good - Specific exceptions with context
|
||||||
|
class InsufficientBalanceError(RoustrError):
|
||||||
|
"""Raised when balance is insufficient for operation."""
|
||||||
|
|
||||||
|
def __init__(self, required: int, available: int):
|
||||||
|
super().__init__(
|
||||||
|
f"Insufficient balance: required {required}, available {available}"
|
||||||
|
)
|
||||||
|
self.required = required
|
||||||
|
self.available = available
|
||||||
|
|
||||||
|
# ❌ Bad - Generic exceptions
|
||||||
|
raise Exception("Not enough balance")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async/Await Patterns
|
||||||
|
|
||||||
|
All I/O operations must be async:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Good - Async all the way
|
||||||
|
async def fetch_user_data(user_id: int) -> UserData:
|
||||||
|
async with get_db() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.id == user_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one()
|
||||||
|
|
||||||
|
# ❌ Bad - Blocking I/O
|
||||||
|
def fetch_user_data(user_id: int) -> UserData:
|
||||||
|
with get_db() as session:
|
||||||
|
return session.query(User).filter_by(id=user_id).first()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
|
||||||
|
#### Module Documentation
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Payment processing module.
|
||||||
|
|
||||||
|
This module handles all payment-related operations including:
|
||||||
|
- Token validation and redemption
|
||||||
|
- Balance management
|
||||||
|
- Cost calculation
|
||||||
|
- Transaction logging
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Function Documentation
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def redeem_token(
|
||||||
|
token: str,
|
||||||
|
mint_url: str | None = None,
|
||||||
|
*,
|
||||||
|
verify: bool = True
|
||||||
|
) -> RedemptionResult:
|
||||||
|
"""Redeem a Cashu eCash token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: Base64-encoded Cashu token
|
||||||
|
mint_url: Optional mint URL override
|
||||||
|
verify: Whether to verify token with mint
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RedemptionResult containing amount and token details
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TokenInvalidError: If token format is invalid
|
||||||
|
TokenExpiredError: If token has expired
|
||||||
|
MintConnectionError: If mint is unreachable
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = await redeem_token("cashuAey...")
|
||||||
|
>>> print(f"Redeemed {result.amount} sats")
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
Only add comments for non-obvious logic:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Good - Explains complex business logic
|
||||||
|
# Apply exponential backoff with jitter to prevent thundering herd
|
||||||
|
# when multiple clients retry simultaneously
|
||||||
|
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
|
||||||
|
|
||||||
|
# ❌ Bad - States the obvious
|
||||||
|
# Increment the counter by 1
|
||||||
|
counter += 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
- New features must include tests
|
||||||
|
- Maintain >80% code coverage
|
||||||
|
- Test edge cases and error conditions
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestFeatureName:
|
||||||
|
"""Test suite for FeatureName functionality."""
|
||||||
|
|
||||||
|
async def test_happy_path(self):
|
||||||
|
"""Test normal operation succeeds."""
|
||||||
|
# Arrange
|
||||||
|
input_data = create_test_data()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = await function_under_test(input_data)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.success is True
|
||||||
|
assert result.value == expected_value
|
||||||
|
|
||||||
|
async def test_error_condition(self):
|
||||||
|
"""Test appropriate error is raised."""
|
||||||
|
with pytest.raises(SpecificError) as exc_info:
|
||||||
|
await function_under_test(invalid_data)
|
||||||
|
|
||||||
|
assert "descriptive message" in str(exc_info.value)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
- `feat`: New feature
|
||||||
|
- `fix`: Bug fix
|
||||||
|
- `docs`: Documentation only
|
||||||
|
- `style`: Code style (formatting, etc.)
|
||||||
|
- `refactor`: Code refactoring
|
||||||
|
- `test`: Test additions or fixes
|
||||||
|
- `chore`: Maintenance tasks
|
||||||
|
- `perf`: Performance improvements
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
feat(payment): add support for multi-mint tokens
|
||||||
|
|
||||||
|
Implement token validation across multiple trusted mints.
|
||||||
|
This allows users to pay with tokens from any configured mint.
|
||||||
|
|
||||||
|
Closes #123
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
fix(proxy): handle streaming timeouts correctly
|
||||||
|
|
||||||
|
Previously, long-running streaming requests would timeout after 30s.
|
||||||
|
This change implements proper timeout handling for streams.
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs(api): update authentication examples
|
||||||
|
|
||||||
|
Add examples for bearer token authentication and
|
||||||
|
update curl commands to match current API.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
### Before Submitting
|
||||||
|
|
||||||
|
1. **Run Quality Checks**
|
||||||
|
```bash
|
||||||
|
make format # Auto-format code
|
||||||
|
make lint # Check style
|
||||||
|
make type-check # Verify types
|
||||||
|
make test # Run all tests
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update Documentation**
|
||||||
|
- Add/update docstrings
|
||||||
|
- Update README if needed
|
||||||
|
- Add to changelog
|
||||||
|
|
||||||
|
3. **Verify Changes**
|
||||||
|
- Only include relevant changes
|
||||||
|
- No commented code
|
||||||
|
- No debug prints
|
||||||
|
- No temporary files
|
||||||
|
|
||||||
|
### PR Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Description
|
||||||
|
Brief description of changes
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
- [ ] Bug fix (non-breaking change fixing an issue)
|
||||||
|
- [ ] New feature (non-breaking change adding functionality)
|
||||||
|
- [ ] Breaking change (fix or feature causing existing functionality to change)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- [ ] Unit tests pass
|
||||||
|
- [ ] Integration tests pass
|
||||||
|
- [ ] Manual testing completed
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] Code follows project style guidelines
|
||||||
|
- [ ] Self-review completed
|
||||||
|
- [ ] Comments added for complex logic
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] No new warnings generated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Process
|
||||||
|
|
||||||
|
1. **Automated Checks**
|
||||||
|
- CI/CD must pass
|
||||||
|
- Coverage maintained
|
||||||
|
- No linting errors
|
||||||
|
|
||||||
|
2. **Peer Review**
|
||||||
|
- At least one approval required
|
||||||
|
- Address all feedback
|
||||||
|
- Resolve all conversations
|
||||||
|
|
||||||
|
3. **Final Steps**
|
||||||
|
- Squash commits if needed
|
||||||
|
- Update branch with main
|
||||||
|
- Maintainer merges
|
||||||
|
|
||||||
|
## Code Organization
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Use lowercase with underscores
|
||||||
|
payment_processor.py # ✅ Good
|
||||||
|
PaymentProcessor.py # ❌ Bad
|
||||||
|
payment-processor.py # ❌ Bad
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Organization
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Standard library imports
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import httpx
|
||||||
|
from fastapi import FastAPI, Depends
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from .auth import validate_token
|
||||||
|
from .models import User, Transaction
|
||||||
|
from .utils import calculate_hash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Module-level constants in UPPER_CASE
|
||||||
|
DEFAULT_TIMEOUT = 30
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
SUPPORTED_MODELS = ["gpt-3.5-turbo", "gpt-4"]
|
||||||
|
|
||||||
|
# Class constants
|
||||||
|
class PaymentProcessor:
|
||||||
|
MIN_AMOUNT = 1
|
||||||
|
MAX_AMOUNT = 1_000_000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
1. **Never commit secrets**
|
||||||
|
```python
|
||||||
|
# ❌ Bad
|
||||||
|
API_KEY = "sk-1234567890"
|
||||||
|
|
||||||
|
# ✅ Good
|
||||||
|
API_KEY = os.environ.get("API_KEY")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Validate all inputs**
|
||||||
|
```python
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
class PaymentRequest(BaseModel):
|
||||||
|
amount: int
|
||||||
|
token: str
|
||||||
|
|
||||||
|
@validator("amount")
|
||||||
|
def validate_amount(cls, v):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Amount must be positive")
|
||||||
|
return v
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use constant-time comparisons**
|
||||||
|
```python
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
# For sensitive comparisons
|
||||||
|
if secrets.compare_digest(provided_hash, expected_hash):
|
||||||
|
# Authenticated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
1. **Use async for I/O**
|
||||||
|
```python
|
||||||
|
# Concurrent operations
|
||||||
|
results = await asyncio.gather(
|
||||||
|
fetch_user(user_id),
|
||||||
|
fetch_balance(user_id),
|
||||||
|
fetch_transactions(user_id)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Implement caching**
|
||||||
|
```python
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_model_pricing(model_id: str) -> dict:
|
||||||
|
return MODELS.get(model_id, DEFAULT_PRICING)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use generators for large data**
|
||||||
|
```python
|
||||||
|
async def stream_transactions() -> AsyncGenerator[Transaction, None]:
|
||||||
|
async for transaction in get_transactions():
|
||||||
|
yield transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
1. **Be specific**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = await risky_operation()
|
||||||
|
except NetworkError:
|
||||||
|
# Handle network issues
|
||||||
|
logger.error("Network error during operation")
|
||||||
|
raise
|
||||||
|
except ValueError as e:
|
||||||
|
# Handle value errors
|
||||||
|
logger.warning(f"Invalid value: {e}")
|
||||||
|
return default_value
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Provide context**
|
||||||
|
```python
|
||||||
|
class PaymentError(RoustrError):
|
||||||
|
def __init__(self, token: str, reason: str):
|
||||||
|
super().__init__(f"Payment failed for token {token[:8]}: {reason}")
|
||||||
|
self.token = token
|
||||||
|
self.reason = reason
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
We use [Semantic Versioning](https://semver.org/):
|
||||||
|
- MAJOR: Breaking API changes
|
||||||
|
- MINOR: New features, backwards compatible
|
||||||
|
- PATCH: Bug fixes
|
||||||
|
|
||||||
|
### Release Checklist
|
||||||
|
|
||||||
|
1. Update version in `pyproject.toml`
|
||||||
|
2. Update CHANGELOG.md
|
||||||
|
3. Run full test suite
|
||||||
|
4. Create git tag
|
||||||
|
5. Push tag to trigger release
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
- [Development Setup](setup.md)
|
||||||
|
- [Architecture Overview](architecture.md)
|
||||||
|
- [Testing Guide](testing.md)
|
||||||
|
- [Database Guide](database.md)
|
||||||
|
|
||||||
|
### Communication
|
||||||
|
|
||||||
|
- **GitHub Issues**: Bug reports and feature requests
|
||||||
|
- **GitHub Discussions**: Questions and ideas
|
||||||
|
- **Pull Requests**: Code contributions
|
||||||
|
|
||||||
|
### Response Times
|
||||||
|
|
||||||
|
- Issues: 2-3 business days
|
||||||
|
- PRs: 2-3 business days
|
||||||
|
- Security issues: <24 hours
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
Contributors are recognized in:
|
||||||
|
- CONTRIBUTORS.md file
|
||||||
|
- Release notes
|
||||||
|
- Project documentation
|
||||||
|
|
||||||
|
Thank you for contributing to Routstr Core! 🎉
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
# Development Setup
|
||||||
|
|
||||||
|
This guide will help you set up a development environment for contributing to Routstr Core.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have:
|
||||||
|
|
||||||
|
- **Python 3.11+** - Required for type hints and modern features
|
||||||
|
- **Git** - For version control
|
||||||
|
- **Docker** (optional) - For running integration tests
|
||||||
|
- **Make** - For running development commands
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Fork and Clone
|
||||||
|
|
||||||
|
First, fork the repository on GitHub, then clone your fork:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/routstr-core.git
|
||||||
|
cd routstr-core
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install uv
|
||||||
|
|
||||||
|
We use [uv](https://github.com/astral-sh/uv) for fast, reliable Python package management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using the installer script
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
|
# Or with pip
|
||||||
|
pip install uv
|
||||||
|
|
||||||
|
# Or with Homebrew (macOS)
|
||||||
|
brew install uv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Set Up Environment
|
||||||
|
|
||||||
|
Run the setup command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- ✅ Install uv if not present
|
||||||
|
- ✅ Create a virtual environment
|
||||||
|
- ✅ Install all dependencies
|
||||||
|
- ✅ Install dev tools (mypy, ruff, pytest)
|
||||||
|
- ✅ Install project in editable mode
|
||||||
|
|
||||||
|
### 4. Configure Environment
|
||||||
|
|
||||||
|
Create your environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your configuration:
|
||||||
|
```bash
|
||||||
|
# Minimum required for development
|
||||||
|
UPSTREAM_BASE_URL=https://api.openai.com/v1
|
||||||
|
UPSTREAM_API_KEY=your-api-key # Optional for mock testing
|
||||||
|
ADMIN_PASSWORD=development-password
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///dev.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verify Installation
|
||||||
|
|
||||||
|
Run these commands to verify your setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check dependencies
|
||||||
|
make check-deps
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
make test-unit
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
|
||||||
|
For development with auto-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
# Server starts at http://localhost:8000
|
||||||
|
# Auto-reloads on code changes
|
||||||
|
```
|
||||||
|
|
||||||
|
For production-like environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
Before committing, always run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
make format
|
||||||
|
|
||||||
|
# Check linting
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
make type-check
|
||||||
|
|
||||||
|
# All checks at once
|
||||||
|
make check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Run different test suites:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Unit tests only (fast)
|
||||||
|
make test-unit
|
||||||
|
|
||||||
|
# Integration tests with mocks
|
||||||
|
make test-integration
|
||||||
|
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# With coverage report
|
||||||
|
make test-coverage
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
uv run pytest tests/unit/test_auth.py::test_token_validation -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
|
||||||
|
Work with database migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new migration
|
||||||
|
make db-migrate
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
make db-upgrade
|
||||||
|
|
||||||
|
# Rollback one migration
|
||||||
|
make db-downgrade
|
||||||
|
|
||||||
|
# View current revision
|
||||||
|
make db-current
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Understanding the codebase:
|
||||||
|
|
||||||
|
```
|
||||||
|
routstr-core/
|
||||||
|
├── routstr/ # Main package
|
||||||
|
│ ├── __init__.py # Package initialization
|
||||||
|
│ ├── auth.py # Authentication logic
|
||||||
|
│ ├── balance.py # Balance management
|
||||||
|
│ ├── discovery.py # Nostr discovery
|
||||||
|
│ ├── proxy.py # Request proxying
|
||||||
|
│ ├── wallet.py # Cashu wallet integration
|
||||||
|
│ │
|
||||||
|
│ ├── core/ # Core modules
|
||||||
|
│ │ ├── admin.py # Admin dashboard
|
||||||
|
│ │ ├── db.py # Database models
|
||||||
|
│ │ ├── exceptions.py # Custom exceptions
|
||||||
|
│ │ ├── logging.py # Logging setup
|
||||||
|
│ │ ├── main.py # FastAPI app
|
||||||
|
│ │ └── middleware.py # HTTP middleware
|
||||||
|
│ │
|
||||||
|
│ └── payment/ # Payment processing
|
||||||
|
│ ├── cost_calculation.py # Cost logic
|
||||||
|
│ ├── helpers.py # Utilities
|
||||||
|
│ ├── lnurl.py # Lightning URLs
|
||||||
|
│ ├── models.py # Model pricing
|
||||||
|
│ ├── price.py # BTC pricing
|
||||||
|
│ └── x_cashu.py # Cashu headers
|
||||||
|
│
|
||||||
|
├── tests/ # Test suite
|
||||||
|
│ ├── unit/ # Unit tests
|
||||||
|
│ └── integration/ # Integration tests
|
||||||
|
│
|
||||||
|
├── migrations/ # Database migrations
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│
|
||||||
|
├── Makefile # Dev commands
|
||||||
|
├── pyproject.toml # Project config
|
||||||
|
└── compose.yml # Docker setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Endpoint
|
||||||
|
|
||||||
|
1. Create route in appropriate module
|
||||||
|
2. Add request/response models
|
||||||
|
3. Write unit tests
|
||||||
|
4. Update API documentation
|
||||||
|
5. Add integration tests
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
# In routstr/core/main.py or appropriate router
|
||||||
|
@app.get("/v1/stats")
|
||||||
|
async def get_stats(
|
||||||
|
user: User = Depends(get_current_user)
|
||||||
|
) -> StatsResponse:
|
||||||
|
"""Get usage statistics for the current user."""
|
||||||
|
# Implementation
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a Database Model
|
||||||
|
|
||||||
|
1. Define model in `routstr/core/db.py`
|
||||||
|
2. Create migration: `make db-migrate`
|
||||||
|
3. Review generated migration
|
||||||
|
4. Apply: `make db-upgrade`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
class Transaction(SQLModel, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
api_key_id: int = Field(foreign_key="apikey.id")
|
||||||
|
amount: int # millisatoshis
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
description: str
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
Follow the AAA pattern:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_balance_deduction():
|
||||||
|
# Arrange
|
||||||
|
api_key = await create_test_api_key(balance=1000)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = await deduct_balance(api_key.key, amount=100)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.success
|
||||||
|
assert result.new_balance == 900
|
||||||
|
assert result.deducted == 100
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
### Makefile Commands
|
||||||
|
|
||||||
|
Key commands for development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make help # Show all commands
|
||||||
|
make setup # Initial setup
|
||||||
|
make dev # Run dev server
|
||||||
|
make test # Run all tests
|
||||||
|
make lint # Check code style
|
||||||
|
make format # Fix code style
|
||||||
|
make type-check # Check types
|
||||||
|
make clean # Clean temp files
|
||||||
|
make docker-build # Build Docker image
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDE Setup
|
||||||
|
|
||||||
|
#### VS Code
|
||||||
|
|
||||||
|
Recommended extensions:
|
||||||
|
- Python
|
||||||
|
- Pylance
|
||||||
|
- Ruff
|
||||||
|
- GitLens
|
||||||
|
|
||||||
|
Settings (`.vscode/settings.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.linting.ruffEnabled": true,
|
||||||
|
"python.formatting.provider": "ruff",
|
||||||
|
"python.analysis.typeCheckingMode": "strict",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PyCharm
|
||||||
|
|
||||||
|
1. Set Python interpreter to uv venv
|
||||||
|
2. Enable type checking
|
||||||
|
3. Configure Ruff as external tool
|
||||||
|
4. Set up file watchers for formatting
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
#### Debug Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with debug logging
|
||||||
|
LOG_LEVEL=DEBUG make dev
|
||||||
|
|
||||||
|
# Or with debugger
|
||||||
|
uv run python -m debugpy --listen 5678 --wait-for-client \
|
||||||
|
-m uvicorn routstr:fastapi_app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debug Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run specific test with output
|
||||||
|
uv run pytest tests/unit/test_auth.py -v -s
|
||||||
|
|
||||||
|
# With debugger
|
||||||
|
uv run pytest tests/unit/test_auth.py --pdb
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Import Errors**
|
||||||
|
```bash
|
||||||
|
# Ensure project is installed in editable mode
|
||||||
|
uv sync
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Errors**
|
||||||
|
```bash
|
||||||
|
# Reset database
|
||||||
|
rm dev.db
|
||||||
|
make db-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type Checking Fails**
|
||||||
|
```bash
|
||||||
|
# Clear mypy cache
|
||||||
|
make clean
|
||||||
|
make type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tests Fail Locally**
|
||||||
|
```bash
|
||||||
|
# Ensure test dependencies are installed
|
||||||
|
uv sync --dev
|
||||||
|
|
||||||
|
# Check for leftover test data
|
||||||
|
rm -rf test_*.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check existing [GitHub Issues](https://github.com/routstr/routstr-core/issues)
|
||||||
|
- Review [Contributing Guidelines](guidelines.md)
|
||||||
|
- Ask in [GitHub Discussions](https://github.com/routstr/routstr-core/discussions)
|
||||||
|
- Read the [Architecture Guide](architecture.md)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you're set up:
|
||||||
|
|
||||||
|
1. Read the [Architecture Overview](architecture.md)
|
||||||
|
2. Review [Code Standards](guidelines.md)
|
||||||
|
3. Check [open issues](https://github.com/routstr/routstr-core/issues)
|
||||||
|
4. Start with a small contribution
|
||||||
|
|
||||||
|
Happy coding! 🚀
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
# Testing Guide
|
||||||
|
|
||||||
|
This guide covers testing practices, patterns, and tools used in Routstr Core development.
|
||||||
|
|
||||||
|
## Testing Philosophy
|
||||||
|
|
||||||
|
We follow these principles:
|
||||||
|
- **Test Behavior, Not Implementation** - Tests should survive refactoring
|
||||||
|
- **Fast Feedback** - Unit tests run in milliseconds
|
||||||
|
- **Reliable Tests** - No flaky tests allowed
|
||||||
|
- **Clear Failures** - Tests should clearly indicate what broke
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── __init__.py
|
||||||
|
├── conftest.py # Shared fixtures and configuration
|
||||||
|
├── unit/ # Fast, isolated unit tests
|
||||||
|
│ ├── test_auth.py
|
||||||
|
│ ├── test_balance.py
|
||||||
|
│ ├── test_cost_calculation.py
|
||||||
|
│ ├── test_models.py
|
||||||
|
│ └── test_wallet.py
|
||||||
|
└── integration/ # Component integration tests
|
||||||
|
├── test_api_endpoints.py
|
||||||
|
├── test_payment_flow.py
|
||||||
|
├── test_proxy_streaming.py
|
||||||
|
└── test_real_mint.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Quick Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run unit tests only (fast)
|
||||||
|
make test-unit
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
make test-integration
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
make test-coverage
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
uv run pytest tests/unit/test_auth.py -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
uv run pytest tests/unit/test_auth.py::test_create_api_key -v
|
||||||
|
|
||||||
|
# Run tests matching pattern
|
||||||
|
uv run pytest -k "balance" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Markers
|
||||||
|
|
||||||
|
Use markers to categorize tests:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.slow
|
||||||
|
async def test_heavy_computation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.requires_docker
|
||||||
|
async def test_real_services():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Run without slow tests
|
||||||
|
pytest -m "not slow"
|
||||||
|
|
||||||
|
# Run only integration tests
|
||||||
|
pytest -m "integration"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
### Unit Test Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/unit/test_auth.py
|
||||||
|
import pytest
|
||||||
|
from routstr.auth import create_api_key, validate_api_key
|
||||||
|
|
||||||
|
class TestAPIKeyAuth:
|
||||||
|
"""Test API key authentication functionality"""
|
||||||
|
|
||||||
|
async def test_create_api_key(self, test_db):
|
||||||
|
"""Test creating a new API key"""
|
||||||
|
# Arrange
|
||||||
|
initial_balance = 10000
|
||||||
|
|
||||||
|
# Act
|
||||||
|
api_key = await create_api_key(
|
||||||
|
balance=initial_balance,
|
||||||
|
name="Test Key"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert api_key.key.startswith("rstr_")
|
||||||
|
assert len(api_key.key) == 32
|
||||||
|
assert api_key.balance == initial_balance
|
||||||
|
|
||||||
|
async def test_validate_invalid_key(self, test_db):
|
||||||
|
"""Test validation fails for invalid key"""
|
||||||
|
# Act & Assert
|
||||||
|
with pytest.raises(InvalidAPIKeyError):
|
||||||
|
await validate_api_key("invalid_key")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Test Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/integration/test_payment_flow.py
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
class TestPaymentFlow:
|
||||||
|
"""Test end-to-end payment flows"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_token_redemption_flow(
|
||||||
|
self,
|
||||||
|
app_client: AsyncClient,
|
||||||
|
mock_cashu_wallet
|
||||||
|
):
|
||||||
|
"""Test complete token redemption and API usage"""
|
||||||
|
# Arrange
|
||||||
|
token = create_test_token(amount=5000)
|
||||||
|
mock_cashu_wallet.redeem.return_value = 5000
|
||||||
|
|
||||||
|
# Act - Create API key
|
||||||
|
response = await app_client.post(
|
||||||
|
"/v1/wallet/create",
|
||||||
|
json={"cashu_token": token}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert - Key created
|
||||||
|
assert response.status_code == 200
|
||||||
|
api_key = response.json()["api_key"]
|
||||||
|
assert response.json()["balance"] == 5000
|
||||||
|
|
||||||
|
# Act - Use API key
|
||||||
|
response = await app_client.post(
|
||||||
|
"/v1/chat/completions",
|
||||||
|
headers={"Authorization": f"Bearer {api_key}"},
|
||||||
|
json={
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"messages": [{"role": "user", "content": "Hi"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert - Request successful
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "choices" in response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Fixtures
|
||||||
|
|
||||||
|
### Common Fixtures
|
||||||
|
|
||||||
|
Located in `tests/conftest.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_db():
|
||||||
|
"""Provide a clean test database"""
|
||||||
|
# Create in-memory SQLite database
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
|
|
||||||
|
async_session = sessionmaker(engine, class_=AsyncSession)
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def app_client(test_db):
|
||||||
|
"""Provide test client with test database"""
|
||||||
|
app.dependency_overrides[get_db] = lambda: test_db
|
||||||
|
|
||||||
|
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_cashu_wallet(mocker):
|
||||||
|
"""Mock Cashu wallet for testing"""
|
||||||
|
mock = mocker.patch("routstr.wallet.Wallet")
|
||||||
|
mock.return_value.redeem.return_value = 1000
|
||||||
|
return mock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Fixtures
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_with_fixtures(
|
||||||
|
test_db, # Get test database
|
||||||
|
app_client, # Get test HTTP client
|
||||||
|
mock_cashu_wallet # Get mocked wallet
|
||||||
|
):
|
||||||
|
# Use fixtures in test
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking Strategies
|
||||||
|
|
||||||
|
### Mocking External Services
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Mock upstream API
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_openai(mocker):
|
||||||
|
mock_response = mocker.Mock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"choices": [{
|
||||||
|
"message": {"content": "Hello!"},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 10,
|
||||||
|
"completion_tokens": 20,
|
||||||
|
"total_tokens": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"httpx.AsyncClient.post",
|
||||||
|
return_value=mock_response
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock Cashu mint
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mint(mocker):
|
||||||
|
mint = mocker.patch("routstr.wallet.Mint")
|
||||||
|
mint.return_value.check_proof_state.return_value = True
|
||||||
|
return mint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocking Time
|
||||||
|
|
||||||
|
```python
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
@freeze_time("2024-01-01 12:00:00")
|
||||||
|
async def test_time_dependent():
|
||||||
|
# Time is frozen during test
|
||||||
|
key = await create_api_key(expires_in_days=30)
|
||||||
|
assert key.expires_at == datetime(2024, 1, 31, 12, 0, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Patterns
|
||||||
|
|
||||||
|
### Testing Async Code
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Always mark async tests
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_function():
|
||||||
|
result = await async_operation()
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
# Test async context managers
|
||||||
|
async def test_async_context():
|
||||||
|
async with create_resource() as resource:
|
||||||
|
assert resource.is_active
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Exceptions
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test specific exception
|
||||||
|
async def test_raises_specific_error():
|
||||||
|
with pytest.raises(InsufficientBalanceError) as exc_info:
|
||||||
|
await deduct_balance(api_key, amount=999999)
|
||||||
|
|
||||||
|
assert "Insufficient balance" in str(exc_info.value)
|
||||||
|
assert exc_info.value.status_code == 402
|
||||||
|
|
||||||
|
# Test exception details
|
||||||
|
async def test_exception_details():
|
||||||
|
with pytest.raises(RoustrError) as exc_info:
|
||||||
|
await risky_operation()
|
||||||
|
|
||||||
|
error = exc_info.value
|
||||||
|
assert error.error_type == "validation_error"
|
||||||
|
assert error.detail == "Invalid input"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Streaming Responses
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_streaming_response():
|
||||||
|
"""Test streaming chat completion"""
|
||||||
|
chunks = []
|
||||||
|
|
||||||
|
async with app_client.stream(
|
||||||
|
"POST",
|
||||||
|
"/v1/chat/completions",
|
||||||
|
json={"model": "gpt-3.5-turbo", "stream": True}
|
||||||
|
) as response:
|
||||||
|
async for line in response.aiter_lines():
|
||||||
|
if line.startswith("data: "):
|
||||||
|
chunks.append(json.loads(line[6:]))
|
||||||
|
|
||||||
|
# Verify chunks
|
||||||
|
assert len(chunks) > 0
|
||||||
|
assert chunks[-1] == "[DONE]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_database_transaction(test_db):
|
||||||
|
"""Test atomic transactions"""
|
||||||
|
async with test_db.begin():
|
||||||
|
# Create test data
|
||||||
|
api_key = APIKey(key_hash="test", balance=1000)
|
||||||
|
test_db.add(api_key)
|
||||||
|
await test_db.flush()
|
||||||
|
|
||||||
|
# Test rollback
|
||||||
|
try:
|
||||||
|
async with test_db.begin_nested():
|
||||||
|
api_key.balance = -100 # Invalid
|
||||||
|
await test_db.flush()
|
||||||
|
raise ValueError("Rollback")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Verify rollback worked
|
||||||
|
await test_db.refresh(api_key)
|
||||||
|
assert api_key.balance == 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Testing
|
||||||
|
|
||||||
|
### Benchmark Tests
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_performance(benchmark):
|
||||||
|
"""Benchmark critical functions"""
|
||||||
|
result = benchmark(expensive_function, arg1, arg2)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
pytest --benchmark-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/integration/test_load.py
|
||||||
|
async def test_concurrent_requests(app_client):
|
||||||
|
"""Test handling multiple concurrent requests"""
|
||||||
|
async def make_request(i):
|
||||||
|
response = await app_client.get(f"/test/{i}")
|
||||||
|
return response.status_code
|
||||||
|
|
||||||
|
# Make 100 concurrent requests
|
||||||
|
tasks = [make_request(i) for i in range(100)]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# All should succeed
|
||||||
|
assert all(status == 200 for status in results)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
### Factories
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/factories.py
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
def create_test_api_key(**kwargs):
|
||||||
|
"""Factory for test API keys"""
|
||||||
|
defaults = {
|
||||||
|
"key": f"rstr_test_{uuid4().hex[:8]}",
|
||||||
|
"balance": 10000,
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"expires_at": datetime.utcnow() + timedelta(days=30)
|
||||||
|
}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return APIKey(**defaults)
|
||||||
|
|
||||||
|
def create_test_token(amount: int = 1000, mint: str = None):
|
||||||
|
"""Factory for test Cashu tokens"""
|
||||||
|
# Create valid test token structure
|
||||||
|
return base64.encode(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Constants
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/constants.py
|
||||||
|
TEST_MODELS = {
|
||||||
|
"gpt-3.5-turbo": {
|
||||||
|
"prompt": 0.0015,
|
||||||
|
"completion": 0.002
|
||||||
|
},
|
||||||
|
"gpt-4": {
|
||||||
|
"prompt": 0.03,
|
||||||
|
"completion": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_API_KEY = "rstr_test_1234567890"
|
||||||
|
TEST_MINT_URL = "https://testmint.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
### Running Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate coverage report
|
||||||
|
make test-coverage
|
||||||
|
|
||||||
|
# View HTML report
|
||||||
|
open htmlcov/index.html
|
||||||
|
|
||||||
|
# Coverage with specific tests
|
||||||
|
pytest --cov=routstr --cov-report=html tests/unit/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Configuration
|
||||||
|
|
||||||
|
In `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["routstr"]
|
||||||
|
omit = ["tests/*", "*/migrations/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if TYPE_CHECKING:"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
### Print Debugging
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Use -s flag to see print output
|
||||||
|
pytest -s tests/unit/test_auth.py
|
||||||
|
|
||||||
|
# In test
|
||||||
|
async def test_debug():
|
||||||
|
print(f"Value: {value}") # Will show with -s
|
||||||
|
assert value == expected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Debugging
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Drop into debugger on failure
|
||||||
|
pytest --pdb
|
||||||
|
|
||||||
|
# Set breakpoint in test
|
||||||
|
async def test_debug():
|
||||||
|
import pdb; pdb.set_trace()
|
||||||
|
# Execution stops here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging in Tests
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Enable debug logging in tests
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
# Or use caplog fixture
|
||||||
|
async def test_logging(caplog):
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
await function_that_logs()
|
||||||
|
|
||||||
|
assert "Expected message" in caplog.text
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
Tests run automatically on:
|
||||||
|
- Pull requests
|
||||||
|
- Pushes to main
|
||||||
|
- Nightly schedules
|
||||||
|
|
||||||
|
See `.github/workflows/test.yml` for configuration.
|
||||||
|
|
||||||
|
### Pre-commit Hooks
|
||||||
|
|
||||||
|
Install pre-commit hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Run manually
|
||||||
|
pre-commit run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Do's
|
||||||
|
|
||||||
|
1. ✅ Write tests first (TDD)
|
||||||
|
2. ✅ Keep tests simple and focused
|
||||||
|
3. ✅ Use descriptive test names
|
||||||
|
4. ✅ Test edge cases
|
||||||
|
5. ✅ Mock external dependencies
|
||||||
|
6. ✅ Use fixtures for setup
|
||||||
|
7. ✅ Assert specific values
|
||||||
|
|
||||||
|
### Don'ts
|
||||||
|
|
||||||
|
1. ❌ Don't test implementation details
|
||||||
|
2. ❌ Don't use production services
|
||||||
|
3. ❌ Don't rely on test order
|
||||||
|
4. ❌ Don't ignore flaky tests
|
||||||
|
5. ❌ Don't skip error cases
|
||||||
|
6. ❌ Don't use hard-coded waits
|
||||||
|
7. ❌ Don't commit commented tests
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Async Test Errors**
|
||||||
|
```python
|
||||||
|
# Wrong
|
||||||
|
def test_async(): # Missing async
|
||||||
|
await function()
|
||||||
|
|
||||||
|
# Right
|
||||||
|
async def test_async():
|
||||||
|
await function()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database State**
|
||||||
|
```python
|
||||||
|
# Ensure clean state
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def cleanup(test_db):
|
||||||
|
yield
|
||||||
|
# Cleanup after each test
|
||||||
|
await test_db.execute("DELETE FROM apikey")
|
||||||
|
await test_db.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mock Not Working**
|
||||||
|
```python
|
||||||
|
# Check import path
|
||||||
|
mocker.patch("routstr.wallet.Wallet") # Full path
|
||||||
|
# Not just "Wallet"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Review [Database Guide](database.md) for schema testing
|
||||||
|
- Check [Guidelines](guidelines.md) for code standards
|
||||||
|
- See [Architecture](architecture.md) for system design
|
||||||
|
- Read [Setup Guide](setup.md) for environment setup
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
Routstr Core is configured through environment variables. This guide covers all available options.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Core Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `UPSTREAM_BASE_URL` | Base URL of the OpenAI-compatible API to proxy | - | ✅ |
|
||||||
|
| `UPSTREAM_API_KEY` | API key for the upstream service | - | ❌ |
|
||||||
|
| `DATABASE_URL` | SQLite database connection string | `sqlite+aiosqlite:///keys.db` | ❌ |
|
||||||
|
| `ADMIN_PASSWORD` | Password for admin dashboard access | - | ⚠️ |
|
||||||
|
|
||||||
|
### Node Information
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `NAME` | Public name of your Routstr node | `ARoutstrNode` | ❌ |
|
||||||
|
| `DESCRIPTION` | Description of your node | `A Routstr Node` | ❌ |
|
||||||
|
| `NPUB` | Nostr public key for node identity | - | ❌ |
|
||||||
|
| `HTTP_URL` | Public HTTP URL of your node | - | ❌ |
|
||||||
|
| `ONION_URL` | Tor hidden service URL | - | ❌ |
|
||||||
|
|
||||||
|
### Cashu Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `CASHU_MINTS` | Comma-separated list of trusted Cashu mint URLs | `https://mint.minibits.cash/Bitcoin` | ❌ |
|
||||||
|
| `RECEIVE_LN_ADDRESS` | Lightning address for automatic payouts | - | ❌ |
|
||||||
|
|
||||||
|
### Pricing Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `MODEL_BASED_PRICING` | Enable model-specific pricing from models.json | `false` | ❌ |
|
||||||
|
| `COST_PER_REQUEST` | Fixed cost per API request in sats | `1` | ❌ |
|
||||||
|
| `COST_PER_1K_INPUT_TOKENS` | Cost per 1000 input tokens in sats | `0` | ❌ |
|
||||||
|
| `COST_PER_1K_OUTPUT_TOKENS` | Cost per 1000 output tokens in sats | `0` | ❌ |
|
||||||
|
| `EXCHANGE_FEE` | Exchange rate markup (1.005 = 0.5% fee) | `1.005` | ❌ |
|
||||||
|
| `UPSTREAM_PROVIDER_FEE` | Provider fee markup (1.05 = 5% fee) | `1.05` | ❌ |
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `CORS_ORIGINS` | Comma-separated list of allowed CORS origins | `*` | ❌ |
|
||||||
|
| `TOR_PROXY_URL` | SOCKS5 proxy URL for Tor connections | `socks5://127.0.0.1:9050` | ❌ |
|
||||||
|
|
||||||
|
### Logging Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `INFO` | ❌ |
|
||||||
|
| `ENABLE_CONSOLE_LOGGING` | Enable console log output | `true` | ❌ |
|
||||||
|
|
||||||
|
### Model Management
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `MODELS_PATH` | Path to custom models.json file | `models.json` | ❌ |
|
||||||
|
| `BASE_URL` | Base URL for fetching model info | `https://openrouter.ai/api/v1` | ❌ |
|
||||||
|
| `SOURCE` | Filter models by source provider | - | ❌ |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Basic OpenAI Proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
UPSTREAM_BASE_URL=https://api.openai.com/v1
|
||||||
|
UPSTREAM_API_KEY=sk-...
|
||||||
|
ADMIN_PASSWORD=my-secure-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom AI Provider
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
UPSTREAM_BASE_URL=https://api.anthropic.com/v1
|
||||||
|
UPSTREAM_API_KEY=your-anthropic-key
|
||||||
|
MODEL_BASED_PRICING=true
|
||||||
|
MODELS_PATH=/app/config/anthropic-models.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### High-Security Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
UPSTREAM_BASE_URL=https://api.openai.com/v1
|
||||||
|
UPSTREAM_API_KEY=sk-...
|
||||||
|
ADMIN_PASSWORD=very-long-secure-password-here
|
||||||
|
CORS_ORIGINS=https://myapp.com,https://app.myapp.com
|
||||||
|
TOR_PROXY_URL=socks5://tor:9050
|
||||||
|
LOG_LEVEL=WARNING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public Node Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
NAME=Lightning AI Gateway
|
||||||
|
DESCRIPTION=Fast and reliable AI API access with Bitcoin payments
|
||||||
|
NPUB=npub1abcd...
|
||||||
|
HTTP_URL=https://api.lightning-ai.com
|
||||||
|
ONION_URL=http://lightningai.onion
|
||||||
|
CASHU_MINTS=https://mint1.com,https://mint2.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pricing Models
|
||||||
|
|
||||||
|
### Fixed Pricing
|
||||||
|
|
||||||
|
Simple per-request pricing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=false
|
||||||
|
COST_PER_REQUEST=10 # 10 sats per request
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token-Based Pricing
|
||||||
|
|
||||||
|
Charge based on token usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=false
|
||||||
|
COST_PER_REQUEST=1 # 1 sat base fee
|
||||||
|
COST_PER_1K_INPUT_TOKENS=5 # 5 sats per 1k input
|
||||||
|
COST_PER_1K_OUTPUT_TOKENS=15 # 15 sats per 1k output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model-Based Pricing
|
||||||
|
|
||||||
|
Use dynamic pricing from models.json:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=true
|
||||||
|
EXCHANGE_FEE=1.01 # 1% exchange fee
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.00 # No additional markup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Models Configuration
|
||||||
|
|
||||||
|
Create a `models.json` file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-4",
|
||||||
|
"name": "GPT-4",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.00003",
|
||||||
|
"completion": "0.00006",
|
||||||
|
"request": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-3.5-turbo",
|
||||||
|
"name": "GPT-3.5 Turbo",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.0000015",
|
||||||
|
"completion": "0.000002",
|
||||||
|
"request": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### Admin Password
|
||||||
|
|
||||||
|
Generate a strong password:
|
||||||
|
```bash
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
- Rotate upstream API keys regularly
|
||||||
|
- Use read-only keys when possible
|
||||||
|
- Monitor key usage
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- Restrict CORS origins in production
|
||||||
|
- Use HTTPS for public endpoints
|
||||||
|
- Enable Tor for anonymity
|
||||||
|
|
||||||
|
### Database Security
|
||||||
|
|
||||||
|
- Regular backups
|
||||||
|
- Encrypted storage volumes
|
||||||
|
- Restricted file permissions
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check Current Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all environment variables
|
||||||
|
docker exec routstr env | sort
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
curl http://localhost:8000/v1/info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Missing Upstream URL**
|
||||||
|
```
|
||||||
|
ERROR: UPSTREAM_BASE_URL not set
|
||||||
|
Solution: Set UPSTREAM_BASE_URL in .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Invalid Cashu Mint**
|
||||||
|
```
|
||||||
|
ERROR: Failed to connect to mint
|
||||||
|
Solution: Verify CASHU_MINTS URLs are accessible
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Errors**
|
||||||
|
```
|
||||||
|
ERROR: Database connection failed
|
||||||
|
Solution: Check DATABASE_URL and file permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Multiple Mints
|
||||||
|
|
||||||
|
Configure fallback mints:
|
||||||
|
```bash
|
||||||
|
CASHU_MINTS=https://primary.mint,https://backup1.mint,https://backup2.mint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Database
|
||||||
|
|
||||||
|
Use PostgreSQL instead of SQLite:
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/routstr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proxy Settings
|
||||||
|
|
||||||
|
For corporate environments:
|
||||||
|
```bash
|
||||||
|
HTTP_PROXY=http://proxy.company.com:8080
|
||||||
|
HTTPS_PROXY=http://proxy.company.com:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [User Guide](../user-guide/introduction.md) - Start using Routstr
|
||||||
|
- [Admin Dashboard](../user-guide/admin-dashboard.md) - Manage your node
|
||||||
|
- [Custom Pricing](../advanced/custom-pricing.md) - Advanced pricing strategies
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# Docker Setup
|
||||||
|
|
||||||
|
This guide covers deploying Routstr Core using Docker for production environments.
|
||||||
|
|
||||||
|
## Docker Images
|
||||||
|
|
||||||
|
Official images are available on GitHub Container Registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghcr.io/routstr/proxy:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Docker Run
|
||||||
|
|
||||||
|
### Minimal Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name routstr \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-e UPSTREAM_BASE_URL=https://api.openai.com/v1 \
|
||||||
|
-e UPSTREAM_API_KEY=sk-... \
|
||||||
|
-e ADMIN_PASSWORD=secure-password \
|
||||||
|
ghcr.io/routstr/proxy:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Persistent Storage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name routstr \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-v routstr-data:/app/data \
|
||||||
|
-v routstr-logs:/app/logs \
|
||||||
|
-e UPSTREAM_BASE_URL=https://api.openai.com/v1 \
|
||||||
|
-e UPSTREAM_API_KEY=sk-... \
|
||||||
|
-e DATABASE_URL=sqlite+aiosqlite:///data/keys.db \
|
||||||
|
ghcr.io/routstr/proxy:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose Setup
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
Create `compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
routstr:
|
||||||
|
image: ghcr.io/routstr/proxy:latest
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Tor Hidden Service
|
||||||
|
|
||||||
|
The included `compose.yml` provides Tor support:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
routstr:
|
||||||
|
build: . # Or use image: ghcr.io/routstr/proxy:latest
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ./logs:/app/logs
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TOR_PROXY_URL=socks5://tor:9050
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
tor:
|
||||||
|
image: ghcr.io/hundehausen/tor-hidden-service:latest
|
||||||
|
volumes:
|
||||||
|
- tor-data:/var/lib/tor
|
||||||
|
environment:
|
||||||
|
- HS_ROUTER=routstr:8000:80
|
||||||
|
depends_on:
|
||||||
|
- routstr
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tor-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment File
|
||||||
|
|
||||||
|
Create `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
UPSTREAM_BASE_URL=https://api.openai.com/v1
|
||||||
|
UPSTREAM_API_KEY=your-api-key
|
||||||
|
ADMIN_PASSWORD=secure-admin-password
|
||||||
|
|
||||||
|
# Cashu Configuration
|
||||||
|
CASHU_MINTS=https://mint.minibits.cash/Bitcoin
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
NAME=My Routstr Node
|
||||||
|
DESCRIPTION=Pay-per-use AI API proxy
|
||||||
|
NPUB=npub1...
|
||||||
|
HTTP_URL=https://api.mynode.com
|
||||||
|
ONION_URL=http://mynode.onion
|
||||||
|
|
||||||
|
# Pricing
|
||||||
|
MODEL_BASED_PRICING=true
|
||||||
|
EXCHANGE_FEE=1.005
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.05
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building Custom Image
|
||||||
|
|
||||||
|
### Dockerfile Overview
|
||||||
|
|
||||||
|
The provided Dockerfile:
|
||||||
|
- Uses Alpine Linux for small size
|
||||||
|
- Installs required dependencies for secp256k1
|
||||||
|
- Runs as non-root user
|
||||||
|
- Exposes port 8000
|
||||||
|
|
||||||
|
### Build Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/routstr/routstr-core.git
|
||||||
|
cd routstr-core
|
||||||
|
|
||||||
|
# Build image
|
||||||
|
docker build -t my-routstr:latest .
|
||||||
|
|
||||||
|
# Run custom image
|
||||||
|
docker run -d \
|
||||||
|
--name routstr \
|
||||||
|
-p 8000:8000 \
|
||||||
|
--env-file .env \
|
||||||
|
my-routstr:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Considerations
|
||||||
|
|
||||||
|
### Resource Requirements
|
||||||
|
|
||||||
|
- **CPU**: 1-2 cores recommended
|
||||||
|
- **Memory**: 512MB-1GB
|
||||||
|
- **Storage**: 1GB + database growth
|
||||||
|
- **Network**: Low latency to upstream provider
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
Add health check to compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
routstr:
|
||||||
|
# ... other config ...
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/v1/info"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse Proxy Setup
|
||||||
|
|
||||||
|
#### Nginx Example
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name api.yournode.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# For streaming responses
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Caddy Example
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
api.yournode.com {
|
||||||
|
reverse_proxy localhost:8000 {
|
||||||
|
flush_interval -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Log Management
|
||||||
|
|
||||||
|
View logs:
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
docker logs -f routstr
|
||||||
|
|
||||||
|
# Docker Compose
|
||||||
|
docker compose logs -f routstr
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
tail -f ./logs/routstr.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
Monitor key metrics:
|
||||||
|
- Request count and latency
|
||||||
|
- Token validation success rate
|
||||||
|
- Upstream API errors
|
||||||
|
- Database size growth
|
||||||
|
|
||||||
|
## Backup and Recovery
|
||||||
|
|
||||||
|
### Database Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup SQLite database
|
||||||
|
docker exec routstr sqlite3 /app/data/keys.db ".backup /app/data/backup.db"
|
||||||
|
|
||||||
|
# Copy backup locally
|
||||||
|
docker cp routstr:/app/data/backup.db ./backup-$(date +%Y%m%d).db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore from Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop service
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
docker cp ./backup.db routstr:/app/data/keys.db
|
||||||
|
|
||||||
|
# Restart service
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- Never commit `.env` files
|
||||||
|
- Use Docker secrets for sensitive data
|
||||||
|
- Rotate API keys regularly
|
||||||
|
- Use strong admin passwords
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- Use HTTPS/TLS termination
|
||||||
|
- Restrict admin interface access
|
||||||
|
- Enable firewall rules
|
||||||
|
- Monitor for suspicious activity
|
||||||
|
|
||||||
|
### Container Security
|
||||||
|
|
||||||
|
- Run as non-root user
|
||||||
|
- Use read-only filesystem where possible
|
||||||
|
- Limit container capabilities
|
||||||
|
- Keep base image updated
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker logs routstr
|
||||||
|
|
||||||
|
# Verify environment
|
||||||
|
docker exec routstr env | grep -E "(UPSTREAM|CASHU|ADMIN)"
|
||||||
|
|
||||||
|
# Test database connection
|
||||||
|
docker exec routstr sqlite3 /app/data/keys.db ".tables"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix volume permissions
|
||||||
|
sudo chown -R 1000:1000 ./data ./logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test upstream connectivity
|
||||||
|
docker exec routstr curl -I https://api.openai.com
|
||||||
|
|
||||||
|
# Check DNS resolution
|
||||||
|
docker exec routstr nslookup api.openai.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [ ] Set strong `ADMIN_PASSWORD`
|
||||||
|
- [ ] Configure proper `UPSTREAM_BASE_URL` and `UPSTREAM_API_KEY`
|
||||||
|
- [ ] Set up persistent volumes for data and logs
|
||||||
|
- [ ] Configure reverse proxy with TLS
|
||||||
|
- [ ] Set up monitoring and alerting
|
||||||
|
- [ ] Implement backup strategy
|
||||||
|
- [ ] Test disaster recovery
|
||||||
|
- [ ] Document deployment process
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Configuration Guide](configuration.md) - All environment variables
|
||||||
|
- [Admin Dashboard](../user-guide/admin-dashboard.md) - Manage your node
|
||||||
|
- [Monitoring](../advanced/monitoring.md) - Set up observability
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
Routstr Core is a powerful payment proxy that brings Bitcoin micropayments to AI APIs. This overview will help you understand the core concepts and architecture.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Payment Proxy
|
||||||
|
|
||||||
|
Routstr acts as a transparent proxy between your application and OpenAI-compatible APIs. It:
|
||||||
|
|
||||||
|
- Intercepts API requests
|
||||||
|
- Validates payment tokens
|
||||||
|
- Forwards requests to the upstream provider
|
||||||
|
- Tracks usage and deducts costs
|
||||||
|
- Returns responses to the client
|
||||||
|
|
||||||
|
### Cashu eCash Protocol
|
||||||
|
|
||||||
|
[Cashu](https://cashu.space) is a Bitcoin eCash protocol that enables:
|
||||||
|
|
||||||
|
- **Privacy**: Payments are unlinkable and untraceable
|
||||||
|
- **Instant Settlement**: No waiting for blockchain confirmations
|
||||||
|
- **Micropayments**: Send fractions of a satoshi
|
||||||
|
- **Offline Capability**: Tokens can be transferred without internet
|
||||||
|
|
||||||
|
### Lightning Network Integration
|
||||||
|
|
||||||
|
Routstr connects to the Lightning Network through Cashu mints, enabling:
|
||||||
|
|
||||||
|
- Fast Bitcoin deposits and withdrawals
|
||||||
|
- Global payment reach
|
||||||
|
- Low transaction fees
|
||||||
|
- No minimum payment amounts
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### System Components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Client Side"
|
||||||
|
A[AI Application]
|
||||||
|
B[OpenAI SDK]
|
||||||
|
C[eCash Wallet]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Routstr Core"
|
||||||
|
D[FastAPI Server]
|
||||||
|
E[Auth Module]
|
||||||
|
F[Payment Module]
|
||||||
|
G[Proxy Module]
|
||||||
|
H[SQLite Database]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "External Services"
|
||||||
|
I[Upstream AI Provider]
|
||||||
|
J[Cashu Mint]
|
||||||
|
K[Bitcoin/Lightning]
|
||||||
|
end
|
||||||
|
|
||||||
|
A --> B
|
||||||
|
B --> D
|
||||||
|
C --> D
|
||||||
|
D --> E
|
||||||
|
E --> F
|
||||||
|
F --> J
|
||||||
|
D --> G
|
||||||
|
G --> I
|
||||||
|
D --> H
|
||||||
|
J --> K
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Modules
|
||||||
|
|
||||||
|
1. **Authentication** (`auth.py`)
|
||||||
|
- API key validation
|
||||||
|
- Balance checking
|
||||||
|
- Request authorization
|
||||||
|
|
||||||
|
2. **Payment Processing** (`payment/`)
|
||||||
|
- Token validation
|
||||||
|
- Cost calculation
|
||||||
|
- Balance updates
|
||||||
|
- Pricing models
|
||||||
|
|
||||||
|
3. **Proxy Handler** (`proxy.py`)
|
||||||
|
- Request forwarding
|
||||||
|
- Response streaming
|
||||||
|
- Usage tracking
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
4. **Wallet Management** (`wallet.py`)
|
||||||
|
- Cashu wallet integration
|
||||||
|
- Token redemption
|
||||||
|
- Balance management
|
||||||
|
- Automatic payouts
|
||||||
|
|
||||||
|
5. **Admin Interface** (`core/admin.py`)
|
||||||
|
- Web dashboard
|
||||||
|
- Balance viewing
|
||||||
|
- Key management
|
||||||
|
- Withdrawal interface
|
||||||
|
|
||||||
|
## Payment Flow
|
||||||
|
|
||||||
|
### Standard Flow (API Key)
|
||||||
|
|
||||||
|
1. User deposits eCash tokens to create an API key
|
||||||
|
2. Client sends requests with the API key
|
||||||
|
3. Routstr checks balance and forwards request
|
||||||
|
4. Cost is deducted based on actual usage
|
||||||
|
5. Response is returned to client
|
||||||
|
|
||||||
|
### Per-Request Flow (Coming Soon)
|
||||||
|
|
||||||
|
1. Client includes eCash token in request header
|
||||||
|
2. Routstr validates token meets minimum amount
|
||||||
|
3. Request is processed
|
||||||
|
4. Change is returned in response header
|
||||||
|
5. No account or balance needed
|
||||||
|
|
||||||
|
## Supported Features
|
||||||
|
|
||||||
|
### API Compatibility
|
||||||
|
|
||||||
|
- ✅ Chat completions (streaming and non-streaming)
|
||||||
|
- ✅ Text completions
|
||||||
|
- ✅ Embeddings
|
||||||
|
- ✅ Image generation
|
||||||
|
- ✅ Audio transcription/translation
|
||||||
|
- ✅ Model listing
|
||||||
|
- ✅ Custom endpoints
|
||||||
|
|
||||||
|
### Payment Features
|
||||||
|
|
||||||
|
- ✅ Multiple Cashu mint support
|
||||||
|
- ✅ Automatic balance tracking
|
||||||
|
- ✅ Model-based pricing
|
||||||
|
- ✅ USD to BTC conversion
|
||||||
|
- ✅ Configurable fees
|
||||||
|
- ✅ Balance withdrawals
|
||||||
|
|
||||||
|
### Operational Features
|
||||||
|
|
||||||
|
- ✅ Docker deployment
|
||||||
|
- ✅ Tor hidden service support
|
||||||
|
- ✅ Nostr relay discovery
|
||||||
|
- ✅ Database migrations
|
||||||
|
- ✅ Comprehensive logging
|
||||||
|
- ✅ Admin dashboard
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Quick Start](quickstart.md) - Get running in minutes
|
||||||
|
- [Docker Setup](docker.md) - Deploy with containers
|
||||||
|
- [Configuration](configuration.md) - Customize your instance
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
Get Routstr Core up and running in minutes with Docker or local development setup.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose (for production)
|
||||||
|
- Python 3.11+ (for development)
|
||||||
|
- A Cashu-compatible wallet (optional for testing)
|
||||||
|
|
||||||
|
## Option 1: Docker (Recommended)
|
||||||
|
|
||||||
|
### Quick Run
|
||||||
|
|
||||||
|
The fastest way to start Routstr Core:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name routstr-proxy \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-e UPSTREAM_BASE_URL=https://api.openai.com/v1 \
|
||||||
|
-e UPSTREAM_API_KEY=your-openai-api-key \
|
||||||
|
ghcr.io/routstr/proxy:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
For a full setup with Tor support:
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/routstr/routstr-core.git
|
||||||
|
cd routstr-core
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create environment file:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the services:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start:
|
||||||
|
- Routstr proxy on port 8000
|
||||||
|
- Tor hidden service (optional)
|
||||||
|
- Automatic database migrations
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
Check that Routstr is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/v1/info
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "ARoutstrNode",
|
||||||
|
"description": "A Routstr Node",
|
||||||
|
"version": "0.1.1b",
|
||||||
|
"npub": "",
|
||||||
|
"mints": ["https://mint.minibits.cash/Bitcoin"],
|
||||||
|
"models": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option 2: Local Development
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
1. Install [uv](https://github.com/astral-sh/uv) package manager:
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Clone and setup:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/routstr/routstr-core.git
|
||||||
|
cd routstr-core
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure environment:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fastapi run routstr --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## First API Call
|
||||||
|
|
||||||
|
### 1. Get an eCash Token
|
||||||
|
|
||||||
|
You'll need a Cashu token to pay for API calls. Options:
|
||||||
|
|
||||||
|
- Use a [Cashu wallet](https://cashu.space) to create tokens
|
||||||
|
- Get test tokens from a testnet mint
|
||||||
|
- Use the example token (for testing only)
|
||||||
|
|
||||||
|
### 2. Create an API Key
|
||||||
|
|
||||||
|
Send your eCash token to create an API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/wallet/create \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"cashu_token": "cashuAeyJ0b2..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "rUvK7...",
|
||||||
|
"balance": 10000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Make an API Call
|
||||||
|
|
||||||
|
Use your API key like a normal OpenAI key:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import openai
|
||||||
|
|
||||||
|
client = openai.OpenAI(
|
||||||
|
api_key="rUvK7...", # Your Routstr API key
|
||||||
|
base_url="http://localhost:8000/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[{"role": "user", "content": "Hello!"}]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Client
|
||||||
|
|
||||||
|
Run the included example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CASHU_TOKEN="your-token" python example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This demonstrates:
|
||||||
|
- Creating an API key from a token
|
||||||
|
- Making streaming chat requests
|
||||||
|
- Automatic balance deduction
|
||||||
|
|
||||||
|
## Testing the Setup
|
||||||
|
|
||||||
|
### Check Available Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Admin Dashboard
|
||||||
|
|
||||||
|
Open http://localhost:8000/admin/ in your browser.
|
||||||
|
|
||||||
|
Default password is set in `ADMIN_PASSWORD` environment variable.
|
||||||
|
|
||||||
|
### Monitor Logs
|
||||||
|
|
||||||
|
Docker:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f routstr
|
||||||
|
```
|
||||||
|
|
||||||
|
Local:
|
||||||
|
```bash
|
||||||
|
# Logs are in ./logs/ directory
|
||||||
|
tail -f logs/routstr.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Connection Refused
|
||||||
|
|
||||||
|
- Ensure the service is running: `docker ps`
|
||||||
|
- Check firewall settings
|
||||||
|
- Verify port 8000 is not in use
|
||||||
|
|
||||||
|
### Invalid API Key
|
||||||
|
|
||||||
|
- Ensure you've created an API key with sufficient balance
|
||||||
|
- Check the token was valid and had value
|
||||||
|
- Verify the mint URL is accessible
|
||||||
|
|
||||||
|
### Upstream Errors
|
||||||
|
|
||||||
|
- Check `UPSTREAM_BASE_URL` is correct
|
||||||
|
- Verify `UPSTREAM_API_KEY` if required
|
||||||
|
- Test upstream service directly
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Configuration Guide](configuration.md) - Customize settings
|
||||||
|
- [Docker Setup](docker.md) - Production deployment
|
||||||
|
- [User Guide](../user-guide/introduction.md) - Detailed usage
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Routstr Core Documentation
|
||||||
|
|
||||||
|
Welcome to the official documentation for **Routstr Core** - a FastAPI-based reverse proxy that enables Bitcoin micropayments for OpenAI-compatible APIs using the Cashu eCash protocol.
|
||||||
|
|
||||||
|
## What is Routstr Core?
|
||||||
|
|
||||||
|
Routstr Core is a payment proxy that sits between API clients and OpenAI-compatible services. It enables:
|
||||||
|
|
||||||
|
- **Pay-per-request billing** using Bitcoin eCash tokens
|
||||||
|
- **Seamless integration** with existing OpenAI clients
|
||||||
|
- **Privacy-preserving payments** through the Cashu protocol
|
||||||
|
- **Flexible pricing models** with per-token or per-request billing
|
||||||
|
- **Multi-provider support** for various AI model providers
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- 🪙 **Cashu Wallet Integration** - Accept Lightning payments and redeem eCash tokens
|
||||||
|
- 🔑 **API Key Management** - Secure key storage with balance tracking
|
||||||
|
- 💰 **Dynamic Pricing** - Model-based pricing with live BTC/USD conversion
|
||||||
|
- 🎛️ **Admin Dashboard** - Web interface for balance and key management
|
||||||
|
- 🌐 **Nostr Discovery** - Find providers through decentralized relay network
|
||||||
|
- 🐋 **Docker Support** - Easy deployment with optional Tor hidden service
|
||||||
|
- ⚡ **Lightning Fast** - Minimal latency overhead for API requests
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Routstr as Routstr Proxy
|
||||||
|
participant DB as Database
|
||||||
|
participant Upstream as AI Provider
|
||||||
|
participant Wallet as Cashu Wallet
|
||||||
|
|
||||||
|
Client->>Routstr: API Request + eCash Token
|
||||||
|
Routstr->>Wallet: Validate & Redeem Token
|
||||||
|
Wallet-->>Routstr: Token Value (sats)
|
||||||
|
Routstr->>DB: Store/Update Balance
|
||||||
|
Routstr->>Upstream: Forward API Request
|
||||||
|
Upstream-->>Routstr: API Response + Usage Data
|
||||||
|
Routstr->>DB: Deduct Actual Cost
|
||||||
|
Routstr-->>Client: API Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
|
- :rocket: **[Quick Start](getting-started/quickstart.md)**
|
||||||
|
|
||||||
|
Get up and running with Docker in minutes
|
||||||
|
|
||||||
|
- :gear: **[Configuration](getting-started/configuration.md)**
|
||||||
|
|
||||||
|
Learn about environment variables and settings
|
||||||
|
|
||||||
|
- :book: **[User Guide](user-guide/introduction.md)**
|
||||||
|
|
||||||
|
Comprehensive guide for using Routstr
|
||||||
|
|
||||||
|
- :hammer: **[Contributing](contributing/setup.md)**
|
||||||
|
|
||||||
|
Help improve Routstr Core
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **AI Application Developers** - Add Bitcoin payments to your AI apps without managing infrastructure
|
||||||
|
- **API Resellers** - Resell API access with custom pricing and profit margins
|
||||||
|
- **Privacy-Focused Users** - Access AI models without revealing personal information
|
||||||
|
- **Micropayment Experiments** - Test new business models with instant, small payments
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- 📖 Browse the [User Guide](user-guide/introduction.md) for detailed usage instructions
|
||||||
|
- 🐛 Report issues on [GitHub](https://github.com/routstr/routstr-core/issues)
|
||||||
|
- 💬 Join the community discussions
|
||||||
|
- 🔧 Check the [API Reference](api/overview.md) for technical details
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Routstr Core is open source software licensed under the GPLv3. See the [LICENSE](https://github.com/routstr/routstr-core/blob/main/LICENSE) file for details.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
mkdocs>=1.5.3
|
||||||
|
mkdocs-material>=9.5.0
|
||||||
|
mkdocs-mermaid2-plugin>=1.1.1
|
||||||
|
pymdown-extensions>=10.5
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
# Admin Dashboard
|
||||||
|
|
||||||
|
The Routstr admin dashboard provides a web interface for managing your node, viewing balances, and handling withdrawals.
|
||||||
|
|
||||||
|
## Accessing the Dashboard
|
||||||
|
|
||||||
|
### URL Format
|
||||||
|
|
||||||
|
The admin dashboard is available at:
|
||||||
|
```
|
||||||
|
https://your-routstr-node.com/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important**: Always include the trailing slash (`/`) in the URL.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
The dashboard is protected by a password set in the `ADMIN_PASSWORD` environment variable.
|
||||||
|
|
||||||
|
1. Navigate to `/admin/`
|
||||||
|
2. Enter the admin password
|
||||||
|
3. Click "Login"
|
||||||
|
|
||||||
|
The password is stored as a secure cookie for the session.
|
||||||
|
|
||||||
|
## Dashboard Overview
|
||||||
|
|
||||||
|
### Main Interface
|
||||||
|
|
||||||
|
The dashboard displays:
|
||||||
|
|
||||||
|
- **Node Information**
|
||||||
|
- Node name and description
|
||||||
|
- Version number
|
||||||
|
- Public URLs (HTTP and Onion)
|
||||||
|
- Supported Cashu mints
|
||||||
|
|
||||||
|
- **Statistics**
|
||||||
|
- Total API keys
|
||||||
|
- Active keys
|
||||||
|
- Total balance across all keys
|
||||||
|
- Recent activity
|
||||||
|
|
||||||
|
- **API Key List**
|
||||||
|
- All keys with balances
|
||||||
|
- Usage statistics
|
||||||
|
- Management options
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Viewing API Keys
|
||||||
|
|
||||||
|
The main table shows all API keys with:
|
||||||
|
|
||||||
|
| Column | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| API Key | Masked key (first/last 4 chars) |
|
||||||
|
| Balance | Current balance in sats |
|
||||||
|
| Created | Creation timestamp |
|
||||||
|
| Last Used | Most recent API call |
|
||||||
|
| Total Spent | Lifetime usage |
|
||||||
|
| Status | Active/Expired/Disabled |
|
||||||
|
|
||||||
|
### Searching and Filtering
|
||||||
|
|
||||||
|
- **Search**: Find keys by partial match
|
||||||
|
- **Sort**: Click column headers to sort
|
||||||
|
- **Filter**: Show only active/expired keys
|
||||||
|
- **Export**: Download data as CSV
|
||||||
|
|
||||||
|
### Key Details
|
||||||
|
|
||||||
|
Click on any key to view:
|
||||||
|
|
||||||
|
- Full API key (masked by default)
|
||||||
|
- Complete transaction history
|
||||||
|
- Usage graphs
|
||||||
|
- Metadata (name, expiry, refund address)
|
||||||
|
|
||||||
|
## Balance Management
|
||||||
|
|
||||||
|
### Viewing Balances
|
||||||
|
|
||||||
|
Balances are displayed in multiple units:
|
||||||
|
- **Sats**: Standard satoshi units
|
||||||
|
- **mSats**: Millisatoshis (internal precision)
|
||||||
|
- **BTC**: Bitcoin decimal format
|
||||||
|
- **USD**: Approximate USD value
|
||||||
|
|
||||||
|
### Balance History
|
||||||
|
|
||||||
|
View balance changes over time:
|
||||||
|
```
|
||||||
|
Time | Type | Amount | Balance | Description
|
||||||
|
-------------|-----------|---------|---------|-------------
|
||||||
|
12:34:56 | Deposit | +10,000 | 10,000 | Token redemption
|
||||||
|
12:35:12 | Usage | -154 | 9,846 | gpt-3.5-turbo call
|
||||||
|
12:36:45 | Usage | -210 | 9,636 | gpt-4 call
|
||||||
|
```
|
||||||
|
|
||||||
|
## Withdrawals
|
||||||
|
|
||||||
|
### Manual Withdrawal
|
||||||
|
|
||||||
|
To withdraw funds from an API key:
|
||||||
|
|
||||||
|
1. Click "Withdraw" next to the key
|
||||||
|
2. Optionally specify amount (default: full balance)
|
||||||
|
3. Select target Cashu mint
|
||||||
|
4. Click "Generate Token"
|
||||||
|
5. Copy the eCash token
|
||||||
|
6. Redeem in your Cashu wallet
|
||||||
|
|
||||||
|
### Bulk Operations
|
||||||
|
|
||||||
|
For multiple withdrawals:
|
||||||
|
|
||||||
|
1. Select keys using checkboxes
|
||||||
|
2. Click "Bulk Actions" → "Withdraw"
|
||||||
|
3. Tokens are generated for each key
|
||||||
|
4. Download all tokens as text file
|
||||||
|
|
||||||
|
### Automatic Withdrawals
|
||||||
|
|
||||||
|
If configured with `RECEIVE_LN_ADDRESS`:
|
||||||
|
- Balances above threshold auto-convert to Lightning
|
||||||
|
- Sent to configured Lightning address
|
||||||
|
- View payout history in dashboard
|
||||||
|
|
||||||
|
## Node Configuration
|
||||||
|
|
||||||
|
### Viewing Settings
|
||||||
|
|
||||||
|
Current node configuration is displayed:
|
||||||
|
- Upstream provider URL
|
||||||
|
- Enabled features
|
||||||
|
- Pricing model
|
||||||
|
- Fee structure
|
||||||
|
|
||||||
|
### Models and Pricing
|
||||||
|
|
||||||
|
View supported models and their pricing:
|
||||||
|
|
||||||
|
| Model | Input $/1K | Output $/1K | Sats/1K |
|
||||||
|
|-------|------------|-------------|---------|
|
||||||
|
| gpt-3.5-turbo | $0.0015 | $0.002 | 3/4 |
|
||||||
|
| gpt-4 | $0.03 | $0.06 | 60/120 |
|
||||||
|
| dall-e-3 | - | - | 1000/image |
|
||||||
|
|
||||||
|
### Updating Configuration
|
||||||
|
|
||||||
|
> **Note**: Configuration changes require node restart.
|
||||||
|
|
||||||
|
To update settings:
|
||||||
|
1. Modify environment variables
|
||||||
|
2. Restart the node
|
||||||
|
3. Verify changes in dashboard
|
||||||
|
|
||||||
|
## Analytics
|
||||||
|
|
||||||
|
### Usage Statistics
|
||||||
|
|
||||||
|
View comprehensive usage data:
|
||||||
|
|
||||||
|
- **Requests per Day**: Line graph
|
||||||
|
- **Token Usage**: Stacked bar chart
|
||||||
|
- **Model Distribution**: Pie chart
|
||||||
|
- **Cost Analysis**: Breakdown by model
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
|
||||||
|
Monitor node performance:
|
||||||
|
- Average response time
|
||||||
|
- Request success rate
|
||||||
|
- Upstream API latency
|
||||||
|
- Cache hit ratio
|
||||||
|
|
||||||
|
### Export Data
|
||||||
|
|
||||||
|
Export analytics data:
|
||||||
|
1. Select date range
|
||||||
|
2. Choose metrics
|
||||||
|
3. Click "Export"
|
||||||
|
4. Download as CSV/JSON
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
- Password protection
|
||||||
|
- Session timeout (configurable)
|
||||||
|
- IP allowlisting (optional)
|
||||||
|
- Audit logging
|
||||||
|
|
||||||
|
### Security Log
|
||||||
|
|
||||||
|
View security events:
|
||||||
|
```
|
||||||
|
2024-01-15 12:34:56 | Login Success | IP: 192.168.1.1
|
||||||
|
2024-01-15 12:35:12 | Withdrawal | Key: rstr_abcd | Amount: 5000
|
||||||
|
2024-01-15 12:40:00 | Session Timeout | IP: 192.168.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Strong Password**: Use a long, random password
|
||||||
|
2. **HTTPS Only**: Always access via HTTPS
|
||||||
|
3. **Regular Monitoring**: Check logs frequently
|
||||||
|
4. **Limited Access**: Restrict dashboard access
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot Access Dashboard
|
||||||
|
|
||||||
|
**Issue**: 404 Not Found
|
||||||
|
- Ensure trailing slash: `/admin/`
|
||||||
|
- Check if admin routes are enabled
|
||||||
|
|
||||||
|
**Issue**: Unauthorized
|
||||||
|
- Verify `ADMIN_PASSWORD` is set
|
||||||
|
- Clear browser cookies
|
||||||
|
- Try incognito/private mode
|
||||||
|
|
||||||
|
### Display Issues
|
||||||
|
|
||||||
|
**Issue**: Broken Layout
|
||||||
|
- Clear browser cache
|
||||||
|
- Disable ad blockers
|
||||||
|
- Try different browser
|
||||||
|
|
||||||
|
**Issue**: Missing Data
|
||||||
|
- Check database connectivity
|
||||||
|
- Verify node is running
|
||||||
|
- Review error logs
|
||||||
|
|
||||||
|
### Withdrawal Problems
|
||||||
|
|
||||||
|
**Issue**: Token Generation Fails
|
||||||
|
- Check mint connectivity
|
||||||
|
- Verify sufficient balance
|
||||||
|
- Try different mint
|
||||||
|
|
||||||
|
**Issue**: Invalid Token
|
||||||
|
- Ensure complete token copy
|
||||||
|
- Check token hasn't expired
|
||||||
|
- Verify mint compatibility
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Custom Branding
|
||||||
|
|
||||||
|
Customize dashboard appearance:
|
||||||
|
```bash
|
||||||
|
# Environment variables
|
||||||
|
ADMIN_LOGO_URL=https://example.com/logo.png
|
||||||
|
ADMIN_THEME_COLOR=#FF6B00
|
||||||
|
ADMIN_CUSTOM_CSS=/path/to/custom.css
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Access
|
||||||
|
|
||||||
|
Access admin functions programmatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get node stats
|
||||||
|
curl -X GET https://your-node.com/admin/api/stats \
|
||||||
|
-H "X-Admin-Password: your-password"
|
||||||
|
|
||||||
|
# Export key data
|
||||||
|
curl -X GET https://your-node.com/admin/api/keys \
|
||||||
|
-H "X-Admin-Password: your-password" \
|
||||||
|
-H "Accept: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
|
||||||
|
Configure notifications:
|
||||||
|
```bash
|
||||||
|
ADMIN_WEBHOOK_URL=https://example.com/webhook
|
||||||
|
ADMIN_WEBHOOK_EVENTS=withdrawal,low_balance,error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dashboard Shortcuts
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
- `Ctrl+K`: Quick search
|
||||||
|
- `Ctrl+R`: Refresh data
|
||||||
|
- `Ctrl+E`: Export current view
|
||||||
|
- `Escape`: Close modals
|
||||||
|
|
||||||
|
### Quick Actions
|
||||||
|
|
||||||
|
- Double-click to copy API key
|
||||||
|
- Right-click for context menu
|
||||||
|
- Drag to reorder columns
|
||||||
|
- Shift-click to select multiple
|
||||||
|
|
||||||
|
## Mobile Access
|
||||||
|
|
||||||
|
The dashboard is mobile-responsive:
|
||||||
|
- Touch-optimized controls
|
||||||
|
- Swipe navigation
|
||||||
|
- Compact view mode
|
||||||
|
- Offline capability
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Models & Pricing](models-pricing.md) - Configure pricing
|
||||||
|
- [API Reference](../api/overview.md) - Admin API endpoints
|
||||||
|
- [Advanced Configuration](../advanced/custom-pricing.md) - Advanced settings
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
# User Guide Introduction
|
||||||
|
|
||||||
|
Welcome to the Routstr Core User Guide. This guide will help you understand how to use Routstr to access AI APIs with Bitcoin micropayments.
|
||||||
|
|
||||||
|
## What You'll Learn
|
||||||
|
|
||||||
|
- How the payment system works
|
||||||
|
- Creating and managing API keys
|
||||||
|
- Making API calls through Routstr
|
||||||
|
- Using the admin dashboard
|
||||||
|
- Managing your balance
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting, you'll need:
|
||||||
|
|
||||||
|
1. **A Running Routstr Instance**
|
||||||
|
- Either your own deployment or access to a public node
|
||||||
|
- The base URL (e.g., `https://api.yournode.com`)
|
||||||
|
|
||||||
|
2. **A Cashu Wallet** (optional but recommended)
|
||||||
|
- [Nutstash](https://nutstash.app) - Web wallet
|
||||||
|
- [Minibits](https://www.minibits.cash) - Mobile wallet
|
||||||
|
- [Cashu.me](https://cashu.me) - Simple web wallet
|
||||||
|
|
||||||
|
3. **An API Client**
|
||||||
|
- OpenAI Python/JavaScript SDK
|
||||||
|
- Any HTTP client (curl, Postman, etc.)
|
||||||
|
- Your application code
|
||||||
|
|
||||||
|
## How Routstr Works
|
||||||
|
|
||||||
|
### Traditional API Access
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Your App] --> B[OpenAI API]
|
||||||
|
B --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
- Direct connection to provider
|
||||||
|
- Monthly billing
|
||||||
|
- Credit card required
|
||||||
|
- Usage limits
|
||||||
|
|
||||||
|
### With Routstr
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Your App] --> B[Routstr Proxy]
|
||||||
|
B --> C[OpenAI API]
|
||||||
|
C --> B
|
||||||
|
B --> A
|
||||||
|
D[Bitcoin/eCash] --> B
|
||||||
|
```
|
||||||
|
|
||||||
|
- Pay per request with Bitcoin
|
||||||
|
- No credit card needed
|
||||||
|
- Anonymous payments
|
||||||
|
- Instant settlement
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### eCash Tokens
|
||||||
|
|
||||||
|
- Digital bearer tokens backed by Bitcoin
|
||||||
|
- Can be sent like cash - whoever has the token owns it
|
||||||
|
- Redeemable at Cashu mints for Bitcoin
|
||||||
|
- Perfect for micropayments
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
- Created by depositing eCash tokens
|
||||||
|
- Track your balance and usage
|
||||||
|
- Can be topped up anytime
|
||||||
|
- Optional expiry and refund address
|
||||||
|
|
||||||
|
### Balance Management
|
||||||
|
|
||||||
|
- Measured in millisatoshis (msats)
|
||||||
|
- 1 Bitcoin = 100,000,000 sats = 100,000,000,000 msats
|
||||||
|
- Deducted based on actual usage
|
||||||
|
- Withdrawable as eCash tokens
|
||||||
|
|
||||||
|
## Typical Workflow
|
||||||
|
|
||||||
|
### 1. Get Bitcoin/eCash
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- Buy Bitcoin and deposit to a Cashu mint
|
||||||
|
- Receive eCash tokens from someone else
|
||||||
|
- Use a testnet mint for testing
|
||||||
|
|
||||||
|
### 2. Create API Key
|
||||||
|
|
||||||
|
Send your eCash token to Routstr:
|
||||||
|
```bash
|
||||||
|
POST /v1/wallet/create
|
||||||
|
{
|
||||||
|
"cashu_token": "cashuAeyJ0..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Receive your API key and balance.
|
||||||
|
|
||||||
|
### 3. Make API Calls
|
||||||
|
|
||||||
|
Use your API key like any OpenAI key:
|
||||||
|
```python
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="your-routstr-key",
|
||||||
|
base_url="https://your-routstr-node/v1"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Monitor Usage
|
||||||
|
|
||||||
|
- Check balance: `GET /v1/wallet/balance`
|
||||||
|
- View admin dashboard
|
||||||
|
- Track costs per request
|
||||||
|
|
||||||
|
### 5. Withdraw Funds
|
||||||
|
|
||||||
|
When done, withdraw remaining balance as eCash through the admin interface.
|
||||||
|
|
||||||
|
## Supported Endpoints
|
||||||
|
|
||||||
|
Routstr supports all standard OpenAI endpoints:
|
||||||
|
|
||||||
|
- ✅ `/v1/chat/completions` - Chat models
|
||||||
|
- ✅ `/v1/completions` - Text completion
|
||||||
|
- ✅ `/v1/embeddings` - Text embeddings
|
||||||
|
- ✅ `/v1/images/generations` - Image generation
|
||||||
|
- ✅ `/v1/audio/transcriptions` - Audio to text
|
||||||
|
- ✅ `/v1/audio/translations` - Audio translation
|
||||||
|
- ✅ `/v1/models` - List available models
|
||||||
|
- ✅ Custom provider endpoints
|
||||||
|
|
||||||
|
## Cost Structure
|
||||||
|
|
||||||
|
### Pricing Models
|
||||||
|
|
||||||
|
1. **Fixed Cost Per Request**
|
||||||
|
- Simple flat fee per API call
|
||||||
|
- Good for uniform usage
|
||||||
|
|
||||||
|
2. **Token-Based Pricing**
|
||||||
|
- Pay per input/output token
|
||||||
|
- More accurate for varied usage
|
||||||
|
|
||||||
|
3. **Model-Based Pricing**
|
||||||
|
- Different rates per model
|
||||||
|
- Reflects actual provider costs
|
||||||
|
|
||||||
|
### Cost Calculation
|
||||||
|
|
||||||
|
```
|
||||||
|
Total Cost = Base Fee + (Input Tokens * Input Rate) + (Output Tokens * Output Rate)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fees may include:
|
||||||
|
- Exchange rate markup (BTC/USD conversion)
|
||||||
|
- Provider margin
|
||||||
|
- Node operator fee
|
||||||
|
|
||||||
|
## Getting Support
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- This user guide for general usage
|
||||||
|
- [API Reference](../api/overview.md) for technical details
|
||||||
|
- [Contributing Guide](../contributing/setup.md) for developers
|
||||||
|
|
||||||
|
### Community
|
||||||
|
|
||||||
|
- GitHub Issues for bugs and features
|
||||||
|
- Nostr for decentralized discussion
|
||||||
|
- Node operator contact info
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
Common issues and solutions:
|
||||||
|
- [Payment Flow](payment-flow.md) - Understanding the payment process
|
||||||
|
- [Using the API](using-api.md) - API integration guide
|
||||||
|
- [Admin Dashboard](admin-dashboard.md) - Managing your node
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### API Key Security
|
||||||
|
|
||||||
|
- Treat API keys like passwords
|
||||||
|
- Never share or commit them
|
||||||
|
- Rotate keys regularly
|
||||||
|
- Use environment variables
|
||||||
|
|
||||||
|
### Payment Security
|
||||||
|
|
||||||
|
- eCash tokens are bearer instruments
|
||||||
|
- Verify mint trustworthiness
|
||||||
|
- Keep backups of tokens
|
||||||
|
- Use small amounts for testing
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- Always use HTTPS connections
|
||||||
|
- Verify SSL certificates
|
||||||
|
- Consider using Tor for privacy
|
||||||
|
- Monitor for unusual activity
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Ready to start? Continue with:
|
||||||
|
|
||||||
|
1. [Payment Flow](payment-flow.md) - Detailed payment process
|
||||||
|
2. [Using the API](using-api.md) - Making your first calls
|
||||||
|
3. [Admin Dashboard](admin-dashboard.md) - Managing your account
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# Models & Pricing
|
||||||
|
|
||||||
|
Understanding how Routstr calculates costs is essential for managing your API usage efficiently. This guide explains the pricing models and how to configure them.
|
||||||
|
|
||||||
|
## Pricing Models
|
||||||
|
|
||||||
|
Routstr supports three pricing models:
|
||||||
|
|
||||||
|
### 1. Fixed Pricing
|
||||||
|
|
||||||
|
Simple per-request charging:
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=false
|
||||||
|
COST_PER_REQUEST=10 # 10 sats per request
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Uniform API usage
|
||||||
|
- Simple applications
|
||||||
|
- Predictable costs
|
||||||
|
|
||||||
|
### 2. Token-Based Pricing
|
||||||
|
|
||||||
|
Charge based on actual token usage:
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=false
|
||||||
|
COST_PER_REQUEST=1 # 1 sat base fee
|
||||||
|
COST_PER_1K_INPUT_TOKENS=5 # 5 sats per 1K input
|
||||||
|
COST_PER_1K_OUTPUT_TOKENS=15 # 15 sats per 1K output
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Varied request sizes
|
||||||
|
- Fair usage billing
|
||||||
|
- Cost optimization
|
||||||
|
|
||||||
|
### 3. Model-Based Pricing
|
||||||
|
|
||||||
|
Dynamic pricing based on model costs:
|
||||||
|
```bash
|
||||||
|
MODEL_BASED_PRICING=true
|
||||||
|
EXCHANGE_FEE=1.005 # 0.5% exchange fee
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.05 # 5% provider fee
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Multiple models
|
||||||
|
- Market-based pricing
|
||||||
|
- Automatic updates
|
||||||
|
|
||||||
|
## Model Configuration
|
||||||
|
|
||||||
|
### Default Models
|
||||||
|
|
||||||
|
Routstr includes pricing for popular models:
|
||||||
|
|
||||||
|
| Model | Input ($/1K) | Output ($/1K) | Context | Notes |
|
||||||
|
|-------|--------------|---------------|---------|-------|
|
||||||
|
| gpt-3.5-turbo | $0.0015 | $0.002 | 16K | Fast, economical |
|
||||||
|
| gpt-4 | $0.03 | $0.06 | 8K | Advanced reasoning |
|
||||||
|
| gpt-4-turbo | $0.01 | $0.03 | 128K | Large context |
|
||||||
|
| claude-3-opus | $0.015 | $0.075 | 200K | Best quality |
|
||||||
|
| claude-3-sonnet | $0.003 | $0.015 | 200K | Balanced |
|
||||||
|
| llama-2-70b | $0.0007 | $0.0009 | 4K | Open source |
|
||||||
|
|
||||||
|
### Custom Models File
|
||||||
|
|
||||||
|
Create `models.json` to override defaults:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-4-vision",
|
||||||
|
"name": "GPT-4 Vision",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.00003",
|
||||||
|
"completion": "0.00006",
|
||||||
|
"request": "0",
|
||||||
|
"image": "0.00255"
|
||||||
|
},
|
||||||
|
"context_length": 128000,
|
||||||
|
"supports_vision": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom-model",
|
||||||
|
"name": "My Custom Model",
|
||||||
|
"pricing": {
|
||||||
|
"prompt": "0.001",
|
||||||
|
"completion": "0.002",
|
||||||
|
"request": "0.0001"
|
||||||
|
},
|
||||||
|
"context_length": 8192
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-updating Models
|
||||||
|
|
||||||
|
Fetch latest models from OpenRouter:
|
||||||
|
```bash
|
||||||
|
# Update models from API
|
||||||
|
python scripts/models_meta.py
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
curl https://openrouter.ai/api/v1/models > models.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Calculation
|
||||||
|
|
||||||
|
### Understanding the Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
Base Cost = (Input Tokens × Input Rate) + (Output Tokens × Output Rate) + Request Fee
|
||||||
|
|
||||||
|
Bitcoin Price = Current BTC/USD rate (e.g., $50,000)
|
||||||
|
Sats Cost = (Base Cost / Bitcoin Price) × 100,000,000
|
||||||
|
|
||||||
|
Final Cost = Sats Cost × Exchange Fee × Provider Fee
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Calculations
|
||||||
|
|
||||||
|
**Example 1: Simple Chat (gpt-3.5-turbo)**
|
||||||
|
```
|
||||||
|
Input: 50 tokens
|
||||||
|
Output: 150 tokens
|
||||||
|
Model rates: $0.0015/1K input, $0.002/1K output
|
||||||
|
|
||||||
|
USD Cost = (50/1000 × 0.0015) + (150/1000 × 0.002)
|
||||||
|
= $0.000075 + $0.0003
|
||||||
|
= $0.000375
|
||||||
|
|
||||||
|
At $50,000/BTC: 0.75 sats
|
||||||
|
With 5.5% total fees: 0.79 sats
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 2: Large Context (gpt-4)**
|
||||||
|
```
|
||||||
|
Input: 2,000 tokens
|
||||||
|
Output: 500 tokens
|
||||||
|
Model rates: $0.03/1K input, $0.06/1K output
|
||||||
|
|
||||||
|
USD Cost = (2000/1000 × 0.03) + (500/1000 × 0.06)
|
||||||
|
= $0.06 + $0.03
|
||||||
|
= $0.09
|
||||||
|
|
||||||
|
At $50,000/BTC: 180 sats
|
||||||
|
With 5.5% total fees: 190 sats
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 3: Image Generation (dall-e-3)**
|
||||||
|
```
|
||||||
|
Model: dall-e-3
|
||||||
|
Size: 1024x1024
|
||||||
|
Quality: standard
|
||||||
|
Cost: $0.04 per image
|
||||||
|
|
||||||
|
At $50,000/BTC: 80 sats
|
||||||
|
With 5.5% fees: 84 sats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fee Structure
|
||||||
|
|
||||||
|
### Exchange Fee
|
||||||
|
|
||||||
|
Covers Bitcoin/USD conversion costs:
|
||||||
|
```bash
|
||||||
|
EXCHANGE_FEE=1.005 # 0.5% default
|
||||||
|
```
|
||||||
|
|
||||||
|
Factors:
|
||||||
|
- Exchange rate volatility
|
||||||
|
- Conversion costs
|
||||||
|
- Price update frequency
|
||||||
|
|
||||||
|
### Provider Fee
|
||||||
|
|
||||||
|
Node operator's margin:
|
||||||
|
```bash
|
||||||
|
UPSTREAM_PROVIDER_FEE=1.05 # 5% default
|
||||||
|
```
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- Infrastructure costs
|
||||||
|
- Maintenance
|
||||||
|
- Support
|
||||||
|
- Profit margin
|
||||||
|
|
||||||
|
### Calculating Total Fees
|
||||||
|
|
||||||
|
```
|
||||||
|
Total Multiplier = EXCHANGE_FEE × UPSTREAM_PROVIDER_FEE
|
||||||
|
Example: 1.005 × 1.05 = 1.05525 (5.525% total)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Pricing
|
||||||
|
|
||||||
|
### Image Models
|
||||||
|
|
||||||
|
Image generation uses per-image pricing:
|
||||||
|
|
||||||
|
| Model | Size | Quality | Price |
|
||||||
|
|-------|------|---------|-------|
|
||||||
|
| dall-e-2 | 256x256 | - | $0.016 |
|
||||||
|
| dall-e-2 | 512x512 | - | $0.018 |
|
||||||
|
| dall-e-2 | 1024x1024 | - | $0.02 |
|
||||||
|
| dall-e-3 | 1024x1024 | standard | $0.04 |
|
||||||
|
| dall-e-3 | 1024x1024 | hd | $0.08 |
|
||||||
|
| dall-e-3 | 1024x1792 | standard | $0.08 |
|
||||||
|
| dall-e-3 | 1024x1792 | hd | $0.12 |
|
||||||
|
|
||||||
|
### Audio Models
|
||||||
|
|
||||||
|
Audio pricing by duration:
|
||||||
|
|
||||||
|
| Model | Type | Price |
|
||||||
|
|-------|------|-------|
|
||||||
|
| whisper-1 | Transcription | $0.006/minute |
|
||||||
|
| whisper-1 | Translation | $0.006/minute |
|
||||||
|
| tts-1 | Text-to-speech | $0.015/1K chars |
|
||||||
|
| tts-1-hd | HD speech | $0.03/1K chars |
|
||||||
|
|
||||||
|
### Embedding Models
|
||||||
|
|
||||||
|
Lower costs for embeddings:
|
||||||
|
|
||||||
|
| Model | Price/1K tokens |
|
||||||
|
|-------|-----------------|
|
||||||
|
| text-embedding-3-small | $0.00002 |
|
||||||
|
| text-embedding-3-large | $0.00013 |
|
||||||
|
| text-embedding-ada-002 | $0.0001 |
|
||||||
|
|
||||||
|
## Monitoring Costs
|
||||||
|
|
||||||
|
### Per-Request Tracking
|
||||||
|
|
||||||
|
Each API response includes usage data:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 50,
|
||||||
|
"completion_tokens": 150,
|
||||||
|
"total_tokens": 200
|
||||||
|
},
|
||||||
|
"x-routstr-cost": {
|
||||||
|
"sats": 79,
|
||||||
|
"usd": 0.000375,
|
||||||
|
"breakdown": {
|
||||||
|
"prompt_cost": 15,
|
||||||
|
"completion_cost": 60,
|
||||||
|
"fees": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daily Summaries
|
||||||
|
|
||||||
|
View in admin dashboard:
|
||||||
|
- Total requests
|
||||||
|
- Token usage by model
|
||||||
|
- Cost distribution
|
||||||
|
- Trending patterns
|
||||||
|
|
||||||
|
### Cost Alerts
|
||||||
|
|
||||||
|
Set up notifications:
|
||||||
|
```python
|
||||||
|
# Example monitoring script
|
||||||
|
def check_daily_spend(api_key):
|
||||||
|
balance_start = get_balance(api_key, "00:00")
|
||||||
|
balance_now = get_balance(api_key)
|
||||||
|
spent = balance_start - balance_now
|
||||||
|
|
||||||
|
if spent > DAILY_LIMIT:
|
||||||
|
send_alert(f"Daily spend exceeded: {spent} sats")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimization Strategies
|
||||||
|
|
||||||
|
### Model Selection
|
||||||
|
|
||||||
|
Choose the right model for each task:
|
||||||
|
|
||||||
|
| Task | Recommended Model | Why |
|
||||||
|
|------|-------------------|-----|
|
||||||
|
| Simple Q&A | gpt-3.5-turbo | Fast, cheap, sufficient |
|
||||||
|
| Code generation | gpt-4 | Better reasoning |
|
||||||
|
| Summarization | claude-3-haiku | Good balance |
|
||||||
|
| Creative writing | claude-3-opus | Best quality |
|
||||||
|
| Embeddings | text-embedding-3-small | Optimized for vectors |
|
||||||
|
|
||||||
|
### Prompt Engineering
|
||||||
|
|
||||||
|
Reduce costs with efficient prompts:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Expensive
|
||||||
|
prompt = """
|
||||||
|
You are an AI assistant. Your task is to help users.
|
||||||
|
Please provide detailed, comprehensive answers.
|
||||||
|
Now, answer this question: What is 2+2?
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Economical
|
||||||
|
prompt = "Calculate: 2+2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Strategies
|
||||||
|
|
||||||
|
Implement smart caching:
|
||||||
|
```python
|
||||||
|
# Cache embedding results
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_embedding(text):
|
||||||
|
return client.embeddings.create(
|
||||||
|
model="text-embedding-3-small",
|
||||||
|
input=text
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache common responses
|
||||||
|
COMMON_RESPONSES = {
|
||||||
|
"greeting": "Hello! How can I help you?",
|
||||||
|
"goodbye": "Goodbye! Have a great day!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Processing
|
||||||
|
|
||||||
|
Process multiple items efficiently:
|
||||||
|
```python
|
||||||
|
# Instead of multiple calls
|
||||||
|
for item in items:
|
||||||
|
response = client.chat.completions.create(...)
|
||||||
|
|
||||||
|
# Use single call with formatted prompt
|
||||||
|
prompt = "\n".join([f"{i+1}. {item}" for i, item in enumerate(items)])
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
messages=[{"role": "user", "content": f"Process these items:\n{prompt}"}]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Pricing Rules
|
||||||
|
|
||||||
|
### Time-Based Pricing
|
||||||
|
|
||||||
|
Implement off-peak discounts:
|
||||||
|
```python
|
||||||
|
def calculate_multiplier():
|
||||||
|
hour = datetime.now().hour
|
||||||
|
if 2 <= hour <= 6: # 2 AM - 6 AM
|
||||||
|
return 0.8 # 20% discount
|
||||||
|
elif 18 <= hour <= 22: # 6 PM - 10 PM
|
||||||
|
return 1.2 # 20% premium
|
||||||
|
return 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Discounts
|
||||||
|
|
||||||
|
Reward high-volume usage:
|
||||||
|
```python
|
||||||
|
def get_discount_rate(monthly_volume_sats):
|
||||||
|
if monthly_volume_sats > 1_000_000:
|
||||||
|
return 0.9 # 10% discount
|
||||||
|
elif monthly_volume_sats > 500_000:
|
||||||
|
return 0.95 # 5% discount
|
||||||
|
return 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model-Specific Rules
|
||||||
|
|
||||||
|
Custom pricing logic:
|
||||||
|
```python
|
||||||
|
def adjust_model_price(model, base_price):
|
||||||
|
# Premium for latest models
|
||||||
|
if "turbo" in model or "latest" in model:
|
||||||
|
return base_price * 1.1
|
||||||
|
|
||||||
|
# Discount for older models
|
||||||
|
if "legacy" in model:
|
||||||
|
return base_price * 0.8
|
||||||
|
|
||||||
|
return base_price
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pricing Transparency
|
||||||
|
|
||||||
|
### Public Pricing Page
|
||||||
|
|
||||||
|
Display current rates:
|
||||||
|
```html
|
||||||
|
<!-- Available at /pricing -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Model</th>
|
||||||
|
<th>Input (sats/1K)</th>
|
||||||
|
<th>Output (sats/1K)</th>
|
||||||
|
</tr>
|
||||||
|
<!-- Dynamically generated from models.json -->
|
||||||
|
</table>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cost Estimation API
|
||||||
|
|
||||||
|
Provide cost estimates:
|
||||||
|
```bash
|
||||||
|
POST /v1/estimate
|
||||||
|
{
|
||||||
|
"model": "gpt-4",
|
||||||
|
"prompt_tokens": 500,
|
||||||
|
"max_tokens": 200
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"estimated_cost_sats": 45,
|
||||||
|
"breakdown": {
|
||||||
|
"prompt": 30,
|
||||||
|
"completion": 12,
|
||||||
|
"fees": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pricing Mismatches
|
||||||
|
|
||||||
|
**Issue**: Costs don't match expectations
|
||||||
|
- Check current BTC/USD rate
|
||||||
|
- Verify fee settings
|
||||||
|
- Review model configuration
|
||||||
|
|
||||||
|
**Issue**: Models not found
|
||||||
|
- Update models.json
|
||||||
|
- Check model ID spelling
|
||||||
|
- Verify upstream support
|
||||||
|
|
||||||
|
### Fee Calculations
|
||||||
|
|
||||||
|
**Issue**: Fees seem too high
|
||||||
|
- Review EXCHANGE_FEE setting
|
||||||
|
- Check UPSTREAM_PROVIDER_FEE
|
||||||
|
- Calculate total multiplier
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [API Reference](../api/overview.md) - Technical details
|
||||||
|
- [Custom Pricing](../advanced/custom-pricing.md) - Advanced configuration
|
||||||
|
- [Contributing](../contributing/setup.md) - Help improve Routstr
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
# Payment Flow
|
||||||
|
|
||||||
|
Understanding how payments work in Routstr is key to using the system effectively. This guide explains the payment process in detail.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Routstr uses a pre-funded account model where:
|
||||||
|
1. Users deposit eCash tokens to create an API key
|
||||||
|
2. Each API request deducts from the balance
|
||||||
|
3. Users can withdraw remaining balance as eCash
|
||||||
|
|
||||||
|
## Creating an API Key
|
||||||
|
|
||||||
|
### Step 1: Obtain eCash Token
|
||||||
|
|
||||||
|
Get a Cashu token from any compatible source:
|
||||||
|
|
||||||
|
**Option A: From a Cashu Wallet**
|
||||||
|
```bash
|
||||||
|
# Example: Creating a 10,000 sat token
|
||||||
|
cashu send 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Lightning Invoice**
|
||||||
|
```bash
|
||||||
|
# Some mints support direct Lightning deposits
|
||||||
|
curl -X POST https://mint.example.com/v1/mint/quote/bolt11 \
|
||||||
|
-d '{"amount": 10000, "unit": "sat"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Test Tokens**
|
||||||
|
```bash
|
||||||
|
# Get test tokens from testnet mints
|
||||||
|
# Check mint documentation for faucets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create API Key
|
||||||
|
|
||||||
|
Send your token to Routstr:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-routstr-node/v1/wallet/create \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"cashu_token": "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vbWlu...",
|
||||||
|
"name": "My Project Key",
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z",
|
||||||
|
"refund_npub": "npub1abc..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Parameters:**
|
||||||
|
- `cashu_token` (required): The eCash token to deposit
|
||||||
|
- `name` (optional): Friendly name for the key
|
||||||
|
- `expires_at` (optional): ISO timestamp for key expiration
|
||||||
|
- `refund_npub` (optional): Nostr pubkey for refunds
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "rstr_1234567890abcdef",
|
||||||
|
"balance": 10000000,
|
||||||
|
"expires_at": "2024-12-31T23:59:59Z",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Verify Balance
|
||||||
|
|
||||||
|
Check your key's balance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET https://your-routstr-node/v1/wallet/balance \
|
||||||
|
-H "Authorization: Bearer rstr_1234567890abcdef"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"balance": 10000000,
|
||||||
|
"total_deposited": 10000000,
|
||||||
|
"total_spent": 0,
|
||||||
|
"last_used": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making API Requests
|
||||||
|
|
||||||
|
### Cost Calculation
|
||||||
|
|
||||||
|
Costs are calculated based on:
|
||||||
|
|
||||||
|
1. **Request Type**
|
||||||
|
- Chat completions
|
||||||
|
- Embeddings
|
||||||
|
- Image generation
|
||||||
|
- Audio processing
|
||||||
|
|
||||||
|
2. **Token Usage**
|
||||||
|
- Input tokens (prompt)
|
||||||
|
- Output tokens (response)
|
||||||
|
- Model-specific rates
|
||||||
|
|
||||||
|
3. **Additional Costs**
|
||||||
|
- Base request fee
|
||||||
|
- Image generation fees
|
||||||
|
- Audio processing time
|
||||||
|
|
||||||
|
### Example: Chat Completion
|
||||||
|
|
||||||
|
```python
|
||||||
|
import openai
|
||||||
|
|
||||||
|
client = openai.OpenAI(
|
||||||
|
api_key="rstr_1234567890abcdef",
|
||||||
|
base_url="https://your-routstr-node/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make request
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[
|
||||||
|
{"role": "user", "content": "Hello, how are you?"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check usage
|
||||||
|
print(f"Input tokens: {response.usage.prompt_tokens}")
|
||||||
|
print(f"Output tokens: {response.usage.completion_tokens}")
|
||||||
|
print(f"Total tokens: {response.usage.total_tokens}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cost Breakdown
|
||||||
|
|
||||||
|
For the above request:
|
||||||
|
```
|
||||||
|
Model: gpt-3.5-turbo
|
||||||
|
Input tokens: 13
|
||||||
|
Output tokens: 27
|
||||||
|
Model rates: $0.0015/1K input, $0.002/1K output
|
||||||
|
|
||||||
|
USD Cost = (13/1000 * 0.0015) + (27/1000 * 0.002) = $0.0000735
|
||||||
|
BTC/USD Rate: $50,000
|
||||||
|
BTC Cost = 0.0000735 / 50000 = 0.00000000147 BTC = 147 sats
|
||||||
|
With fees (5%): 154 sats
|
||||||
|
|
||||||
|
Final cost: 154 sats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Balance Management
|
||||||
|
|
||||||
|
### Monitoring Usage
|
||||||
|
|
||||||
|
Track your usage in real-time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get current balance
|
||||||
|
curl -X GET https://your-routstr-node/v1/wallet/balance \
|
||||||
|
-H "Authorization: Bearer your-api-key"
|
||||||
|
|
||||||
|
# View recent transactions (through admin dashboard)
|
||||||
|
# Access at https://your-routstr-node/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Low Balance Handling
|
||||||
|
|
||||||
|
When balance is insufficient:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "insufficient_balance",
|
||||||
|
"message": "Insufficient balance. Current: 100 sats, Required: 154 sats",
|
||||||
|
"code": "payment_required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Topping Up
|
||||||
|
|
||||||
|
Add funds to existing key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-routstr-node/v1/wallet/topup \
|
||||||
|
-H "Authorization: Bearer your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"cashu_token": "cashuAeyJ0b2..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Withdrawing Balance
|
||||||
|
|
||||||
|
### Via Admin Dashboard
|
||||||
|
|
||||||
|
1. Navigate to `/admin/`
|
||||||
|
2. Enter admin password
|
||||||
|
3. Find your API key
|
||||||
|
4. Click "Withdraw"
|
||||||
|
5. Receive eCash token
|
||||||
|
|
||||||
|
### Via API (if enabled)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-routstr-node/v1/wallet/withdraw \
|
||||||
|
-H "Authorization: Bearer your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"amount": 5000,
|
||||||
|
"mint": "https://mint.minibits.cash/Bitcoin"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Payment Security
|
||||||
|
|
||||||
|
### Token Validation
|
||||||
|
|
||||||
|
Routstr validates tokens by:
|
||||||
|
1. Checking signature validity
|
||||||
|
2. Verifying with the issuing mint
|
||||||
|
3. Ensuring no double-spending
|
||||||
|
4. Confirming sufficient value
|
||||||
|
|
||||||
|
### Failed Payments
|
||||||
|
|
||||||
|
Common failure reasons:
|
||||||
|
- Invalid token signature
|
||||||
|
- Already spent token
|
||||||
|
- Untrusted mint
|
||||||
|
- Network issues with mint
|
||||||
|
|
||||||
|
### Refund Policy
|
||||||
|
|
||||||
|
- Unused balance can be withdrawn anytime
|
||||||
|
- Expired keys with balance can be refunded to `refund_npub`
|
||||||
|
- Node operators may have additional policies
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Multi-Mint Support
|
||||||
|
|
||||||
|
Routstr accepts tokens from multiple mints:
|
||||||
|
```bash
|
||||||
|
CASHU_MINTS=https://mint1.com,https://mint2.com,https://mint3.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Redundancy if one mint is down
|
||||||
|
- User choice of mints
|
||||||
|
- Geographic distribution
|
||||||
|
|
||||||
|
### Automatic Payouts
|
||||||
|
|
||||||
|
Configure automatic Lightning payouts:
|
||||||
|
```bash
|
||||||
|
RECEIVE_LN_ADDRESS=satoshi@getalby.com
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled:
|
||||||
|
- Balances above threshold are swept
|
||||||
|
- Converted to Lightning payments
|
||||||
|
- Sent to configured address
|
||||||
|
|
||||||
|
### Per-Request Payments (Coming Soon)
|
||||||
|
|
||||||
|
Future support for Nut-24 headers:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-routstr-node/v1/chat/completions \
|
||||||
|
-H "x-cashu: cashuAeyJ0..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{...}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes change:
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
x-cashu: cashuAeyJjaGFuZ2Ui...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### API Key Management
|
||||||
|
|
||||||
|
1. **Separate Keys per Application**
|
||||||
|
- Easier tracking
|
||||||
|
- Better security
|
||||||
|
- Independent budgets
|
||||||
|
|
||||||
|
2. **Set Expiration Dates**
|
||||||
|
- Automatic cleanup
|
||||||
|
- Security improvement
|
||||||
|
- Budget control
|
||||||
|
|
||||||
|
3. **Monitor Balances**
|
||||||
|
- Set up alerts
|
||||||
|
- Regular checks
|
||||||
|
- Usage analytics
|
||||||
|
|
||||||
|
### Cost Optimization
|
||||||
|
|
||||||
|
1. **Choose Appropriate Models**
|
||||||
|
- Smaller models for simple tasks
|
||||||
|
- Larger models only when needed
|
||||||
|
|
||||||
|
2. **Optimize Prompts**
|
||||||
|
- Concise, clear instructions
|
||||||
|
- Avoid unnecessary tokens
|
||||||
|
|
||||||
|
3. **Use Streaming**
|
||||||
|
- Early termination possible
|
||||||
|
- Better user experience
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
1. **Secure Storage**
|
||||||
|
- Environment variables
|
||||||
|
- Secrets management
|
||||||
|
- Never in code
|
||||||
|
|
||||||
|
2. **Network Security**
|
||||||
|
- Always use HTTPS
|
||||||
|
- Verify certificates
|
||||||
|
- Consider Tor for privacy
|
||||||
|
|
||||||
|
3. **Regular Rotation**
|
||||||
|
- Change keys periodically
|
||||||
|
- Withdraw unused funds
|
||||||
|
- Audit usage logs
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Payment Rejected
|
||||||
|
|
||||||
|
**Error:** "Invalid token"
|
||||||
|
- Check token format
|
||||||
|
- Verify mint is trusted
|
||||||
|
- Ensure not already spent
|
||||||
|
|
||||||
|
**Error:** "Insufficient value"
|
||||||
|
- Token value too low
|
||||||
|
- Check current pricing
|
||||||
|
- Add larger token
|
||||||
|
|
||||||
|
### Balance Discrepancies
|
||||||
|
|
||||||
|
- Allow for price fluctuations
|
||||||
|
- Check model pricing updates
|
||||||
|
- Review transaction history
|
||||||
|
|
||||||
|
### Mint Issues
|
||||||
|
|
||||||
|
- Try different mint from list
|
||||||
|
- Check mint status
|
||||||
|
- Contact mint operator
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Using the API](using-api.md) - Integration guide
|
||||||
|
- [Admin Dashboard](admin-dashboard.md) - Account management
|
||||||
|
- [Models & Pricing](models-pricing.md) - Cost details
|
||||||
@@ -0,0 +1,532 @@
|
|||||||
|
# Using the API
|
||||||
|
|
||||||
|
This guide shows how to integrate Routstr with your applications using various programming languages and tools.
|
||||||
|
|
||||||
|
## API Compatibility
|
||||||
|
|
||||||
|
Routstr maintains full compatibility with the OpenAI API, meaning:
|
||||||
|
- Existing OpenAI client libraries work without modification
|
||||||
|
- Only the base URL and API key need to change
|
||||||
|
- All parameters and responses match OpenAI's format
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
Using the official OpenAI Python library:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
# Initialize client with Routstr endpoint
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_api_key_here",
|
||||||
|
base_url="https://your-routstr-node.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use exactly like OpenAI
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "Hello!"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
Using the official OpenAI Node.js library:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: 'rstr_your_api_key_here',
|
||||||
|
baseURL: 'https://your-routstr-node.com/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make a request
|
||||||
|
async function main() {
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||||
|
{ role: 'user', content: 'Hello!' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(completion.choices[0].message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
Direct HTTP requests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://your-routstr-node.com/v1/chat/completions \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer rstr_your_api_key_here" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Hello!"}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Chat Completions
|
||||||
|
|
||||||
|
Standard chat with conversation history:
|
||||||
|
|
||||||
|
```python
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
def chat(user_input):
|
||||||
|
# Add user message
|
||||||
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
# Get AI response
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=messages,
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=150
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add AI response to history
|
||||||
|
ai_message = response.choices[0].message
|
||||||
|
messages.append({"role": "assistant", "content": ai_message.content})
|
||||||
|
|
||||||
|
return ai_message.content
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
print(chat("What's the weather like?"))
|
||||||
|
print(chat("How should I dress?")) # Maintains context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Responses
|
||||||
|
|
||||||
|
For real-time output:
|
||||||
|
|
||||||
|
```python
|
||||||
|
stream = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": "Write a short story"}],
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for chunk in stream:
|
||||||
|
if chunk.choices[0].delta.content is not None:
|
||||||
|
print(chunk.choices[0].delta.content, end="", flush=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Calling
|
||||||
|
|
||||||
|
Using OpenAI's function calling feature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tools = [{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "Get current weather",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {"type": "string"},
|
||||||
|
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-4",
|
||||||
|
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
|
||||||
|
tools=tools,
|
||||||
|
tool_choice="auto"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if function was called
|
||||||
|
if response.choices[0].message.tool_calls:
|
||||||
|
tool_call = response.choices[0].message.tool_calls[0]
|
||||||
|
print(f"Function: {tool_call.function.name}")
|
||||||
|
print(f"Arguments: {tool_call.function.arguments}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embeddings
|
||||||
|
|
||||||
|
Generate text embeddings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = client.embeddings.create(
|
||||||
|
model="text-embedding-3-small",
|
||||||
|
input="The quick brown fox jumps over the lazy dog"
|
||||||
|
)
|
||||||
|
|
||||||
|
embedding = response.data[0].embedding
|
||||||
|
print(f"Embedding dimension: {len(embedding)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Generation
|
||||||
|
|
||||||
|
Create images with DALL-E:
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = client.images.generate(
|
||||||
|
model="dall-e-3",
|
||||||
|
prompt="A futuristic city with flying cars",
|
||||||
|
size="1024x1024",
|
||||||
|
quality="standard",
|
||||||
|
n=1
|
||||||
|
)
|
||||||
|
|
||||||
|
image_url = response.data[0].url
|
||||||
|
print(f"Image URL: {image_url}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio Transcription
|
||||||
|
|
||||||
|
Convert speech to text:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with open("audio.mp3", "rb") as audio_file:
|
||||||
|
response = client.audio.transcriptions.create(
|
||||||
|
model="whisper-1",
|
||||||
|
file=audio_file,
|
||||||
|
response_format="text"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Balance Errors
|
||||||
|
|
||||||
|
Handle insufficient balance gracefully:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-4",
|
||||||
|
messages=[{"role": "user", "content": "Hello"}]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if "insufficient_balance" in str(e):
|
||||||
|
print("Low balance! Please top up your API key.")
|
||||||
|
# Implement top-up logic
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
Implement exponential backoff:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def make_request_with_retry(
|
||||||
|
func,
|
||||||
|
max_retries: int = 3,
|
||||||
|
initial_delay: float = 1.0
|
||||||
|
) -> Optional[any]:
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except Exception as e:
|
||||||
|
if "rate_limit" in str(e) and attempt < max_retries - 1:
|
||||||
|
delay = initial_delay * (2 ** attempt)
|
||||||
|
print(f"Rate limited. Waiting {delay}s...")
|
||||||
|
time.sleep(delay)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Errors
|
||||||
|
|
||||||
|
Handle network issues:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# Configure timeout and retries
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="https://your-node.com/v1",
|
||||||
|
timeout=httpx.Timeout(60.0, connect=5.0),
|
||||||
|
max_retries=2
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Using Tor
|
||||||
|
|
||||||
|
Route requests through Tor for privacy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# Configure Tor proxy
|
||||||
|
proxies = {
|
||||||
|
"http://": "socks5://127.0.0.1:9050",
|
||||||
|
"https://": "socks5://127.0.0.1:9050"
|
||||||
|
}
|
||||||
|
|
||||||
|
http_client = httpx.Client(proxies=proxies)
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="http://your-onion-address.onion/v1",
|
||||||
|
http_client=http_client
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Headers
|
||||||
|
|
||||||
|
Add custom headers if needed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
class CustomClient(httpx.Client):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.headers["X-Custom-Header"] = "value"
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="https://your-node.com/v1",
|
||||||
|
http_client=CustomClient()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Operations
|
||||||
|
|
||||||
|
For high-performance applications:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
|
async_client = AsyncOpenAI(
|
||||||
|
api_key="rstr_your_key",
|
||||||
|
base_url="https://your-node.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def process_messages(messages):
|
||||||
|
tasks = []
|
||||||
|
for msg in messages:
|
||||||
|
task = async_client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": msg}]
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
responses = await asyncio.gather(*tasks)
|
||||||
|
return [r.choices[0].message.content for r in responses]
|
||||||
|
|
||||||
|
# Run async
|
||||||
|
messages = ["Hello", "How are you?", "What's 2+2?"]
|
||||||
|
results = asyncio.run(process_messages(messages))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Environment Variables
|
||||||
|
|
||||||
|
Never hardcode API keys:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=os.getenv("ROUTSTR_API_KEY"),
|
||||||
|
base_url=os.getenv("ROUTSTR_BASE_URL", "https://api.routstr.com/v1")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Error Logging
|
||||||
|
|
||||||
|
Implement comprehensive logging:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(...)
|
||||||
|
logger.info(f"Request successful. Tokens used: {response.usage.total_tokens}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"API request failed: {e}")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cost Tracking
|
||||||
|
|
||||||
|
Monitor your usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class UsageTracker:
|
||||||
|
def __init__(self):
|
||||||
|
self.total_tokens = 0
|
||||||
|
self.total_requests = 0
|
||||||
|
|
||||||
|
def track(self, response):
|
||||||
|
self.total_tokens += response.usage.total_tokens
|
||||||
|
self.total_requests += 1
|
||||||
|
|
||||||
|
# Estimate cost (example rates)
|
||||||
|
cost_per_1k = 0.002 # $0.002 per 1K tokens
|
||||||
|
estimated_cost = (self.total_tokens / 1000) * cost_per_1k
|
||||||
|
|
||||||
|
logger.info(f"Total usage: {self.total_tokens} tokens, "
|
||||||
|
f"${estimated_cost:.4f} (~{estimated_cost * 50000:.0f} sats)")
|
||||||
|
|
||||||
|
tracker = UsageTracker()
|
||||||
|
response = client.chat.completions.create(...)
|
||||||
|
tracker.track(response)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Caching Responses
|
||||||
|
|
||||||
|
Reduce costs with intelligent caching:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
@lru_cache(maxsize=100)
|
||||||
|
def cached_completion(prompt: str, model: str = "gpt-3.5-turbo"):
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
temperature=0 # Deterministic for caching
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
# Repeated calls with same prompt use cache
|
||||||
|
result1 = cached_completion("What is 2+2?")
|
||||||
|
result2 = cached_completion("What is 2+2?") # From cache, no API call
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Mock Responses
|
||||||
|
|
||||||
|
For development without spending sats:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MockOpenAI:
|
||||||
|
class Completions:
|
||||||
|
def create(self, **kwargs):
|
||||||
|
return type('Response', (), {
|
||||||
|
'choices': [type('Choice', (), {
|
||||||
|
'message': type('Message', (), {
|
||||||
|
'content': 'Mock response'
|
||||||
|
})()
|
||||||
|
})],
|
||||||
|
'usage': type('Usage', (), {
|
||||||
|
'total_tokens': 10
|
||||||
|
})()
|
||||||
|
})()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.chat = type('Chat', (), {
|
||||||
|
'completions': self.Completions()
|
||||||
|
})()
|
||||||
|
|
||||||
|
# Use mock in tests
|
||||||
|
if os.getenv('TESTING'):
|
||||||
|
client = MockOpenAI()
|
||||||
|
else:
|
||||||
|
client = OpenAI(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
Test your Routstr integration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_routstr_connection():
|
||||||
|
try:
|
||||||
|
# Test models endpoint
|
||||||
|
models = client.models.list()
|
||||||
|
assert len(models.data) > 0
|
||||||
|
|
||||||
|
# Test simple completion
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": "test"}],
|
||||||
|
max_tokens=5
|
||||||
|
)
|
||||||
|
assert response.choices[0].message.content
|
||||||
|
|
||||||
|
print("✅ Routstr integration working!")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Integration test failed: {e}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**SSL Certificate Errors**
|
||||||
|
```python
|
||||||
|
# For development only - not for production!
|
||||||
|
import ssl
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_key",
|
||||||
|
base_url="https://localhost:8000/v1",
|
||||||
|
http_client=httpx.Client(verify=False)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timeout Issues**
|
||||||
|
```python
|
||||||
|
# Increase timeout for slow connections
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="rstr_key",
|
||||||
|
base_url="https://your-node.com/v1",
|
||||||
|
timeout=httpx.Timeout(120.0) # 2 minutes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debugging Requests**
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# Enable debug logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
httpx_logger = logging.getLogger("httpx")
|
||||||
|
httpx_logger.setLevel(logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Admin Dashboard](admin-dashboard.md) - Manage your account
|
||||||
|
- [Models & Pricing](models-pricing.md) - Understanding costs
|
||||||
|
- [API Reference](../api/overview.md) - Technical details
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
site_name: Routstr Core Documentation
|
||||||
|
site_url: https://docs.routstr.com
|
||||||
|
site_description: FastAPI-based reverse proxy for OpenAI-compatible APIs with Bitcoin eCash micropayments
|
||||||
|
site_author: Routstr Team
|
||||||
|
|
||||||
|
repo_name: routstr/routstr-core
|
||||||
|
repo_url: https://github.com/routstr/routstr-core
|
||||||
|
edit_uri: tree/main/docs
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
language: en
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: orange
|
||||||
|
accent: amber
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: orange
|
||||||
|
accent: amber
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.expand
|
||||||
|
- navigation.top
|
||||||
|
- navigation.indexes
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
- content.tabs.link
|
||||||
|
- content.code.copy
|
||||||
|
- content.code.annotate
|
||||||
|
icon:
|
||||||
|
logo: material/lightning-bolt
|
||||||
|
repo: fontawesome/brands/github
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- mermaid2
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- codehilite:
|
||||||
|
guess_lang: false
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/routstr
|
||||||
|
- icon: fontawesome/brands/bitcoin
|
||||||
|
link: https://cashu.space
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting Started:
|
||||||
|
- Overview: getting-started/overview.md
|
||||||
|
- Quick Start: getting-started/quickstart.md
|
||||||
|
- Docker Setup: getting-started/docker.md
|
||||||
|
- Configuration: getting-started/configuration.md
|
||||||
|
- User Guide:
|
||||||
|
- Introduction: user-guide/introduction.md
|
||||||
|
- Payment Flow: user-guide/payment-flow.md
|
||||||
|
- Using the API: user-guide/using-api.md
|
||||||
|
- Admin Dashboard: user-guide/admin-dashboard.md
|
||||||
|
- Models & Pricing: user-guide/models-pricing.md
|
||||||
|
- Contributing:
|
||||||
|
- Setup Development: contributing/setup.md
|
||||||
|
- Architecture: contributing/architecture.md
|
||||||
|
- Code Structure: contributing/code-structure.md
|
||||||
|
- Testing: contributing/testing.md
|
||||||
|
- Database: contributing/database.md
|
||||||
|
- Guidelines: contributing/guidelines.md
|
||||||
|
- API Reference:
|
||||||
|
- Overview: api/overview.md
|
||||||
|
- Authentication: api/authentication.md
|
||||||
|
- Endpoints: api/endpoints.md
|
||||||
|
- Errors: api/errors.md
|
||||||
|
- Advanced:
|
||||||
|
- Tor Support: advanced/tor.md
|
||||||
|
- Nostr Discovery: advanced/nostr.md
|
||||||
|
- Custom Pricing: advanced/custom-pricing.md
|
||||||
|
- Migrations: advanced/migrations.md
|
||||||
Reference in New Issue
Block a user