From 5416cefd872a2bb7e1e27d5001b2bddecb4f5f4f Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Sat, 20 Dec 2025 16:16:23 +0100 Subject: [PATCH 1/3] use cost field if available --- routstr/payment/cost_calculation.py | 46 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/routstr/payment/cost_calculation.py b/routstr/payment/cost_calculation.py index f8eb4ffb..9e1846f9 100644 --- a/routstr/payment/cost_calculation.py +++ b/routstr/payment/cost_calculation.py @@ -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) From 301dd8121516ad3fe2e5b719423e7665c6a48e20 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 21 Dec 2025 09:55:25 +0100 Subject: [PATCH 2/3] include openrouters upstream_inference_cost + manually add gemini image completion cost --- routstr/payment/cost_calculation.py | 67 ++++++++++++++++++++++++----- routstr/payment/models.py | 25 +++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/routstr/payment/cost_calculation.py b/routstr/payment/cost_calculation.py index 9e1846f9..037efc42 100644 --- a/routstr/payment/cost_calculation.py +++ b/routstr/payment/cost_calculation.py @@ -67,15 +67,29 @@ async def calculate_cost( # todo: can be sync usage_data = response_data["usage"] - if "cost" in usage_data and usage_data["cost"] is not None: + usd_cost = 0.0 + + # Prioritize cost_details.upstream_inference_cost + if "cost_details" in usage_data: + usd_cost = float( + usage_data["cost_details"].get("upstream_inference_cost", 0) or 0 + ) + + # Fallback to cost field if upstream_inference_cost is 0 + if usd_cost == 0 and "cost" in usage_data: + try: + usd_cost = float(usage_data.get("cost", 0) or 0) + except Exception: + pass + + if usd_cost > 0: 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", + "Using cost from usage data/details", extra={ "usd_cost": usd_cost, "cost_in_sats": cost_in_sats, @@ -85,17 +99,17 @@ async def calculate_cost( # todo: can be sync ) return CostData( - base_msats=0, - input_msats=0, # Cost field doesn't break down by token type - output_msats=0, + base_msats=-1, + input_msats=-1, # Cost field doesn't break down by token type + output_msats=-1, total_msats=cost_in_msats, ) except Exception as e: logger.warning( - "Error using cost field, falling back to token-based calculation", + "Error calculating cost from usage data", extra={ "error": str(e), - "cost_value": usage_data.get("cost"), + "usd_cost": usd_cost, "model": response_data.get("model", "unknown"), }, ) @@ -107,6 +121,7 @@ async def calculate_cost( # todo: can be sync 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 +156,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 +170,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, }, ) @@ -170,13 +188,39 @@ async def calculate_cost( # todo: can be sync 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 +229,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"), }, diff --git a/routstr/payment/models.py b/routstr/payment/models.py index d6eccf5d..4c8dbde4 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -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", ] From e39742c429b3e8f026df2624f11e5c70f5c7ea44 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Dec 2025 09:35:23 +0100 Subject: [PATCH 3/3] rm completion_image pricing with manual overrides --- routstr/payment/cost_calculation.py | 29 +---------------------------- routstr/payment/models.py | 25 ------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/routstr/payment/cost_calculation.py b/routstr/payment/cost_calculation.py index 037efc42..03b37362 100644 --- a/routstr/payment/cost_calculation.py +++ b/routstr/payment/cost_calculation.py @@ -121,7 +121,6 @@ async def calculate_cost( # todo: can be sync 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", "") @@ -156,13 +155,11 @@ 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", @@ -170,7 +167,6 @@ 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, }, ) @@ -195,32 +191,10 @@ async def calculate_cost( # todo: can be sync 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 + image_completion_msats) + token_based_cost = math.ceil(input_msats + output_msats) logger.info( "Calculated token-based cost", @@ -229,7 +203,6 @@ 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"), }, diff --git a/routstr/payment/models.py b/routstr/payment/models.py index 4c8dbde4..d6eccf5d 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -31,7 +31,6 @@ 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 @@ -41,13 +40,6 @@ 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 @@ -124,16 +116,6 @@ 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 @@ -166,12 +148,6 @@ 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, @@ -531,7 +507,6 @@ def _pricing_matches( "completion", "request", "image", - "completion_image", "web_search", "internal_reasoning", ]