mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-31 15:56:14 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f160384e14 | ||
|
|
4ee6801413 | ||
|
|
e4153920a9 | ||
|
|
1b4e092242 | ||
|
|
5416cefd87 | ||
|
|
8edc3512c1 | ||
|
|
590fb4bc2c | ||
|
|
5db9abc3ce |
@@ -5,6 +5,7 @@ from pydantic.v1 import BaseModel
|
||||
from ..core import get_logger
|
||||
from ..core.db import AsyncSession
|
||||
from ..core.settings import settings
|
||||
from .price import sats_usd_price
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -64,6 +65,42 @@ async def calculate_cost( # todo: can be sync
|
||||
)
|
||||
return cost_data
|
||||
|
||||
usage_data = response_data["usage"]
|
||||
|
||||
if "cost" in usage_data and usage_data["cost"] is not None:
|
||||
try:
|
||||
usd_cost = float(usage_data["cost"])
|
||||
sats_per_usd = 1.0 / sats_usd_price()
|
||||
cost_in_sats = usd_cost * sats_per_usd
|
||||
cost_in_msats = math.ceil(cost_in_sats * 1000)
|
||||
|
||||
logger.info(
|
||||
"Using cost field from usage data",
|
||||
extra={
|
||||
"usd_cost": usd_cost,
|
||||
"cost_in_sats": cost_in_sats,
|
||||
"cost_in_msats": cost_in_msats,
|
||||
"model": response_data.get("model", "unknown"),
|
||||
},
|
||||
)
|
||||
|
||||
return CostData(
|
||||
base_msats=0,
|
||||
input_msats=0, # Cost field doesn't break down by token type
|
||||
output_msats=0,
|
||||
total_msats=cost_in_msats,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error using cost field, falling back to token-based calculation",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"cost_value": usage_data.get("cost"),
|
||||
"model": response_data.get("model", "unknown"),
|
||||
},
|
||||
)
|
||||
# Fall through to token-based calculation
|
||||
|
||||
MSATS_PER_1K_INPUT_TOKENS: float = (
|
||||
float(settings.fixed_per_1k_input_tokens) * 1000.0
|
||||
)
|
||||
@@ -129,10 +166,15 @@ async def calculate_cost( # todo: can be sync
|
||||
)
|
||||
return cost_data
|
||||
|
||||
input_tokens = response_data.get("usage", {}).get("prompt_tokens", 0)
|
||||
output_tokens = response_data.get("usage", {}).get("completion_tokens", 0)
|
||||
input_tokens = usage_data.get("prompt_tokens", 0)
|
||||
output_tokens = usage_data.get("completion_tokens", 0)
|
||||
|
||||
# added for response api
|
||||
input_tokens = input_tokens if input_tokens != 0 else usage_data.get("input_tokens", 0)
|
||||
output_tokens = output_tokens if output_tokens != 0 else usage_data.get("output_tokens", 0)
|
||||
|
||||
input_msats = round(input_tokens / 1000 * MSATS_PER_1K_INPUT_TOKENS, 3)
|
||||
|
||||
output_msats = round(output_tokens / 1000 * MSATS_PER_1K_OUTPUT_TOKENS, 3)
|
||||
token_based_cost = math.ceil(input_msats + output_msats)
|
||||
|
||||
|
||||
@@ -528,4 +528,5 @@ async def models(session: AsyncSession = Depends(get_session)) -> dict:
|
||||
from ..proxy import get_unique_models
|
||||
|
||||
items = get_unique_models()
|
||||
print(items)
|
||||
return {"data": items}
|
||||
|
||||
@@ -85,6 +85,7 @@ async def _fetch_btc_usd_price() -> float:
|
||||
_binance_btc_usdt(client),
|
||||
)
|
||||
valid_prices = [price for price in prices if price is not None]
|
||||
print(valid_prices)
|
||||
if not valid_prices:
|
||||
logger.error("No valid BTC prices obtained from any exchange")
|
||||
raise ValueError("Unable to fetch BTC price from any exchange")
|
||||
@@ -102,14 +103,26 @@ async def _update_prices() -> None:
|
||||
global BTC_USD_PRICE, SATS_USD_PRICE
|
||||
try:
|
||||
btc_price = await _fetch_btc_usd_price()
|
||||
BTC_USD_PRICE = btc_price
|
||||
SATS_USD_PRICE = btc_price / 100_000_000
|
||||
logger.info(
|
||||
"Updated BTC/SATS prices successfully",
|
||||
extra={"btc_usd": btc_price, "sats_usd": SATS_USD_PRICE},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Skipping price update; unable to fetch BTC price",
|
||||
"Failed to fetch live prices, using fallback if available",
|
||||
extra={"error": str(e), "error_type": type(e).__name__},
|
||||
)
|
||||
return
|
||||
BTC_USD_PRICE = btc_price
|
||||
SATS_USD_PRICE = btc_price / 100_000_000
|
||||
# Use fallback price if no price is set
|
||||
if BTC_USD_PRICE is None or SATS_USD_PRICE is None:
|
||||
fallback_btc_price = getattr(settings, "fallback_btc_usd_price", 50000.0)
|
||||
BTC_USD_PRICE = fallback_btc_price
|
||||
SATS_USD_PRICE = fallback_btc_price / 100_000_000
|
||||
logger.info(
|
||||
"Using fallback BTC price",
|
||||
extra={"fallback_btc_usd": fallback_btc_price, "sats_usd": SATS_USD_PRICE},
|
||||
)
|
||||
|
||||
|
||||
def btc_usd_price() -> float:
|
||||
|
||||
@@ -3,7 +3,6 @@ Integration tests for wallet authentication system including API key generation
|
||||
Tests POST /v1/wallet/topup endpoint and authorization header validation.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
@@ -15,7 +14,6 @@ from routstr.core.db import ApiKey
|
||||
|
||||
from .utils import (
|
||||
CashuTokenGenerator,
|
||||
ConcurrencyTester,
|
||||
ResponseValidator,
|
||||
)
|
||||
|
||||
@@ -389,69 +387,6 @@ async def test_api_key_with_expiry_time(
|
||||
# The expiry time and refund address functionality is tested elsewhere
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_token_submissions(
|
||||
integration_client: AsyncClient, testmint_wallet: Any, integration_session: Any
|
||||
) -> None:
|
||||
"""Test concurrent submissions of different tokens"""
|
||||
|
||||
# Generate multiple unique tokens with known amounts
|
||||
num_tokens = 10
|
||||
tokens = []
|
||||
expected_balances = {}
|
||||
|
||||
for i in range(num_tokens):
|
||||
amount = 100 + i * 10
|
||||
token = await testmint_wallet.mint_tokens(amount)
|
||||
tokens.append(token)
|
||||
# Store expected balance by token hash
|
||||
hashed_key = hashlib.sha256(token.encode()).hexdigest()
|
||||
expected_balances[hashed_key] = amount * 1000 # msats
|
||||
|
||||
# Create concurrent requests
|
||||
requests = [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/v1/wallet/info",
|
||||
"headers": {"Authorization": f"Bearer {token}"},
|
||||
}
|
||||
for token in tokens
|
||||
]
|
||||
|
||||
# Execute concurrently
|
||||
tester = ConcurrencyTester()
|
||||
responses = await tester.run_concurrent_requests(
|
||||
integration_client, requests, max_concurrent=5
|
||||
)
|
||||
|
||||
# All should succeed
|
||||
assert len(responses) == num_tokens
|
||||
api_keys = set()
|
||||
|
||||
for response in responses:
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
api_key = data["api_key"]
|
||||
api_keys.add(api_key)
|
||||
|
||||
# Verify balance matches the expected amount
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
assert data["balance"] == expected_balances[hashed_key]
|
||||
|
||||
# Should have created unique API keys
|
||||
assert len(api_keys) == num_tokens
|
||||
|
||||
# Verify all keys exist in database
|
||||
for api_key in api_keys:
|
||||
hashed_key = api_key[3:] # Remove "sk-" prefix
|
||||
result = await integration_session.execute(
|
||||
select(ApiKey).where(ApiKey.hashed_key == hashed_key) # type: ignore[arg-type]
|
||||
)
|
||||
db_key = result.scalar_one()
|
||||
assert db_key.balance == expected_balances[hashed_key]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_authorization_with_cashu_token_directly(
|
||||
@@ -504,48 +439,6 @@ async def test_x_cashu_header_support(
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.slow
|
||||
async def test_api_key_consistency_under_load(
|
||||
integration_client: AsyncClient, testmint_wallet: Any, integration_session: Any
|
||||
) -> None:
|
||||
"""Test API key generation consistency under concurrent load"""
|
||||
|
||||
# Generate a single token
|
||||
token = await testmint_wallet.mint_tokens(1000)
|
||||
|
||||
# First request to create the API key
|
||||
integration_client.headers["Authorization"] = f"Bearer {token}"
|
||||
initial_response = await integration_client.get("/v1/wallet/info")
|
||||
assert initial_response.status_code == 200
|
||||
expected_api_key = initial_response.json()["api_key"]
|
||||
expected_balance = initial_response.json()["balance"]
|
||||
|
||||
# Try to use the same token concurrently multiple times
|
||||
# All should return the same API key since it's already created
|
||||
requests = [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/v1/wallet/info",
|
||||
"headers": {"Authorization": f"Bearer {token}"},
|
||||
}
|
||||
for _ in range(20) # 20 concurrent attempts
|
||||
]
|
||||
|
||||
tester = ConcurrencyTester()
|
||||
responses = await tester.run_concurrent_requests(
|
||||
integration_client, requests, max_concurrent=10
|
||||
)
|
||||
|
||||
# All should succeed and return the same API key
|
||||
for response in responses:
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["api_key"] == expected_api_key
|
||||
assert data["balance"] == expected_balance
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_timestamp_accuracy(
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlmodel import select, update
|
||||
|
||||
from routstr.core.db import ApiKey
|
||||
|
||||
from .utils import ConcurrencyTester, ResponseValidator
|
||||
from .utils import ResponseValidator
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -204,45 +204,6 @@ async def test_expired_api_key_behavior(
|
||||
assert db_key.refund_address == "test@lightning.address"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_access_same_api_key(
|
||||
integration_client: AsyncClient, authenticated_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test concurrent access with the same API key"""
|
||||
|
||||
# Get the API key from authenticated client
|
||||
response = await authenticated_client.get("/v1/wallet/")
|
||||
api_key = response.json()["api_key"]
|
||||
initial_balance = response.json()["balance"]
|
||||
|
||||
# Create multiple concurrent requests
|
||||
requests = []
|
||||
for i in range(20):
|
||||
# Alternate between both endpoints
|
||||
endpoint = "/v1/wallet/" if i % 2 == 0 else "/v1/wallet/info"
|
||||
requests.append(
|
||||
{
|
||||
"method": "GET",
|
||||
"url": endpoint,
|
||||
"headers": {"Authorization": f"Bearer {api_key}"},
|
||||
}
|
||||
)
|
||||
|
||||
# Execute concurrently
|
||||
tester = ConcurrencyTester()
|
||||
responses = await tester.run_concurrent_requests(
|
||||
integration_client, requests, max_concurrent=10
|
||||
)
|
||||
|
||||
# All should succeed with consistent data
|
||||
for response in responses:
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["api_key"] == api_key
|
||||
assert data["balance"] == initial_balance
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_info_data_consistency(
|
||||
|
||||
@@ -15,7 +15,6 @@ from routstr.core.db import ApiKey
|
||||
|
||||
from .utils import (
|
||||
CashuTokenGenerator,
|
||||
ConcurrencyTester,
|
||||
ResponseValidator,
|
||||
)
|
||||
|
||||
@@ -284,60 +283,6 @@ async def test_transaction_history_tracking( # type: ignore[no-untyped-def]
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_topups_same_api_key( # type: ignore[no-untyped-def]
|
||||
integration_client: AsyncClient,
|
||||
authenticated_client: AsyncClient,
|
||||
testmint_wallet: Any,
|
||||
) -> None:
|
||||
"""Test concurrent top-ups to the same API key"""
|
||||
|
||||
# Get API key
|
||||
response = await authenticated_client.get("/v1/wallet/")
|
||||
api_key = response.json()["api_key"]
|
||||
initial_balance = response.json()["balance"]
|
||||
|
||||
# Generate multiple unique tokens
|
||||
num_tokens = 10
|
||||
tokens = []
|
||||
total_amount = 0
|
||||
|
||||
for i in range(num_tokens):
|
||||
amount = 100 + i * 10 # Different amounts
|
||||
token = await testmint_wallet.mint_tokens(amount)
|
||||
tokens.append(token)
|
||||
total_amount += amount
|
||||
|
||||
# Create concurrent top-up requests
|
||||
requests = [
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "/v1/wallet/topup",
|
||||
"params": {"cashu_token": token},
|
||||
"headers": {"Authorization": f"Bearer {api_key}"},
|
||||
}
|
||||
for token in tokens
|
||||
]
|
||||
|
||||
# Execute concurrently
|
||||
tester = ConcurrencyTester()
|
||||
responses = await tester.run_concurrent_requests(
|
||||
integration_client, requests, max_concurrent=5
|
||||
)
|
||||
|
||||
# All should succeed
|
||||
for response in responses:
|
||||
assert response.status_code == 200
|
||||
assert "msats" in response.json()
|
||||
|
||||
# Verify final balance is correct
|
||||
final_response = await authenticated_client.get("/v1/wallet/")
|
||||
final_balance = final_response.json()["balance"]
|
||||
expected_balance = initial_balance + (total_amount * 1000)
|
||||
assert final_balance == expected_balance
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_topup_during_active_proxy_request( # type: ignore[no-untyped-def]
|
||||
|
||||
Reference in New Issue
Block a user