mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
docs: Add comprehensive documentation for Routstr Core project
Co-authored-by: db2002dominic <db2002dominic@gmail.com>
This commit is contained in:
67
.env.example
67
.env.example
@@ -1,40 +1,43 @@
|
||||
# NAME = "Your Routstr Proxy Name"
|
||||
# DESCRIPTION = "A short Description"
|
||||
# Core Configuration
|
||||
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
|
||||
UPSTREAM_BASE_URL="https://api.openai.com/v1"
|
||||
UPSTREAM_API_KEY="sk-21212121212121212121212121212121"
|
||||
# UPSTREAM_PROVIDER_FEE=1 # 1 = no fees, 1.05 = 5% fees
|
||||
# Database
|
||||
DATABASE_URL=sqlite+aiosqlite:///keys.db
|
||||
|
||||
# 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.
|
||||
# RECEIVE_LN_ADDRESS="user@minibits.cash"
|
||||
#MINIMUM_PAYOUT = "100"
|
||||
# Cashu Configuration
|
||||
CASHU_MINTS=https://mint.minibits.cash/Bitcoin
|
||||
RECEIVE_LN_ADDRESS=
|
||||
|
||||
# If set to true, pricing is loaded from the file specified by MODELS_PATH
|
||||
# Defaults to "models.json" and falls back to "models.example.json" if missing
|
||||
# MODEL_BASED_PRICING = "true"
|
||||
# MODELS_PATH="models.json"
|
||||
# Pricing Configuration
|
||||
MODEL_BASED_PRICING=true
|
||||
COST_PER_REQUEST=1
|
||||
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
|
||||
# COST_PER_REQUEST="10"
|
||||
# COST_PER_1K_INPUT_TOKENS = "0"
|
||||
# COST_PER_1K_OUTPUT_TOKENS = "0"
|
||||
# EXCHANGE_FEE = "1.005" # 0.5 % currency exchange fee
|
||||
# Network Configuration
|
||||
CORS_ORIGINS=*
|
||||
TOR_PROXY_URL=socks5://127.0.0.1:9050
|
||||
|
||||
# password used to log into admin interface
|
||||
# ADMIN_PASSWORD=""
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
ENABLE_CONSOLE_LOGGING=true
|
||||
|
||||
# Public Endpoint
|
||||
# HTTP_URL="https://your.domain.com"
|
||||
# Model Management
|
||||
MODELS_PATH=models.json
|
||||
BASE_URL=https://openrouter.ai/api/v1
|
||||
SOURCE=
|
||||
|
||||
# Tor Endpoint (copy from docker logs)
|
||||
# ONION_URL=".onion"
|
||||
|
||||
# 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
|
||||
# Optional Features
|
||||
PREPAID_API_KEY=
|
||||
PREPAID_BALANCE=0
|
||||
17
Makefile
17
Makefile
@@ -244,3 +244,20 @@ profile:
|
||||
@echo "🔥 Running with profiling..."
|
||||
$(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."
|
||||
|
||||
# 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
|
||||
|
||||
28
build-docs.sh
Executable file
28
build-docs.sh
Executable file
@@ -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
docs/README.md
Normal file
155
docs/README.md
Normal file
@@ -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/)
|
||||
619
docs/advanced/custom-pricing.md
Normal file
619
docs/advanced/custom-pricing.md
Normal file
@@ -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
|
||||
631
docs/advanced/migrations.md
Normal file
631
docs/advanced/migrations.md
Normal file
@@ -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
|
||||
592
docs/advanced/nostr.md
Normal file
592
docs/advanced/nostr.md
Normal file
@@ -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
|
||||
516
docs/advanced/tor.md
Normal file
516
docs/advanced/tor.md
Normal file
@@ -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
|
||||
427
docs/api/authentication.md
Normal file
427
docs/api/authentication.md
Normal file
@@ -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
|
||||
622
docs/api/endpoints.md
Normal file
622
docs/api/endpoints.md
Normal file
@@ -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
|
||||
591
docs/api/errors.md
Normal file
591
docs/api/errors.md
Normal file
@@ -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
|
||||
349
docs/api/overview.md
Normal file
349
docs/api/overview.md
Normal file
@@ -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
|
||||
381
docs/contributing/architecture.md
Normal file
381
docs/contributing/architecture.md
Normal file
@@ -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
|
||||
490
docs/contributing/code-structure.md
Normal file
490
docs/contributing/code-structure.md
Normal file
@@ -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
|
||||
698
docs/contributing/database.md
Normal file
698
docs/contributing/database.md
Normal file
@@ -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
|
||||
505
docs/contributing/guidelines.md
Normal file
505
docs/contributing/guidelines.md
Normal file
@@ -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! 🎉
|
||||
381
docs/contributing/setup.md
Normal file
381
docs/contributing/setup.md
Normal file
@@ -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! 🚀
|
||||
578
docs/contributing/testing.md
Normal file
578
docs/contributing/testing.md
Normal file
@@ -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
|
||||
259
docs/getting-started/configuration.md
Normal file
259
docs/getting-started/configuration.md
Normal file
@@ -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
|
||||
335
docs/getting-started/docker.md
Normal file
335
docs/getting-started/docker.md
Normal file
@@ -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
|
||||
156
docs/getting-started/overview.md
Normal file
156
docs/getting-started/overview.md
Normal file
@@ -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
|
||||
214
docs/getting-started/quickstart.md
Normal file
214
docs/getting-started/quickstart.md
Normal file
@@ -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
|
||||
83
docs/index.md
Normal file
83
docs/index.md
Normal file
@@ -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.
|
||||
4
docs/requirements.txt
Normal file
4
docs/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
mkdocs>=1.5.3
|
||||
mkdocs-material>=9.5.0
|
||||
mkdocs-mermaid2-plugin>=1.1.1
|
||||
pymdown-extensions>=10.5
|
||||
311
docs/user-guide/admin-dashboard.md
Normal file
311
docs/user-guide/admin-dashboard.md
Normal file
@@ -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
|
||||
216
docs/user-guide/introduction.md
Normal file
216
docs/user-guide/introduction.md
Normal file
@@ -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
|
||||
452
docs/user-guide/models-pricing.md
Normal file
452
docs/user-guide/models-pricing.md
Normal file
@@ -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
|
||||
359
docs/user-guide/payment-flow.md
Normal file
359
docs/user-guide/payment-flow.md
Normal file
@@ -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
|
||||
532
docs/user-guide/using-api.md
Normal file
532
docs/user-guide/using-api.md
Normal file
@@ -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
mkdocs.yml
Normal file
107
mkdocs.yml
Normal file
@@ -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