Compare commits

...
Author SHA1 Message Date
Shroominic 35c9ce5699 extra temp logs for debugging 2026-01-19 12:08:17 +08:00
Shroominic 24015ebec1 Revert "Merge remote-tracking branch 'origin/mock-upstream-with-testnut-mint' into v0.2.2"
This reverts commit 5a4ba60072, reversing
changes made to eed5bc5b04.
2026-01-13 06:31:24 +08:00
6 changed files with 33 additions and 306 deletions
+24 -2
View File
@@ -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)
+1 -8
View File
@@ -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")
-20
View File
@@ -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(
-265
View File
@@ -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
-8
View File
@@ -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
View File
@@ -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(