mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-05 17:34:38 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87efed0021 | ||
|
|
3dfbd3815c | ||
|
|
6199f6467b | ||
|
|
9960e5596e | ||
|
|
a516a10737 | ||
|
|
be9b1da832 | ||
|
|
e4dd0aceae | ||
|
|
30170c2ec6 | ||
|
|
825bd38d8e | ||
|
|
d3b3152520 | ||
|
|
653b51452a | ||
|
|
aa00664cd6 | ||
|
|
ea677cf66b | ||
|
|
11868f9180 | ||
|
|
daae4cd2cb | ||
|
|
2cb8d2d744 | ||
|
|
22f7d198a6 | ||
|
|
73af5e23c7 | ||
|
|
eb5af32fac | ||
|
|
0f9df3ca77 | ||
|
|
3b1b3da847 | ||
|
|
a34583d2ca | ||
|
|
4eda9eaf1b | ||
|
|
b9879cbea7 |
+2
-24
@@ -142,7 +142,6 @@ 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,
|
||||||
@@ -158,7 +157,6 @@ 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:
|
||||||
@@ -174,15 +172,6 @@ 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",
|
||||||
@@ -191,16 +180,7 @@ 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
|
||||||
)
|
)
|
||||||
@@ -211,12 +191,10 @@ async def refund_wallet_endpoint(
|
|||||||
else:
|
else:
|
||||||
result["msats"] = str(remaining_balance_msats)
|
result["msats"] = str(remaining_balance_msats)
|
||||||
|
|
||||||
except HTTPException as e:
|
except HTTPException:
|
||||||
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 (
|
||||||
@@ -227,7 +205,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: " + error_msg)
|
raise HTTPException(status_code=500, detail="Refund failed")
|
||||||
|
|
||||||
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
|
from pydantic.v1 import BaseModel, BaseSettings, Field, validator
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +37,13 @@ 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,6 +16,7 @@ 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,
|
||||||
@@ -25,6 +26,7 @@ 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()
|
||||||
@@ -146,6 +148,24 @@ 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(
|
||||||
|
|||||||
+212
-1
@@ -1064,6 +1064,170 @@ class BaseUpstreamProvider:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def handle_streaming_messages_completion(
|
||||||
|
self, response: httpx.Response, key: ApiKey, max_cost_for_model: int
|
||||||
|
) -> StreamingResponse:
|
||||||
|
async def stream_with_cost(
|
||||||
|
max_cost_for_model: int,
|
||||||
|
) -> AsyncGenerator[bytes, None]:
|
||||||
|
stored_chunks: list[bytes] = []
|
||||||
|
usage_finalized: bool = False
|
||||||
|
last_model_seen: str | None = None
|
||||||
|
input_tokens: int = 0
|
||||||
|
output_tokens: int = 0
|
||||||
|
|
||||||
|
async def finalize_without_usage() -> bytes | None:
|
||||||
|
nonlocal usage_finalized
|
||||||
|
if usage_finalized:
|
||||||
|
return None
|
||||||
|
async with create_session() as new_session:
|
||||||
|
fresh_key = await new_session.get(key.__class__, key.hashed_key)
|
||||||
|
if not fresh_key:
|
||||||
|
usage_finalized = True
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
fallback: dict = {
|
||||||
|
"model": last_model_seen or "unknown",
|
||||||
|
"usage": None,
|
||||||
|
}
|
||||||
|
cost_data = await adjust_payment_for_tokens(
|
||||||
|
fresh_key, fallback, new_session, max_cost_for_model
|
||||||
|
)
|
||||||
|
usage_finalized = True
|
||||||
|
return f"event: cost\ndata: {json.dumps({'cost': cost_data})}\n\n".encode()
|
||||||
|
except Exception:
|
||||||
|
usage_finalized = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for chunk in response.aiter_bytes():
|
||||||
|
stored_chunks.append(chunk)
|
||||||
|
try:
|
||||||
|
decoded_chunk = chunk.decode("utf-8", errors="ignore")
|
||||||
|
for line in decoded_chunk.split("\n"):
|
||||||
|
if line.startswith("data: "):
|
||||||
|
try:
|
||||||
|
data = json.loads(line[6:])
|
||||||
|
if isinstance(data, dict):
|
||||||
|
msg = data.get("message", {})
|
||||||
|
if msg and msg.get("model"):
|
||||||
|
last_model_seen = str(msg.get("model"))
|
||||||
|
|
||||||
|
if usage := msg.get("usage"):
|
||||||
|
input_tokens += usage.get("input_tokens", 0)
|
||||||
|
output_tokens += usage.get(
|
||||||
|
"output_tokens", 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if usage := data.get("usage"):
|
||||||
|
input_tokens += usage.get("input_tokens", 0)
|
||||||
|
output_tokens += usage.get(
|
||||||
|
"output_tokens", 0
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
usage_data = {
|
||||||
|
"input_tokens": input_tokens,
|
||||||
|
"output_tokens": output_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
if input_tokens > 0 or output_tokens > 0:
|
||||||
|
async with create_session() as new_session:
|
||||||
|
fresh_key = await new_session.get(key.__class__, key.hashed_key)
|
||||||
|
if fresh_key:
|
||||||
|
try:
|
||||||
|
combined_data = {
|
||||||
|
"model": last_model_seen or "unknown",
|
||||||
|
"usage": usage_data,
|
||||||
|
}
|
||||||
|
cost_data = await adjust_payment_for_tokens(
|
||||||
|
fresh_key,
|
||||||
|
combined_data,
|
||||||
|
new_session,
|
||||||
|
max_cost_for_model,
|
||||||
|
)
|
||||||
|
usage_finalized = True
|
||||||
|
yield f"event: cost\ndata: {json.dumps({'cost': cost_data})}\n\n".encode()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not usage_finalized:
|
||||||
|
maybe_cost_event = await finalize_without_usage()
|
||||||
|
if maybe_cost_event is not None:
|
||||||
|
yield maybe_cost_event
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
if not usage_finalized:
|
||||||
|
await finalize_without_usage()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if not usage_finalized:
|
||||||
|
await finalize_without_usage()
|
||||||
|
|
||||||
|
response_headers = dict(response.headers)
|
||||||
|
response_headers.pop("content-encoding", None)
|
||||||
|
response_headers.pop("content-length", None)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_with_cost(max_cost_for_model),
|
||||||
|
status_code=response.status_code,
|
||||||
|
headers=response_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_non_streaming_messages_completion(
|
||||||
|
self,
|
||||||
|
response: httpx.Response,
|
||||||
|
key: ApiKey,
|
||||||
|
session: AsyncSession,
|
||||||
|
deducted_max_cost: int,
|
||||||
|
path: str,
|
||||||
|
) -> Response:
|
||||||
|
try:
|
||||||
|
content = await response.aread()
|
||||||
|
response_json = json.loads(content)
|
||||||
|
|
||||||
|
if path.endswith("count_tokens") and "usage" not in response_json:
|
||||||
|
input_tokens = response_json.get("input_tokens", 0)
|
||||||
|
response_json["usage"] = {"input_tokens": input_tokens}
|
||||||
|
|
||||||
|
cost_data = await adjust_payment_for_tokens(
|
||||||
|
key, response_json, session, deducted_max_cost
|
||||||
|
)
|
||||||
|
response_json["cost"] = cost_data
|
||||||
|
|
||||||
|
allowed_headers = {
|
||||||
|
"content-type",
|
||||||
|
"cache-control",
|
||||||
|
"date",
|
||||||
|
"vary",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"access-control-allow-credentials",
|
||||||
|
"access-control-expose-headers",
|
||||||
|
"access-control-max-age",
|
||||||
|
}
|
||||||
|
|
||||||
|
response_headers = {
|
||||||
|
k: v
|
||||||
|
for k, v in response.headers.items()
|
||||||
|
if k.lower() in allowed_headers
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=json.dumps(response_json).encode(),
|
||||||
|
status_code=response.status_code,
|
||||||
|
headers=response_headers,
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
|
||||||
async def forward_request(
|
async def forward_request(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -1157,7 +1321,54 @@ class BaseUpstreamProvider:
|
|||||||
await client.aclose()
|
await client.aclose()
|
||||||
return mapped_error
|
return mapped_error
|
||||||
|
|
||||||
if path.endswith("chat/completions") or path.endswith("embeddings"):
|
if (
|
||||||
|
path.endswith("chat/completions")
|
||||||
|
or path.endswith("embeddings")
|
||||||
|
or path.endswith("messages")
|
||||||
|
or path.endswith("messages/count_tokens")
|
||||||
|
):
|
||||||
|
if path.endswith("messages"):
|
||||||
|
client_wants_streaming = False
|
||||||
|
if request_body:
|
||||||
|
try:
|
||||||
|
request_data = json.loads(request_body)
|
||||||
|
client_wants_streaming = request_data.get("stream", False)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
content_type = response.headers.get("content-type", "")
|
||||||
|
upstream_is_streaming = "text/event-stream" in content_type
|
||||||
|
is_streaming = client_wants_streaming and upstream_is_streaming
|
||||||
|
|
||||||
|
if is_streaming and response.status_code == 200:
|
||||||
|
result = await self.handle_streaming_messages_completion(
|
||||||
|
response, key, max_cost_for_model
|
||||||
|
)
|
||||||
|
background_tasks = BackgroundTasks()
|
||||||
|
background_tasks.add_task(response.aclose)
|
||||||
|
background_tasks.add_task(client.aclose)
|
||||||
|
result.background = background_tasks
|
||||||
|
return result
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
try:
|
||||||
|
return await self.handle_non_streaming_messages_completion(
|
||||||
|
response, key, session, max_cost_for_model, path
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await response.aclose()
|
||||||
|
await client.aclose()
|
||||||
|
|
||||||
|
if path.endswith("messages/count_tokens"):
|
||||||
|
if response.status_code == 200:
|
||||||
|
try:
|
||||||
|
return await self.handle_non_streaming_messages_completion(
|
||||||
|
response, key, session, max_cost_for_model, path
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await response.aclose()
|
||||||
|
await client.aclose()
|
||||||
|
|
||||||
if path.endswith("chat/completions"):
|
if path.endswith("chat/completions"):
|
||||||
client_wants_streaming = False
|
client_wants_streaming = False
|
||||||
if request_body:
|
if request_body:
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
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,6 +218,14 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-8
@@ -40,23 +40,16 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -320,6 +313,8 @@ 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