diff --git a/.env.example b/.env.example index 3dcf8ec9..c69b923f 100644 --- a/.env.example +++ b/.env.example @@ -17,8 +17,10 @@ COST_PER_REQUEST="10" COST_PER_1K_INPUT_TOKENS = "0" COST_PER_1K_OUTPUT_TOKENS = "0" -# If set to true, make sure model pricings are defined in models.json +# 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 = "false" +# MODELS_PATH="models.json" # Time in seconds between each automatically refunding funds to users whose API keys have expired # Setting this to "0" disables automatic refunds diff --git a/README.md b/README.md index 936e0e9a..be4b7bf0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # proxy -a reverse proxy that you can plug in front of any openai compatible api endpoint -to handle payments using the cashu protocol (Bitcoin L3) +a reverse proxy that you can plug in front of any OpenAI compatible API +endpoint to handle payments using the Cashu protocol (Bitcoin L3). + +Model pricing information is loaded from ``models.json`` by default. If that +file is not present, the bundled ``models.example.json`` will be used. You can +specify a custom path with the ``MODELS_PATH`` environment variable. diff --git a/router/auth.py b/router/auth.py index a6010c78..2c8f4d1a 100644 --- a/router/auth.py +++ b/router/auth.py @@ -105,7 +105,7 @@ async def pay_for_request( request: Request | None, request_body: bytes | None = None, ) -> None: - if MODEL_BASED_PRICING and os.path.exists("models.json"): + if MODEL_BASED_PRICING and MODELS: if request_body: body = json.loads(request_body) else: @@ -180,7 +180,7 @@ async def adjust_payment_for_tokens( MSATS_PER_1K_INPUT_TOKENS = COST_PER_1K_INPUT_TOKENS MSATS_PER_1K_OUTPUT_TOKENS = COST_PER_1K_OUTPUT_TOKENS - if MODEL_BASED_PRICING and os.path.exists("models.json"): + if MODEL_BASED_PRICING and MODELS: response_model = response_data.get("model", "") if response_model not in [model.id for model in MODELS]: raise HTTPException( diff --git a/router/models.py b/router/models.py index 8bda6b77..965313b7 100644 --- a/router/models.py +++ b/router/models.py @@ -1,5 +1,7 @@ import asyncio import json +import os +from pathlib import Path from pydantic.v1 import BaseModel from .price import sats_usd_ask_price @@ -44,8 +46,35 @@ class Model(BaseModel): MODELS: list[Model] = [] -with open("models.json", "r") as f: - MODELS = [Model(**model) for model in json.load(f)["models"]] + +def load_models() -> list[Model]: + """Load model definitions from a JSON file. + + The file path can be specified via the ``MODELS_PATH`` environment variable. + If ``models.json`` is not found, the bundled ``models.example.json`` is used + as a fallback. If neither file exists or an error occurs while loading, an + empty list is returned. + """ + + models_path = Path(os.environ.get("MODELS_PATH", "models.json")) + if not models_path.exists(): + example = Path(__file__).resolve().parent.parent / "models.example.json" + if example.exists(): + models_path = example + else: + return [] + + try: + with models_path.open("r") as f: + data = json.load(f) + except Exception as e: # pragma: no cover - log and continue + print(f"Error loading models from {models_path}: {e}") + return [] + + return [Model(**model) for model in data.get("models", [])] + + +MODELS = load_models() async def update_sats_pricing() -> None: