Compare commits

..
3 changed files with 64 additions and 59 deletions
+35 -41
View File
@@ -5,7 +5,6 @@ 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__)
@@ -65,48 +64,13 @@ 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
)
MSATS_PER_1K_OUTPUT_TOKENS: float = (
float(settings.fixed_per_1k_output_tokens) * 1000.0
)
MSATS_PER_1K_IMAGE_COMPLETION_TOKENS: float = 0.0
if not settings.fixed_pricing:
response_model = response_data.get("model", "")
@@ -141,11 +105,13 @@ async def calculate_cost( # todo: can be sync
try:
mspp = float(model_obj.sats_pricing.prompt)
mspc = float(model_obj.sats_pricing.completion)
mspci = float(getattr(model_obj.sats_pricing, "completion_image", 0.0))
except Exception:
return CostDataError(message="Invalid pricing data", code="pricing_invalid")
MSATS_PER_1K_INPUT_TOKENS = mspp * 1_000_000.0
MSATS_PER_1K_OUTPUT_TOKENS = mspc * 1_000_000.0
MSATS_PER_1K_IMAGE_COMPLETION_TOKENS = mspci * 1_000_000.0
logger.info(
"Applied model-specific pricing",
@@ -153,6 +119,7 @@ async def calculate_cost( # todo: can be sync
"model": response_model,
"input_price_msats_per_1k": MSATS_PER_1K_INPUT_TOKENS,
"output_price_msats_per_1k": MSATS_PER_1K_OUTPUT_TOKENS,
"image_completion_price_msats_per_1k": MSATS_PER_1K_IMAGE_COMPLETION_TOKENS,
},
)
@@ -165,18 +132,44 @@ async def calculate_cost( # todo: can be sync
},
)
return cost_data
usage_data = response_data["usage"]
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_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)
)
# Calculate image completion cost
image_completion_msats = 0.0
if MSATS_PER_1K_IMAGE_COMPLETION_TOKENS > 0:
completion_details = usage_data.get("completion_tokens_details", {})
image_tokens = completion_details.get("image_tokens", 0)
if image_tokens > 0:
if output_tokens >= image_tokens:
output_tokens -= image_tokens
image_completion_msats = round(
image_tokens / 1000 * MSATS_PER_1K_IMAGE_COMPLETION_TOKENS, 3
)
logger.info(
"Calculated image completion cost",
extra={
"image_tokens": image_tokens,
"image_completion_msats": image_completion_msats,
},
)
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)
token_based_cost = math.ceil(input_msats + output_msats + image_completion_msats)
logger.info(
"Calculated token-based cost",
@@ -185,6 +178,7 @@ async def calculate_cost( # todo: can be sync
"output_tokens": output_tokens,
"input_cost_msats": input_msats,
"output_cost_msats": output_msats,
"image_completion_msats": image_completion_msats,
"total_cost_msats": token_based_cost,
"model": response_data.get("model", "unknown"),
},
+25 -1
View File
@@ -31,6 +31,7 @@ class Pricing(BaseModel):
completion: float
request: float = 0.0
image: float = 0.0
completion_image: float = 0.0
web_search: float = 0.0
internal_reasoning: float = 0.0
input_cache_read: float = 0.0
@@ -40,6 +41,13 @@ class Pricing(BaseModel):
max_cost: float = 0.0 # in sats not msats
PRICING_OVERRIDES = {
"gemini-3-pro-image-preview": {"completion_image": 0.00012},
"gemini-2.5-flash-image": {"completion_image": 0.00003},
"gemini-2.0-flash": {"completion_image": 0.00003},
}
class TopProvider(BaseModel):
context_length: int | None = None
max_completion_tokens: int | None = None
@@ -116,6 +124,16 @@ async def async_fetch_openrouter_models(source_filter: str | None = None) -> lis
if not _has_valid_pricing(model):
continue
# Apply manual pricing overrides
if model_id in PRICING_OVERRIDES:
pricing = model.get("pricing", {})
if pricing:
for k, v in PRICING_OVERRIDES[model_id].items():
pricing[k] = str(
v
) # OpenRouter API returns strings for pricing
model["pricing"] = pricing
models_data.append(model)
return models_data
@@ -148,6 +166,12 @@ def _row_to_model(
if isinstance(pricing, dict) and float(pricing.get("request", 0.0)) <= 0.0:
pricing["request"] = max(pricing.get("request", 0.0), 0.0)
# Apply defaults for missing fields from manual overrides
if row.id in PRICING_OVERRIDES and isinstance(pricing, dict):
for k, v in PRICING_OVERRIDES[row.id].items():
if k not in pricing:
pricing[k] = v
parsed_pricing = Pricing.parse_obj(pricing)
model = Model(
id=row.id,
@@ -507,6 +531,7 @@ def _pricing_matches(
"completion",
"request",
"image",
"completion_image",
"web_search",
"internal_reasoning",
]
@@ -528,5 +553,4 @@ async def models(session: AsyncSession = Depends(get_session)) -> dict:
from ..proxy import get_unique_models
items = get_unique_models()
print(items)
return {"data": items}
+4 -17
View File
@@ -85,7 +85,6 @@ 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")
@@ -103,26 +102,14 @@ 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(
"Failed to fetch live prices, using fallback if available",
"Skipping price update; unable to fetch BTC price",
extra={"error": str(e), "error_type": type(e).__name__},
)
# 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},
)
return
BTC_USD_PRICE = btc_price
SATS_USD_PRICE = btc_price / 100_000_000
def btc_usd_price() -> float: