mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-04 09:06:15 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c9ce5699 | ||
|
|
24015ebec1 |
+24
-2
@@ -142,6 +142,7 @@ async def refund_wallet_endpoint(
|
|||||||
authorization: Annotated[str, Header(...)],
|
authorization: Annotated[str, Header(...)],
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
|
logger.info("Refund request received", extra={"authorization": authorization})
|
||||||
if not authorization.startswith("Bearer "):
|
if not authorization.startswith("Bearer "):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
@@ -157,6 +158,7 @@ async def refund_wallet_endpoint(
|
|||||||
|
|
||||||
remaining_balance_msats: int = key.total_balance
|
remaining_balance_msats: int = key.total_balance
|
||||||
|
|
||||||
|
logger.info("Refunding key: %s", key.dict())
|
||||||
if key.refund_currency == "sat":
|
if key.refund_currency == "sat":
|
||||||
remaining_balance = remaining_balance_msats // 1000
|
remaining_balance = remaining_balance_msats // 1000
|
||||||
else:
|
else:
|
||||||
@@ -172,6 +174,15 @@ async def refund_wallet_endpoint(
|
|||||||
if key.refund_address:
|
if key.refund_address:
|
||||||
from .core.settings import settings as global_settings
|
from .core.settings import settings as global_settings
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sending refund to lnurl",
|
||||||
|
extra={
|
||||||
|
"remaining_balance": remaining_balance,
|
||||||
|
"refund_currency": key.refund_currency,
|
||||||
|
"refund_mint_url": key.refund_mint_url,
|
||||||
|
"refund_address": key.refund_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
await send_to_lnurl(
|
await send_to_lnurl(
|
||||||
remaining_balance,
|
remaining_balance,
|
||||||
key.refund_currency or "sat",
|
key.refund_currency or "sat",
|
||||||
@@ -180,7 +191,16 @@ async def refund_wallet_endpoint(
|
|||||||
)
|
)
|
||||||
result = {"recipient": key.refund_address}
|
result = {"recipient": key.refund_address}
|
||||||
else:
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Sending refund as token",
|
||||||
|
extra={
|
||||||
|
"remaining_balance": remaining_balance,
|
||||||
|
"refund_currency": key.refund_currency,
|
||||||
|
"refund_mint_url": key.refund_mint_url,
|
||||||
|
},
|
||||||
|
)
|
||||||
refund_currency = key.refund_currency or "sat"
|
refund_currency = key.refund_currency or "sat"
|
||||||
|
|
||||||
token = await send_token(
|
token = await send_token(
|
||||||
remaining_balance, refund_currency, key.refund_mint_url
|
remaining_balance, refund_currency, key.refund_mint_url
|
||||||
)
|
)
|
||||||
@@ -191,10 +211,12 @@ async def refund_wallet_endpoint(
|
|||||||
else:
|
else:
|
||||||
result["msats"] = str(remaining_balance_msats)
|
result["msats"] = str(remaining_balance_msats)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException as e:
|
||||||
|
logger.error("Refund failed", extra={"exception": e})
|
||||||
# Re-raise HTTP exceptions (like 400 for balance too small)
|
# Re-raise HTTP exceptions (like 400 for balance too small)
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error("Refund failed", extra={"exception": e})
|
||||||
# If refund fails, don't modify the database
|
# If refund fails, don't modify the database
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
if (
|
if (
|
||||||
@@ -205,7 +227,7 @@ async def refund_wallet_endpoint(
|
|||||||
):
|
):
|
||||||
raise HTTPException(status_code=503, detail="Mint service unavailable")
|
raise HTTPException(status_code=503, detail="Mint service unavailable")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="Refund failed")
|
raise HTTPException(status_code=500, detail="Refund failed: " + error_msg)
|
||||||
|
|
||||||
await _refund_cache_set(bearer_value, result)
|
await _refund_cache_set(bearer_value, result)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic.v1 import BaseModel, BaseSettings, Field, validator
|
from pydantic.v1 import BaseModel, BaseSettings, Field
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@@ -37,13 +37,6 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# Cashu
|
# Cashu
|
||||||
cashu_mints: list[str] = Field(default_factory=list, env="CASHU_MINTS")
|
cashu_mints: list[str] = Field(default_factory=list, env="CASHU_MINTS")
|
||||||
|
|
||||||
@validator("cashu_mints", pre=True, each_item=True)
|
|
||||||
def normalize_mint_url(cls, v: str) -> str:
|
|
||||||
if isinstance(v, str):
|
|
||||||
return v.rstrip("/")
|
|
||||||
return v
|
|
||||||
|
|
||||||
receive_ln_address: str = Field(default="", env="RECEIVE_LN_ADDRESS")
|
receive_ln_address: str = Field(default="", env="RECEIVE_LN_ADDRESS")
|
||||||
primary_mint: str = Field(default="", env="PRIMARY_MINT_URL")
|
primary_mint: str = Field(default="", env="PRIMARY_MINT_URL")
|
||||||
primary_mint_unit: str = Field(default="sat", env="PRIMARY_MINT_UNIT")
|
primary_mint_unit: str = Field(default="sat", env="PRIMARY_MINT_UNIT")
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from .core.db import (
|
|||||||
create_session,
|
create_session,
|
||||||
get_session,
|
get_session,
|
||||||
)
|
)
|
||||||
from .core.settings import settings
|
|
||||||
from .payment.helpers import (
|
from .payment.helpers import (
|
||||||
calculate_discounted_max_cost,
|
calculate_discounted_max_cost,
|
||||||
check_token_balance,
|
check_token_balance,
|
||||||
@@ -26,7 +25,6 @@ from .payment.helpers import (
|
|||||||
from .payment.models import Model
|
from .payment.models import Model
|
||||||
from .upstream import BaseUpstreamProvider
|
from .upstream import BaseUpstreamProvider
|
||||||
from .upstream.helpers import init_upstreams
|
from .upstream.helpers import init_upstreams
|
||||||
from .wallet import deserialize_token_from_string
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
proxy_router = APIRouter()
|
proxy_router = APIRouter()
|
||||||
@@ -148,24 +146,6 @@ async def proxy(
|
|||||||
else:
|
else:
|
||||||
model_id = request_body_dict.get("model", "unknown")
|
model_id = request_body_dict.get("model", "unknown")
|
||||||
|
|
||||||
if "https://testnut.cashu.space" in settings.cashu_mints:
|
|
||||||
try:
|
|
||||||
token_str = None
|
|
||||||
if x_cashu_header := headers.get("x-cashu"):
|
|
||||||
token_str = x_cashu_header
|
|
||||||
elif auth_header := headers.get("authorization"):
|
|
||||||
parts = auth_header.split(" ")
|
|
||||||
if len(parts) > 1 and not parts[1].startswith("sk-"):
|
|
||||||
token_str = parts[1]
|
|
||||||
|
|
||||||
if token_str:
|
|
||||||
token_obj = deserialize_token_from_string(token_str)
|
|
||||||
if token_obj.mint == "https://testnut.cashu.space":
|
|
||||||
model_id = "mock/gpt-420-mock"
|
|
||||||
request_body_dict["model"] = model_id
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
model_obj = get_model_instance(model_id)
|
model_obj = get_model_instance(model_id)
|
||||||
if not model_obj:
|
if not model_obj:
|
||||||
return create_error_response(
|
return create_error_response(
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
from typing import AsyncIterator
|
|
||||||
|
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.responses import Response, StreamingResponse
|
|
||||||
|
|
||||||
from ..core.db import ApiKey, AsyncSession
|
|
||||||
from ..payment.models import Architecture, Model, Pricing
|
|
||||||
from .base import BaseUpstreamProvider
|
|
||||||
|
|
||||||
|
|
||||||
class MockUpstreamProvider(BaseUpstreamProvider):
|
|
||||||
"""Fack Mock Upstream provider specifically for Testing."""
|
|
||||||
|
|
||||||
provider_type = "mock"
|
|
||||||
|
|
||||||
async def forward_request(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
path: str,
|
|
||||||
headers: dict,
|
|
||||||
request_body: bytes | None,
|
|
||||||
key: ApiKey,
|
|
||||||
max_cost_for_model: int,
|
|
||||||
session: AsyncSession,
|
|
||||||
model_obj: Model,
|
|
||||||
) -> Response | StreamingResponse:
|
|
||||||
if path.endswith("chat/completions"):
|
|
||||||
is_streaming = False
|
|
||||||
if request_body:
|
|
||||||
request_data = json.loads(request_body)
|
|
||||||
is_streaming = request_data.get("stream", False)
|
|
||||||
|
|
||||||
if is_streaming:
|
|
||||||
|
|
||||||
async def fake_streaming_response(
|
|
||||||
chunk_size: int | None = None,
|
|
||||||
) -> AsyncIterator[bytes]:
|
|
||||||
suffix = random.randint(1000, 9999)
|
|
||||||
req_id = f"gen-mock-stream-{suffix}"
|
|
||||||
created = 1766138895
|
|
||||||
model = "mock/gpt-420-mock"
|
|
||||||
|
|
||||||
def make_chunk(
|
|
||||||
delta: dict,
|
|
||||||
finish_reason: str | None = None,
|
|
||||||
usage: dict | None = None,
|
|
||||||
) -> bytes:
|
|
||||||
chunk = {
|
|
||||||
"id": req_id,
|
|
||||||
"provider": "MockProvider",
|
|
||||||
"model": model,
|
|
||||||
"object": "chat.completion.chunk",
|
|
||||||
"created": created,
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"delta": delta,
|
|
||||||
"finish_reason": finish_reason,
|
|
||||||
"native_finish_reason": "completed"
|
|
||||||
if finish_reason
|
|
||||||
else None,
|
|
||||||
"logprobs": None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if usage:
|
|
||||||
chunk["usage"] = usage
|
|
||||||
return f"data: {json.dumps(chunk)}\n\n".encode()
|
|
||||||
|
|
||||||
# 1. Initial chunk
|
|
||||||
yield make_chunk({"role": "assistant", "content": ""})
|
|
||||||
await asyncio.sleep(0.02)
|
|
||||||
|
|
||||||
# 2. Reasoning chunks
|
|
||||||
reasoning_tokens = ["Mock", " reason", "ing", "..."]
|
|
||||||
for token in reasoning_tokens:
|
|
||||||
delta = {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "",
|
|
||||||
"reasoning": token,
|
|
||||||
"reasoning_details": [
|
|
||||||
{
|
|
||||||
"type": "reasoning.summary",
|
|
||||||
"summary": token,
|
|
||||||
"format": "openai-responses-v1",
|
|
||||||
"index": 0,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
yield make_chunk(delta)
|
|
||||||
await asyncio.sleep(0.03)
|
|
||||||
|
|
||||||
# 3. Content chunks
|
|
||||||
content_tokens = ["This", " is", " a", " mock", " stream", "."]
|
|
||||||
for token in content_tokens:
|
|
||||||
yield make_chunk({"role": "assistant", "content": token})
|
|
||||||
await asyncio.sleep(0.03)
|
|
||||||
|
|
||||||
# 4. Finish chunk
|
|
||||||
yield make_chunk(
|
|
||||||
{"role": "assistant", "content": ""}, finish_reason="stop"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Usage chunk
|
|
||||||
usage_data = {
|
|
||||||
"prompt_tokens": 10,
|
|
||||||
"completion_tokens": 20,
|
|
||||||
"total_tokens": 30,
|
|
||||||
"cost": 0.001,
|
|
||||||
"is_byok": False,
|
|
||||||
"prompt_tokens_details": {
|
|
||||||
"cached_tokens": 0,
|
|
||||||
"audio_tokens": 0,
|
|
||||||
"video_tokens": 0,
|
|
||||||
},
|
|
||||||
"cost_details": {
|
|
||||||
"upstream_inference_cost": None,
|
|
||||||
"upstream_inference_prompt_cost": 0,
|
|
||||||
"upstream_inference_completions_cost": 0.001,
|
|
||||||
},
|
|
||||||
"completion_tokens_details": {
|
|
||||||
"reasoning_tokens": 10,
|
|
||||||
"image_tokens": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
usage_chunk = {
|
|
||||||
"id": req_id,
|
|
||||||
"provider": "MockProvider",
|
|
||||||
"model": model,
|
|
||||||
"object": "chat.completion.chunk",
|
|
||||||
"created": created,
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"delta": {"role": "assistant", "content": ""},
|
|
||||||
"finish_reason": None,
|
|
||||||
"native_finish_reason": None,
|
|
||||||
"logprobs": None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"usage": usage_data,
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(usage_chunk)}\n\n".encode()
|
|
||||||
|
|
||||||
# 6. DONE
|
|
||||||
yield b"data: [DONE]\n\n"
|
|
||||||
|
|
||||||
# 7. Cost
|
|
||||||
cost_chunk = {
|
|
||||||
"cost": {
|
|
||||||
"base_msats": 0,
|
|
||||||
"input_msats": 2,
|
|
||||||
"output_msats": 10,
|
|
||||||
"total_msats": 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(cost_chunk)}\n\n".encode()
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
fake_streaming_response(),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
suffix = random.randint(1000, 9999)
|
|
||||||
content_dict = {
|
|
||||||
"id": f"gen-mock-{suffix}",
|
|
||||||
"provider": "MockProvider",
|
|
||||||
"model": "mock/gpt-5-mini",
|
|
||||||
"object": "chat.completion",
|
|
||||||
"created": 1766138655,
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"logprobs": None,
|
|
||||||
"finish_reason": "length",
|
|
||||||
"native_finish_reason": "max_output_tokens",
|
|
||||||
"index": 0,
|
|
||||||
"message": {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": f"Mock Content {suffix}",
|
|
||||||
"refusal": None,
|
|
||||||
"reasoning": f"Mock Reasoning {suffix}",
|
|
||||||
"reasoning_details": [
|
|
||||||
{
|
|
||||||
"format": "openai-responses-v1",
|
|
||||||
"index": 0,
|
|
||||||
"type": "reasoning.summary",
|
|
||||||
"summary": f"Mock Summary {suffix}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": f"rs_mock_{suffix}",
|
|
||||||
"format": "openai-responses-v1",
|
|
||||||
"index": 0,
|
|
||||||
"type": "reasoning.encrypted",
|
|
||||||
"data": "mock_encrypted_data",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"usage": {
|
|
||||||
"prompt_tokens": 10,
|
|
||||||
"completion_tokens": 10,
|
|
||||||
"total_tokens": 20,
|
|
||||||
"cost": 0,
|
|
||||||
"is_byok": False,
|
|
||||||
"prompt_tokens_details": {
|
|
||||||
"cached_tokens": 0,
|
|
||||||
"audio_tokens": 0,
|
|
||||||
"video_tokens": 0,
|
|
||||||
},
|
|
||||||
"cost_details": {
|
|
||||||
"upstream_inference_cost": None,
|
|
||||||
"upstream_inference_prompt_cost": 0,
|
|
||||||
"upstream_inference_completions_cost": 0,
|
|
||||||
},
|
|
||||||
"completion_tokens_details": {
|
|
||||||
"reasoning_tokens": 5,
|
|
||||||
"image_tokens": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"cost": {
|
|
||||||
"base_msats": 0,
|
|
||||||
"input_msats": 0,
|
|
||||||
"output_msats": 0,
|
|
||||||
"total_msats": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return Response(json.dumps(content_dict).encode(), 200)
|
|
||||||
|
|
||||||
elif path.endswith("embeddings"):
|
|
||||||
raise NotImplementedError
|
|
||||||
elif path.endswith("responses"):
|
|
||||||
raise NotImplementedError
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def fetch_models(self) -> list[Model]:
|
|
||||||
return [
|
|
||||||
Model(
|
|
||||||
id="mock/gpt-420-mock",
|
|
||||||
name="mock/gpt-420-mock",
|
|
||||||
created=0,
|
|
||||||
description="mock model for testing",
|
|
||||||
context_length=8192,
|
|
||||||
architecture=Architecture(
|
|
||||||
modality="text",
|
|
||||||
input_modalities=["text"],
|
|
||||||
output_modalities=["text"],
|
|
||||||
tokenizer="",
|
|
||||||
instruct_type=None,
|
|
||||||
),
|
|
||||||
pricing=Pricing(prompt=0.01, completion=0.01),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
def transform_model_name(self, model_id: str) -> str:
|
|
||||||
return "fake-model"
|
|
||||||
|
|
||||||
async def get_balance(self) -> float | None:
|
|
||||||
return 420.69
|
|
||||||
@@ -218,14 +218,6 @@ async def init_upstreams() -> list[BaseUpstreamProvider]:
|
|||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
upstreams = [p for p in results if p is not None]
|
upstreams = [p for p in results if p is not None]
|
||||||
|
|
||||||
if "https://testnut.cashu.space" in settings.cashu_mints:
|
|
||||||
from .fake import MockUpstreamProvider
|
|
||||||
|
|
||||||
mock_provider = MockUpstreamProvider("mock", "mock")
|
|
||||||
await mock_provider.refresh_models_cache()
|
|
||||||
upstreams.append(mock_provider)
|
|
||||||
logger.info("Initialized MockUpstreamProvider for testnut mint")
|
|
||||||
|
|
||||||
return upstreams
|
return upstreams
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -40,16 +40,23 @@ async def recieve_token(
|
|||||||
async def send(amount: int, unit: str, mint_url: str | None = None) -> tuple[int, str]:
|
async def send(amount: int, unit: str, mint_url: str | None = None) -> tuple[int, str]:
|
||||||
"""Internal send function - returns amount and serialized token"""
|
"""Internal send function - returns amount and serialized token"""
|
||||||
wallet: Wallet = await get_wallet(mint_url or settings.primary_mint, unit)
|
wallet: Wallet = await get_wallet(mint_url or settings.primary_mint, unit)
|
||||||
|
logger.info("Sending", extra={"amount": amount, "unit": unit, "mint_url": mint_url})
|
||||||
proofs = get_proofs_per_mint_and_unit(
|
proofs = get_proofs_per_mint_and_unit(
|
||||||
wallet, mint_url or settings.primary_mint, unit
|
wallet, mint_url or settings.primary_mint, unit
|
||||||
)
|
)
|
||||||
|
logger.info("Proofs", extra={"proofs": proofs})
|
||||||
|
logger.info(
|
||||||
|
"Selecting to send",
|
||||||
|
extra={"amount": amount, "unit": unit, "mint_url": mint_url},
|
||||||
|
)
|
||||||
send_proofs, _ = await wallet.select_to_send(
|
send_proofs, _ = await wallet.select_to_send(
|
||||||
proofs, amount, set_reserved=True, include_fees=False
|
proofs, amount, set_reserved=True, include_fees=False
|
||||||
)
|
)
|
||||||
|
logger.info("Send proofs", extra={"send_proofs": send_proofs})
|
||||||
token = await wallet.serialize_proofs(
|
token = await wallet.serialize_proofs(
|
||||||
send_proofs, include_dleq=False, legacy=False, memo=None
|
send_proofs, include_dleq=False, legacy=False, memo=None
|
||||||
)
|
)
|
||||||
|
logger.info("Token created", extra={"token": token})
|
||||||
return amount, token
|
return amount, token
|
||||||
|
|
||||||
|
|
||||||
@@ -313,8 +320,6 @@ async def periodic_payout() -> None:
|
|||||||
try:
|
try:
|
||||||
async with db.create_session() as session:
|
async with db.create_session() as session:
|
||||||
for mint_url in settings.cashu_mints:
|
for mint_url in settings.cashu_mints:
|
||||||
if mint_url == "https://testnut.cashu.space":
|
|
||||||
continue
|
|
||||||
for unit in ["sat", "msat"]:
|
for unit in ["sat", "msat"]:
|
||||||
wallet = await get_wallet(mint_url, unit)
|
wallet = await get_wallet(mint_url, unit)
|
||||||
proofs = get_proofs_per_mint_and_unit(
|
proofs = get_proofs_per_mint_and_unit(
|
||||||
|
|||||||
Reference in New Issue
Block a user