Compare commits

...
Author SHA1 Message Date
Shroominic aa3011a7f5 wip 2026-01-10 17:47:42 +08:00
shroominicandGitHub 367265b9fe Merge pull request #300 from Routstr/reset-reserved-balance-on-startup
Reset reserved balance on startup
2026-01-10 08:50:32 +08:00
shroominicandGitHub 0b3ccb5fb0 Merge pull request #299 from Routstr/urgent-reserved-balance-fix
Reserved balance fix
2026-01-10 08:50:20 +08:00
shroominicandGitHub dbd43f52fb Merge pull request #298 from Routstr/fix-provider-balance
Fix provider balance
2026-01-10 08:49:23 +08:00
Shroominic 39657ed64f reset reserved balance on startup 2026-01-09 17:36:04 +08:00
Shroominic 493b4f0f1f urgent reserved balance fix 2026-01-09 17:32:35 +08:00
Shroominic ca7e8bec71 fmt 2026-01-09 16:53:25 +08:00
Shroominic c4cc09d61e fix provider balance not displaying when 0 2026-01-09 16:50:10 +08:00
Shroominic 21d363f6aa bump v0.2.2 2026-01-06 19:26:10 +01:00
shroominicandGitHub 5e21f6ccbc Merge pull request #292 from Routstr/fix-not-enough-inputs-to-melt
Fix not enough inputs to melt
2026-01-06 19:24:33 +01:00
shroominicandGitHub b7603dcf69 Merge pull request #295 from Routstr/update-ui-deps
update vuln deps
2026-01-06 17:38:13 +01:00
Shroominic d192a6a6b4 fix not enough inputs to melt bug 2026-01-05 00:34:58 +01:00
12 changed files with 113 additions and 21 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6 -2
View File
@@ -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:
+3
View File
@@ -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")
+1 -1
View File
@@ -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
+28
View File
@@ -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
View File
@@ -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",
+6
View File
@@ -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
View File
@@ -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)
+6 -1
View File
@@ -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;
}
Generated
+1 -1
View File
@@ -1878,7 +1878,7 @@ wheels = [
[[package]]
name = "routstr"
version = "0.2.1"
version = "0.2.2"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },