Merge pull request #107 from Routstr/main

This commit is contained in:
shroominic
2025-08-07 15:56:11 -03:00
committed by GitHub
7 changed files with 112 additions and 43 deletions
+1
View File
@@ -10,6 +10,7 @@ wallet.sqlite3
*models.json
.cashu
.dockerignore
relay-data
compose.override.yml
+2
View File
@@ -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
-1
View File
@@ -77,7 +77,6 @@ app.add_middleware(
@app.get("/", include_in_schema=False)
@app.get("/v1/info")
async def info() -> dict:
logger.info("Info endpoint accessed")
return {
"name": app.title,
"description": app.description,
+8 -1
View File
@@ -1,5 +1,6 @@
import json
import os
from typing import Optional
from fastapi import HTTPException, Response
@@ -148,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",
@@ -159,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(
{
@@ -171,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),
)
+65 -17
View File
@@ -2,6 +2,7 @@ import asyncio
import json
import os
from pathlib import Path
from urllib.request import urlopen
from fastapi import APIRouter
from pydantic.v1 import BaseModel
@@ -51,31 +52,76 @@ class Model(BaseModel):
MODELS: list[Model] = []
def fetch_openrouter_models(source_filter: str | None = None) -> list[dict]:
"""Fetches model information from OpenRouter API."""
base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
try:
with urlopen(f"{base_url}/models") as response:
data = json.loads(response.read().decode("utf-8"))
models_data: list[dict] = []
for model in data.get("data", []):
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_id == "openrouter/auto"
or model_id == "google/gemini-2.5-pro-exp-03-25"
):
continue
models_data.append(model)
return models_data
except Exception as e:
print(f"Error fetching models from OpenRouter API: {e}")
return []
def load_models() -> list[Model]:
"""Load model definitions from a JSON file.
"""Load model definitions from a JSON file or auto-generate from OpenRouter API.
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.
If a user-provided models.json exists, it will be used. Otherwise, models are
automatically fetched from OpenRouter API in memory. If the example file exists
and no user file is provided, it will be used as a fallback.
"""
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}")
# Check if user has actively provided a models.json file
if models_path.exists():
print(f"Loading models from user-provided file: {models_path}")
try:
with models_path.open("r") as f:
data = json.load(f)
return [Model(**model) for model in data.get("models", [])]
except Exception as e:
print(f"Error loading models from {models_path}: {e}")
# Fall through to auto-generation
# Auto-generate models from OpenRouter API
print("Auto-generating models from OpenRouter API")
source_filter = os.getenv("SOURCE")
source_filter = source_filter if source_filter and source_filter.strip() else None
models_data = fetch_openrouter_models(source_filter=source_filter)
if not models_data:
print("Failed to fetch models from OpenRouter API")
return []
return [Model(**model) for model in data.get("models", [])]
print(f"Successfully fetched {len(models_data)} models from OpenRouter API")
return [Model(**model) for model in models_data]
MODELS = load_models()
@@ -91,7 +137,9 @@ async def update_sats_pricing() -> None:
)
mspp = model.sats_pricing.prompt
mspc = model.sats_pricing.completion
if model.top_provider:
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
):
+19 -15
View File
@@ -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
+17 -9
View File
@@ -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: