mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 02:54:37 +00:00
Merge pull request #107 from Routstr/main
This commit is contained in:
@@ -10,6 +10,7 @@ wallet.sqlite3
|
||||
*models.json
|
||||
.cashu
|
||||
.dockerignore
|
||||
relay-data
|
||||
|
||||
compose.override.yml
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user