mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-30 23:36:15 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3011a7f5 | ||
|
|
367265b9fe | ||
|
|
0b3ccb5fb0 | ||
|
|
dbd43f52fb | ||
|
|
39657ed64f | ||
|
|
493b4f0f1f | ||
|
|
ca7e8bec71 | ||
|
|
c4cc09d61e | ||
|
|
21d363f6aa | ||
|
|
5e21f6ccbc | ||
|
|
b7603dcf69 | ||
|
|
d192a6a6b4 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "routstr"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "Payment proxy for your LLM endpoint using cashu and nostr."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+2
-1
@@ -154,7 +154,8 @@ async def refund_wallet_endpoint(
|
||||
return cached
|
||||
|
||||
key: ApiKey = await validate_bearer_key(bearer_value, session)
|
||||
remaining_balance_msats: int = key.balance
|
||||
|
||||
remaining_balance_msats: int = key.total_balance
|
||||
|
||||
if key.refund_currency == "sat":
|
||||
remaining_balance = remaining_balance_msats // 1000
|
||||
|
||||
+9
-1
@@ -6,7 +6,7 @@ from typing import AsyncGenerator
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy.ext.asyncio.engine import create_async_engine
|
||||
from sqlmodel import Field, Relationship, SQLModel, func, select
|
||||
from sqlmodel import Field, Relationship, SQLModel, func, select, update
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from .logging import get_logger
|
||||
@@ -53,6 +53,14 @@ class ApiKey(SQLModel, table=True): # type: ignore
|
||||
return self.balance - self.reserved_balance
|
||||
|
||||
|
||||
async def reset_all_reserved_balances(session: AsyncSession) -> None:
|
||||
logger.info("Resetting all reserved balances to 0")
|
||||
stmt = update(ApiKey).values(reserved_balance=0)
|
||||
await session.exec(stmt) # type: ignore[call-overload]
|
||||
await session.commit()
|
||||
logger.info("Reserved balances reset successfully")
|
||||
|
||||
|
||||
class ModelRow(SQLModel, table=True): # type: ignore
|
||||
__tablename__ = "models"
|
||||
id: str = Field(primary_key=True)
|
||||
|
||||
@@ -33,9 +33,9 @@ setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
if os.getenv("VERSION_SUFFIX") is not None:
|
||||
__version__ = f"0.2.1-{os.getenv('VERSION_SUFFIX')}"
|
||||
__version__ = f"0.2.2-{os.getenv('VERSION_SUFFIX')}"
|
||||
else:
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.2.2"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -61,6 +61,10 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
# Initialize application settings (env -> computed -> DB precedence)
|
||||
async with create_session() as session:
|
||||
s = await SettingsService.initialize(session)
|
||||
if s.reset_reserved_balance_on_startup:
|
||||
from .db import reset_all_reserved_balances
|
||||
|
||||
await reset_all_reserved_balances(session)
|
||||
|
||||
# Apply app metadata from settings
|
||||
try:
|
||||
|
||||
@@ -61,6 +61,9 @@ class Settings(BaseSettings):
|
||||
tolerance_percentage: float = Field(default=1.0, env="TOLERANCE_PERCENTAGE")
|
||||
# Minimum per-request charge in millisatoshis when model pricing is free/zero
|
||||
min_request_msat: int = Field(default=1, env="MIN_REQUEST_MSAT")
|
||||
reset_reserved_balance_on_startup: bool = Field(
|
||||
default=True, env="RESET_RESERVED_BALANCE_ON_STARTUP"
|
||||
) # deactivate in horizontal scaling setups
|
||||
|
||||
# Network
|
||||
cors_origins: list[str] = Field(default_factory=lambda: ["*"], env="CORS_ORIGINS")
|
||||
|
||||
@@ -283,7 +283,7 @@ async def raw_send_to_lnurl(
|
||||
f"({min_sendable_sat} - {max_sendable_sat} {unit})"
|
||||
)
|
||||
|
||||
estimated_fees_sat = int(max(math.ceil((amount_msat / 1000) * 0.01), 2))
|
||||
estimated_fees_sat = int(max(math.ceil((amount_msat / 1000) * 0.01), 2)) + 1
|
||||
estimated_fees_msat = estimated_fees_sat * 1000
|
||||
final_amount = amount_msat - estimated_fees_msat
|
||||
|
||||
|
||||
@@ -61,6 +61,34 @@ class AnthropicUpstreamProvider(BaseUpstreamProvider):
|
||||
model_id = fixed_transforms[model_id]
|
||||
return model_id
|
||||
|
||||
def transform_parameters(self, data: dict) -> dict:
|
||||
"""Transform parameters for Anthropic API compatibility."""
|
||||
if "reasoning" in data:
|
||||
reasoning = data.pop("reasoning")
|
||||
if isinstance(reasoning, dict) and "effort" in reasoning:
|
||||
effort = reasoning.pop("effort")
|
||||
if effort == "low":
|
||||
data["thinking"] = {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 8192,
|
||||
}
|
||||
elif effort == "medium":
|
||||
data["thinking"] = {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 16384,
|
||||
}
|
||||
elif effort == "high":
|
||||
data["thinking"] = {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 32768,
|
||||
}
|
||||
elif effort == "none":
|
||||
data["thinking"] = {
|
||||
"type": "disabled",
|
||||
}
|
||||
|
||||
return super().transform_parameters(data)
|
||||
|
||||
async def fetch_models(self) -> list[Model]:
|
||||
"""Fetch Anthropic models from OpenRouter API filtered by anthropic source."""
|
||||
models_data = await async_fetch_openrouter_models(source_filter="anthropic")
|
||||
|
||||
+49
-12
@@ -276,19 +276,17 @@ class BaseUpstreamProvider:
|
||||
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if isinstance(data, dict) and "model" in data:
|
||||
if isinstance(data, dict):
|
||||
original_model = model_obj.id
|
||||
transformed_model = self.transform_model_name(original_model)
|
||||
data["model"] = transformed_model
|
||||
logger.debug(
|
||||
"Transformed model name in request",
|
||||
extra={
|
||||
"original": original_model,
|
||||
"transformed": transformed_model,
|
||||
"provider": self.provider_type or self.base_url,
|
||||
},
|
||||
)
|
||||
return json.dumps(data).encode()
|
||||
|
||||
data = self.update_parameters_from_model_name(data, original_model)
|
||||
|
||||
if "model" in data:
|
||||
transformed_model = self.transform_model_name(original_model)
|
||||
data["model"] = transformed_model
|
||||
|
||||
data = self.transform_parameters(data)
|
||||
return json.dumps(data).encode()
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"Could not transform request body",
|
||||
@@ -300,6 +298,43 @@ class BaseUpstreamProvider:
|
||||
|
||||
return body
|
||||
|
||||
def update_parameters_from_model_name(self, data: dict, model_id: str) -> dict:
|
||||
"""Extract parameters from model name for provider-specific requirements.
|
||||
|
||||
Args:
|
||||
data: Original request body data
|
||||
|
||||
Returns:
|
||||
Transformed request body data
|
||||
"""
|
||||
if model_id.endswith(":thinking"):
|
||||
model_id = model_id.removesuffix(":thinking")
|
||||
data["reasoning"] = {"effort": "medium"}
|
||||
if model_id.endswith("-thinking"):
|
||||
model_id = model_id.removesuffix("-thinking")
|
||||
data["reasoning"] = {"effort": "medium"}
|
||||
|
||||
return data
|
||||
|
||||
def transform_parameters(self, data: dict) -> dict:
|
||||
"""Transform parameters for provider-specific requirements.
|
||||
|
||||
Args:
|
||||
data: Original request body data
|
||||
|
||||
Returns:
|
||||
Transformed request body data
|
||||
"""
|
||||
# generic input to messages transformation
|
||||
if (
|
||||
"input" in data
|
||||
and isinstance(data["input"], list)
|
||||
and isinstance(data["input"][0], dict)
|
||||
and "role" in data["input"][0]
|
||||
):
|
||||
data["messages"] = data.pop("input")
|
||||
return data
|
||||
|
||||
def _extract_upstream_error_message(
|
||||
self, body_bytes: bytes
|
||||
) -> tuple[str, str | None]:
|
||||
@@ -1094,7 +1129,9 @@ class BaseUpstreamProvider:
|
||||
|
||||
url = f"{self.base_url}/{path}"
|
||||
|
||||
print(f"request_body: {request_body[:100]!r}")
|
||||
transformed_body = self.prepare_request_body(request_body, model_obj)
|
||||
print(f"transformed_body: {transformed_body[:100]!r}")
|
||||
|
||||
logger.info(
|
||||
"Forwarding request to upstream",
|
||||
|
||||
@@ -38,6 +38,12 @@ class OpenAIUpstreamProvider(BaseUpstreamProvider):
|
||||
"platform_url": cls.platform_url,
|
||||
}
|
||||
|
||||
def transform_parameters(self, data: dict) -> dict:
|
||||
"""Transform parameters for OpenAI API compatibility."""
|
||||
if "max_tokens" in data:
|
||||
data["max_completion_tokens"] = data.pop("max_tokens")
|
||||
return super().transform_parameters(data)
|
||||
|
||||
def transform_model_name(self, model_id: str) -> str:
|
||||
"""Strip 'openai/' prefix for OpenAI API compatibility."""
|
||||
return model_id.removeprefix("openai/")
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ async def swap_to_primary_mint(
|
||||
amount_msat = token_amount
|
||||
else:
|
||||
raise ValueError("Invalid unit")
|
||||
estimated_fee_sat = math.ceil(max(amount_msat // 1000 * 0.01, 2))
|
||||
estimated_fee_sat = math.ceil(max(amount_msat // 1000 * 0.01, 2)) + 1
|
||||
amount_msat_after_fee = amount_msat - estimated_fee_sat * 1000
|
||||
primary_wallet = await get_wallet(settings.primary_mint, settings.primary_mint_unit)
|
||||
|
||||
|
||||
@@ -208,7 +208,12 @@ function ProviderBalance({
|
||||
return <Skeleton className='h-9 w-24' />;
|
||||
}
|
||||
|
||||
if (error || !balanceData?.ok || !balanceData.balance_data) {
|
||||
if (
|
||||
error ||
|
||||
!balanceData?.ok ||
|
||||
balanceData.balance_data === undefined ||
|
||||
balanceData.balance_data === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user