mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 19:04:47 +00:00
Merge branch 'main' into kwsantiago/62-comprehensive-tests
This commit is contained in:
+2
-2
@@ -24,13 +24,13 @@ MODEL_BASED_PRICING = "true"
|
||||
# EXCHANGE_FEE = "1.005" # 0.5 % currency exchange fee
|
||||
|
||||
# password used to log into admin interface
|
||||
ADMIN_PASSWORD="CHANGE-THIS"
|
||||
# ADMIN_PASSWORD=""
|
||||
|
||||
# Public Endpoint
|
||||
HTTP_URL="https://your.domain.com"
|
||||
|
||||
# Tor Endpoint (copy from docker logs)
|
||||
ONION_URL=".onion"
|
||||
# ONION_URL=".onion"
|
||||
|
||||
RELAYS="wss://relay.routstr.com,wss://relay.nostr.band"
|
||||
CASHU_MINTS="https://mint.minibits.cash/Bitcoin,https://mint.cubabitcoin.org"
|
||||
|
||||
@@ -11,6 +11,7 @@ wallet.sqlite3
|
||||
.cashu
|
||||
.relay
|
||||
relay-data
|
||||
.dockerignore
|
||||
|
||||
compose.override.yml
|
||||
|
||||
|
||||
@@ -32,22 +32,32 @@ sequenceDiagram
|
||||
- **Cashu Wallet Integration** – Accept Lightning payments and redeem eCash tokens before forwarding requests
|
||||
- **API Key Management** – Hashed keys stored in SQLite with balance tracking and optional expiry/refund address
|
||||
- **Model-Based Pricing** – Convert USD prices in `models.json` to sats using live BTC/USD rates
|
||||
- **Admin Dashboard** – Simple HTML interface at `/admin` to view balances and API keys
|
||||
- **Admin Dashboard** – Simple HTML interface at `/admin/` to view balances and API keys
|
||||
- **Discovery** – Fetch available providers from Nostr relays
|
||||
- **Docker Support** – Provided `Dockerfile` and `compose.yml` for running with an optional Tor hidden service
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
### Running the proxy using Docker
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Development Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- [uv](https://github.com/astral-sh/uv) package manager (used in development)
|
||||
- A Cashu wallet secret (`NSEC`) and Lightning address for receiving payments
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
uv sync --dev # install dependencies
|
||||
uv sync # install dependencies
|
||||
```
|
||||
|
||||
Create a `.env` file based on `.env.example` and fill in the required values:
|
||||
@@ -78,15 +88,18 @@ The most common settings are shown below. See `.env.example` for the full list.
|
||||
|
||||
- `UPSTREAM_BASE_URL` – URL of the OpenAI-compatible service
|
||||
- `UPSTREAM_API_KEY` – API key for the upstream service (optional)
|
||||
- `RECEIVE_LN_ADDRESS` – Lightning address that receives payouts
|
||||
- `MINIMUM_PAYOUT` – Minimum sats before forwarding earnings
|
||||
- `MODEL_BASED_PRICING` – Set to `true` to use pricing from `models.json`
|
||||
- `REFUND_PROCESSING_INTERVAL` – Seconds between automatic refunds
|
||||
- `ADMIN_PASSWORD` – Password for the `/admin` dashboard
|
||||
- `ADMIN_PASSWORD` – Password for the `/admin/` dashboard
|
||||
- `CASHU_MINTS` – Comma-separated list of Cashu mint URLs
|
||||
- `NAME` – Name of the proxy
|
||||
- `DESCRIPTION` – Description of the proxy
|
||||
- `NPUB` – Nostr public key of the proxy
|
||||
- `HTTP_URL` – Public-facing URL of the proxy
|
||||
- `ONION_URL` – Tor hidden service URL of the proxy
|
||||
|
||||
## Withdrawing Balance
|
||||
|
||||
Go to `https://<your.routstr.proxy>/admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token.
|
||||
Go to `https://<your.routstr.proxy>/admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token.
|
||||
|
||||
## Example Client
|
||||
|
||||
@@ -147,7 +160,8 @@ The proxy should implement either a dedicated endpoint to communicate minimum eC
|
||||
|
||||
To use this feature, you'll need a client that handles both OpenAI API calls and eCash header management. The following clients provide seamless integration:
|
||||
|
||||
- **[cashu-402-client](https://github.com/9qeklajc/ecash-402-client)** – rust client with automatic wallet management
|
||||
- **[routstr-chat](https://github.com/routstr/routstr-chat)** – chat app for the routstr network
|
||||
- **[otrta-client](https://github.com/routstr/otrta-client)** – rust web app for the routstr network
|
||||
|
||||
clients automatically:
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ services:
|
||||
- TOR_PROXY_URL=socks5://tor:9050
|
||||
ports:
|
||||
- 8000:8000
|
||||
extra_hosts: # Needed to access locally running models
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
tor:
|
||||
image: ghcr.io/hundehausen/tor-hidden-service:latest
|
||||
|
||||
@@ -276,6 +276,7 @@ def setup_logging() -> None:
|
||||
"propagate": False,
|
||||
},
|
||||
"watchfiles.main": {"level": "WARNING", "handlers": [], "propagate": False},
|
||||
"aiosqlite": {"level": "ERROR", "handlers": [], "propagate": False},
|
||||
},
|
||||
"root": {
|
||||
"level": log_level,
|
||||
|
||||
+5
-18
@@ -10,7 +10,7 @@ from ..balance import balance_router, deprecated_wallet_router
|
||||
from ..discovery import providers_router
|
||||
from ..payment.models import MODELS, models_router, update_sats_pricing
|
||||
from ..proxy import proxy_router
|
||||
from ..wallet import check_for_refunds, periodic_payout
|
||||
from ..wallet import periodic_payout
|
||||
from .admin import admin_router
|
||||
from .db import init_db
|
||||
from .logging import get_logger, setup_logging
|
||||
@@ -19,23 +19,20 @@ from .logging import get_logger, setup_logging
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
logger.info("Application startup initiated", extra={"version": __version__})
|
||||
|
||||
# Initialize task variables to None
|
||||
pricing_task = None
|
||||
refund_task = None
|
||||
payout_task = None
|
||||
|
||||
try:
|
||||
await init_db()
|
||||
|
||||
pricing_task = asyncio.create_task(update_sats_pricing())
|
||||
refund_task = asyncio.create_task(check_for_refunds())
|
||||
payout_task = asyncio.create_task(periodic_payout())
|
||||
|
||||
yield
|
||||
@@ -49,21 +46,11 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
finally:
|
||||
logger.info("Application shutdown initiated")
|
||||
|
||||
# Cancel tasks if they were created
|
||||
if refund_task:
|
||||
refund_task.cancel()
|
||||
if pricing_task:
|
||||
pricing_task.cancel()
|
||||
if payout_task:
|
||||
payout_task.cancel()
|
||||
pricing_task.cancel()
|
||||
payout_task.cancel()
|
||||
|
||||
try:
|
||||
# Only gather tasks that were created
|
||||
tasks_to_wait = [
|
||||
task for task in [pricing_task, refund_task, payout_task] if task
|
||||
]
|
||||
if tasks_to_wait:
|
||||
await asyncio.gather(*tasks_to_wait, return_exceptions=True)
|
||||
await asyncio.gather(pricing_task, payout_task, return_exceptions=True)
|
||||
logger.info("Background tasks stopped successfully")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
+12
-10
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, Response
|
||||
|
||||
@@ -10,17 +11,12 @@ from .models import MODELS
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
UPSTREAM_BASE_URL = os.environ["UPSTREAM_BASE_URL"]
|
||||
|
||||
UPSTREAM_BASE_URL = os.environ.get("UPSTREAM_BASE_URL", "")
|
||||
UPSTREAM_API_KEY = os.environ.get("UPSTREAM_API_KEY", "")
|
||||
|
||||
logger.info(
|
||||
"Payment helpers initialized",
|
||||
extra={
|
||||
"upstream_base_url": UPSTREAM_BASE_URL,
|
||||
"has_upstream_api_key": bool(UPSTREAM_API_KEY),
|
||||
"model_based_pricing": MODEL_BASED_PRICING,
|
||||
},
|
||||
)
|
||||
if not UPSTREAM_BASE_URL:
|
||||
raise ValueError("Please set the UPSTREAM_BASE_URL environment variable")
|
||||
|
||||
|
||||
def get_cost_per_request(model: str | None = None) -> int:
|
||||
@@ -153,7 +149,9 @@ def get_max_cost_for_model(model: str) -> int:
|
||||
return COST_PER_REQUEST
|
||||
|
||||
|
||||
def create_error_response(error_type: str, message: str, status_code: int) -> Response:
|
||||
def create_error_response(
|
||||
error_type: str, message: str, status_code: int, token: Optional[str] = None
|
||||
) -> Response:
|
||||
"""Create a standardized error response."""
|
||||
logger.info(
|
||||
"Creating error response",
|
||||
@@ -164,6 +162,9 @@ def create_error_response(error_type: str, message: str, status_code: int) -> Re
|
||||
},
|
||||
)
|
||||
|
||||
response_headers = {}
|
||||
if token:
|
||||
response_headers["X-Cashu"] = token
|
||||
return Response(
|
||||
content=json.dumps(
|
||||
{
|
||||
@@ -176,6 +177,7 @@ def create_error_response(error_type: str, message: str, status_code: int) -> Re
|
||||
),
|
||||
status_code=status_code,
|
||||
media_type="application/json",
|
||||
headers=dict(response_headers),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+18
-35
@@ -89,43 +89,25 @@ async def update_sats_pricing() -> None:
|
||||
model.sats_pricing = Pricing(
|
||||
**{k: v / sats_to_usd for k, v in model.pricing.dict().items()}
|
||||
)
|
||||
if model.top_provider:
|
||||
if (
|
||||
model.top_provider.context_length
|
||||
and model.top_provider.max_completion_tokens
|
||||
mspp = model.sats_pricing.prompt
|
||||
mspc = model.sats_pricing.completion
|
||||
if (tp := model.top_provider) and (
|
||||
tp.context_length or tp.max_completion_tokens
|
||||
):
|
||||
if (cl := model.top_provider.context_length) and (
|
||||
mct := model.top_provider.max_completion_tokens
|
||||
):
|
||||
max_context_cost = (
|
||||
model.top_provider.context_length
|
||||
* model.sats_pricing.prompt
|
||||
)
|
||||
max_completion_cost = (
|
||||
model.top_provider.max_completion_tokens
|
||||
* model.sats_pricing.completion
|
||||
)
|
||||
model.sats_pricing.max_cost = (
|
||||
max_context_cost + max_completion_cost
|
||||
)
|
||||
elif model.top_provider.context_length:
|
||||
max_context_cost = (
|
||||
model.top_provider.context_length
|
||||
* model.sats_pricing.prompt
|
||||
)
|
||||
max_completion_cost = 32_000 * model.sats_pricing.completion
|
||||
model.sats_pricing.max_cost = (
|
||||
max_context_cost + max_completion_cost
|
||||
)
|
||||
elif model.top_provider.max_completion_tokens:
|
||||
max_completion_cost = (
|
||||
model.top_provider.max_completion_tokens
|
||||
* model.sats_pricing.completion
|
||||
)
|
||||
max_context_cost = 1_048_576 * model.sats_pricing.prompt
|
||||
model.sats_pricing.max_cost = max_completion_cost
|
||||
model.sats_pricing.max_cost = (cl - mct) * mspp + mct * mspc
|
||||
elif cl := model.top_provider.context_length:
|
||||
model.sats_pricing.max_cost = cl * 0.8 * mspp + cl * 0.2 * mspc
|
||||
elif mct := model.top_provider.max_completion_tokens:
|
||||
model.sats_pricing.max_cost = mct * 4 * mspp + mct * mspc
|
||||
else:
|
||||
model.sats_pricing.max_cost = (
|
||||
1_048_576 * model.sats_pricing.prompt
|
||||
+ 32_000 * model.sats_pricing.completion
|
||||
)
|
||||
model.sats_pricing.max_cost = 1_000_000 * mspp + 32_000 * mspc
|
||||
elif model.context_length:
|
||||
model.sats_pricing.max_cost = (
|
||||
model.sats_pricing.prompt * model.context_length * 0.8
|
||||
) + (model.sats_pricing.completion * model.context_length * 0.2)
|
||||
else:
|
||||
p = model.sats_pricing.prompt * 1_000_000
|
||||
c = model.sats_pricing.completion * 32_000
|
||||
@@ -145,5 +127,6 @@ async def update_sats_pricing() -> None:
|
||||
|
||||
|
||||
@models_router.get("/v1/models")
|
||||
@models_router.get("/models")
|
||||
async def models() -> dict:
|
||||
return {"data": MODELS}
|
||||
|
||||
@@ -18,7 +18,6 @@ async def kraken_btc_usd(client: httpx.AsyncClient) -> float | None:
|
||||
"""Fetch BTC/USD price from Kraken API."""
|
||||
api = "https://api.kraken.com/0/public/Ticker?pair=XBTUSD"
|
||||
try:
|
||||
logger.debug("Fetching BTC price from Kraken")
|
||||
response = await client.get(api)
|
||||
price_data = response.json()
|
||||
price = float(price_data["result"]["XXBTZUSD"]["c"][0])
|
||||
@@ -40,7 +39,6 @@ async def coinbase_btc_usd(client: httpx.AsyncClient) -> float | None:
|
||||
"""Fetch BTC/USD price from Coinbase API."""
|
||||
api = "https://api.coinbase.com/v2/prices/BTC-USD/spot"
|
||||
try:
|
||||
logger.debug("Fetching BTC price from Coinbase")
|
||||
response = await client.get(api)
|
||||
price_data = response.json()
|
||||
price = float(price_data["data"]["amount"])
|
||||
@@ -62,7 +60,6 @@ async def binance_btc_usdt(client: httpx.AsyncClient) -> float | None:
|
||||
"""Fetch BTC/USDT price from Binance API."""
|
||||
api = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"
|
||||
try:
|
||||
logger.debug("Fetching BTC price from Binance")
|
||||
response = await client.get(api)
|
||||
price_data = response.json()
|
||||
price = float(price_data["price"])
|
||||
@@ -82,7 +79,6 @@ async def binance_btc_usdt(client: httpx.AsyncClient) -> float | None:
|
||||
|
||||
async def btc_usd_ask_price() -> float:
|
||||
"""Get the highest BTC/USD price from multiple exchanges with fee adjustment."""
|
||||
logger.debug("Starting BTC price aggregation from multiple exchanges")
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
@@ -113,7 +109,6 @@ async def btc_usd_ask_price() -> float:
|
||||
|
||||
async def sats_usd_ask_price() -> float:
|
||||
"""Get the USD price per satoshi."""
|
||||
logger.debug("Calculating satoshi price from BTC price")
|
||||
|
||||
try:
|
||||
btc_price = await btc_usd_ask_price()
|
||||
|
||||
+19
-15
@@ -8,12 +8,7 @@ from fastapi.responses import Response, StreamingResponse
|
||||
|
||||
from ..core import get_logger
|
||||
from ..wallet import CurrencyUnit, recieve_token, send_token
|
||||
from .cost_caculation import (
|
||||
CostData,
|
||||
CostDataError,
|
||||
MaxCostData,
|
||||
calculate_cost,
|
||||
)
|
||||
from .cost_caculation import CostData, CostDataError, MaxCostData, calculate_cost
|
||||
from .helpers import (
|
||||
UPSTREAM_BASE_URL,
|
||||
create_error_response,
|
||||
@@ -68,21 +63,30 @@ async def x_cashu_handler(
|
||||
"token_already_spent",
|
||||
"The provided CASHU token has already been spent",
|
||||
400,
|
||||
x_cashu_token,
|
||||
)
|
||||
elif "invalid token" in error_message.lower():
|
||||
|
||||
if "invalid token" in error_message.lower():
|
||||
return create_error_response(
|
||||
"invalid_token", "The provided CASHU token is invalid", 400
|
||||
"invalid_token",
|
||||
"The provided CASHU token is invalid",
|
||||
400,
|
||||
x_cashu_token,
|
||||
)
|
||||
elif "mint error" in error_message.lower():
|
||||
|
||||
if "mint error" in error_message.lower():
|
||||
return create_error_response(
|
||||
"mint_error", f"CASHU mint error: {error_message}", 422
|
||||
)
|
||||
else:
|
||||
# Generic error for other cases
|
||||
return create_error_response(
|
||||
"cashu_error", f"CASHU token processing failed: {error_message}", 400
|
||||
"mint_error", f"CASHU mint error: {error_message}", 422, x_cashu_token
|
||||
)
|
||||
|
||||
# Generic error for other cases
|
||||
return create_error_response(
|
||||
"cashu_error",
|
||||
f"CASHU token processing failed: {error_message}",
|
||||
400,
|
||||
x_cashu_token,
|
||||
)
|
||||
|
||||
|
||||
async def forward_to_upstream(
|
||||
request: Request, path: str, headers: dict, amount: int, unit: CurrencyUnit
|
||||
|
||||
+2
-5
@@ -11,7 +11,8 @@ logger = get_logger(__name__)
|
||||
|
||||
CurrencyUnit = Literal["sat", "msat"]
|
||||
|
||||
TRUSTED_MINTS = os.environ["CASHU_MINTS"].split(",")
|
||||
CASHU_MINTS = os.environ.get("CASHU_MINTS", "https://mint.minibits.cash/Bitcoin")
|
||||
TRUSTED_MINTS = CASHU_MINTS.split(",")
|
||||
PRIMARY_MINT_URL = TRUSTED_MINTS[0]
|
||||
|
||||
|
||||
@@ -157,10 +158,6 @@ async def send_to_lnurl(amount: int, unit: CurrencyUnit, lnurl: str) -> dict[str
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def check_for_refunds() -> None:
|
||||
logger.warning("check_for_refunds, temporary not implemented")
|
||||
|
||||
|
||||
async def periodic_payout() -> None:
|
||||
logger.warning("periodic_payout, temporary not implemented")
|
||||
|
||||
|
||||
+17
-9
@@ -43,24 +43,33 @@ class Model(TypedDict):
|
||||
|
||||
OUTPUT_FILE = os.getenv("OUTPUT_FILE", "models.json")
|
||||
BASE_URL = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
|
||||
SOURCE = os.getenv("SOURCE")
|
||||
|
||||
|
||||
def fetch_openrouter_models() -> list[Model]:
|
||||
def fetch_openrouter_models(source_filter: str | None = None) -> list[Model]:
|
||||
"""Fetches model information from OpenRouter API."""
|
||||
with urlopen(f"{BASE_URL}/models") as response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
models_data: list[Model] = []
|
||||
for model in data.get("data", []):
|
||||
# Skip models with '(free)' in the name or id = 'openrouter/auto'
|
||||
model_id = model.get("id", "")
|
||||
|
||||
if source_filter:
|
||||
source_prefix = f"{source_filter}/"
|
||||
if not model_id.startswith(source_prefix):
|
||||
continue
|
||||
|
||||
model = dict(model)
|
||||
model["id"] = model_id[len(source_prefix) :]
|
||||
model_id = model["id"]
|
||||
|
||||
if (
|
||||
"(free)" in model.get("name", "")
|
||||
or model.get("id") == "openrouter/auto"
|
||||
or model_id == "openrouter/auto"
|
||||
or model_id == "google/gemini-2.5-pro-exp-03-25"
|
||||
):
|
||||
continue
|
||||
# Skip free Gemini 2.5 Pro Exp
|
||||
if model.get("id") == "google/gemini-2.5-pro-exp-03-25":
|
||||
continue
|
||||
|
||||
models_data.append(model)
|
||||
|
||||
@@ -68,10 +77,9 @@ def fetch_openrouter_models() -> list[Model]:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
models = fetch_openrouter_models()
|
||||
source_filter = SOURCE if SOURCE and SOURCE.strip() else None
|
||||
models = fetch_openrouter_models(source_filter=source_filter)
|
||||
|
||||
# Print the first model data in a nicely indented JSON format
|
||||
# print(json.dumps(models[0], indent=4))
|
||||
print(f"Writing {len(models)} models to {OUTPUT_FILE}")
|
||||
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
|
||||
+17
-13
@@ -87,11 +87,18 @@ async def test_update_sats_pricing_calculation(sample_model: Model) -> None:
|
||||
0.001 / 0.0001
|
||||
) # 10 sats
|
||||
|
||||
# Verify max_cost calculation for model with top_provider
|
||||
expected_max_context = 4096 * sample_model.sats_pricing.prompt
|
||||
expected_max_completion = 2048 * sample_model.sats_pricing.completion
|
||||
assert sample_model.top_provider is not None
|
||||
assert sample_model.top_provider.context_length is not None
|
||||
assert sample_model.top_provider.max_completion_tokens is not None
|
||||
|
||||
assert sample_model.sats_pricing.max_cost == pytest.approx(
|
||||
expected_max_context + expected_max_completion
|
||||
(
|
||||
sample_model.top_provider.context_length
|
||||
- sample_model.top_provider.max_completion_tokens
|
||||
)
|
||||
* sample_model.sats_pricing.prompt
|
||||
+ sample_model.top_provider.max_completion_tokens
|
||||
* sample_model.sats_pricing.completion
|
||||
)
|
||||
|
||||
# Cancel and await the task
|
||||
@@ -159,16 +166,13 @@ async def test_update_sats_pricing_without_top_provider() -> None:
|
||||
assert model_without_top.sats_pricing is not None
|
||||
|
||||
# Verify the fallback max_cost calculation
|
||||
p = model_without_top.sats_pricing.prompt * 1_000_000
|
||||
c = model_without_top.sats_pricing.completion * 32_000
|
||||
r = model_without_top.sats_pricing.request * 100_000
|
||||
i = model_without_top.sats_pricing.image * 100
|
||||
w = model_without_top.sats_pricing.web_search * 1000
|
||||
ir = model_without_top.sats_pricing.internal_reasoning * 100
|
||||
expected_max = p + c + r + i + w + ir
|
||||
|
||||
assert model_without_top.sats_pricing.max_cost == pytest.approx(
|
||||
expected_max
|
||||
model_without_top.context_length
|
||||
* 0.8
|
||||
* model_without_top.sats_pricing.prompt
|
||||
+ model_without_top.context_length
|
||||
* 0.2
|
||||
* model_without_top.sats_pricing.completion
|
||||
)
|
||||
|
||||
# Cancel and await the task
|
||||
|
||||
@@ -1742,7 +1742,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "routstr"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "cashu" },
|
||||
@@ -1758,8 +1758,6 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "cashu" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mypy" },
|
||||
{ name = "openai" },
|
||||
@@ -1768,7 +1766,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-benchmark" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "rich" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -1788,8 +1785,6 @@ requires-dist = [
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "aiohttp", specifier = ">=3.9.0" },
|
||||
{ name = "cashu", specifier = ">=0.17.0" },
|
||||
{ name = "fastapi", specifier = ">=0.115.14" },
|
||||
{ name = "httpx", specifier = ">=0.25.2" },
|
||||
{ name = "mypy", specifier = ">=1.15.0" },
|
||||
{ name = "openai", specifier = ">=1.76.0" },
|
||||
@@ -1798,7 +1793,6 @@ dev = [
|
||||
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
||||
{ name = "pytest-benchmark", specifier = ">=4.0.0" },
|
||||
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
||||
{ name = "rich", specifier = ">=14.1.0" },
|
||||
{ name = "ruff", specifier = ">=0.11.6" },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user