From d122c5e1a466244965731cee1e87489afef5f2d5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Dec 2025 09:41:32 +0100 Subject: [PATCH] add completion_image_pricing to Pricing Model + hardcoded values for Gemini Image models --- routstr/payment/cost_calculation.py | 42 ++++++++++++++++++++++++++--- routstr/payment/models.py | 25 +++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/routstr/payment/cost_calculation.py b/routstr/payment/cost_calculation.py index f8eb4ffb..f791e013 100644 --- a/routstr/payment/cost_calculation.py +++ b/routstr/payment/cost_calculation.py @@ -70,6 +70,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", "") @@ -104,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", @@ -116,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, }, ) @@ -128,13 +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) - input_tokens = response_data.get("usage", {}).get("prompt_tokens", 0) - output_tokens = response_data.get("usage", {}).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) + ) + + # 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", @@ -143,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"), }, 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", ]