diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6536941a094..2e72859fd59 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -655,6 +655,7 @@ def cost_per_token( if ( (model_info.get("input_cost_per_token") or 0.0) > 0 or (model_info.get("output_cost_per_token") or 0.0) > 0 + or (model_info.get("input_cost_per_request") or 0.0) > 0 or model_info.get("tiered_pricing") is not None ): return generic_cost_per_token( diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 0a52e1d283e..099b3a40e5a 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -918,6 +918,11 @@ def generic_cost_per_token( service_tier=service_tier, ) + ## FLAT PER-REQUEST COST + input_cost_per_request: Final = model_info.get("input_cost_per_request") + if input_cost_per_request: + prompt_cost += input_cost_per_request + ## CALCULATE OUTPUT COST text_tokens = 0 audio_tokens = 0 diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 73f46bd2181..945d3beaa75 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -234,6 +234,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): input_cost_per_token_above_512k_tokens: float | None # MiniMax-M3: prompts >512K priced at 2x input input_cost_per_character_above_128k_tokens: float | None # only for vertex ai models input_cost_per_query: float | None # only for rerank models + input_cost_per_request: ReadOnly[float | None] # flat per-request pricing input_cost_per_image: float | None # only for vertex ai models input_cost_per_image_token: float | None # for gpt-image-1 and similar models input_cost_per_video_token: float | None # for gemini omni models with video input @@ -3338,6 +3339,7 @@ class MirroredPricingParams(BaseModel): class CustomPricingLiteLLMParams(MirroredPricingParams): ## CUSTOM PRICING ## input_cost_per_second: float | None = None + input_cost_per_request: float | None = None output_cost_per_second: float | None = None output_cost_per_second_1080p: float | None = None output_cost_per_second_480p: float | None = None diff --git a/litellm/utils.py b/litellm/utils.py index 802dc151428..c0e7a648eaa 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5781,6 +5781,7 @@ def _get_model_info_helper( ), input_cost_per_token_above_512k_tokens=_model_info.get("input_cost_per_token_above_512k_tokens", None), input_cost_per_query=_model_info.get("input_cost_per_query", None), + input_cost_per_request=_model_info.get("input_cost_per_request", None), input_cost_per_second=_model_info.get("input_cost_per_second", None), input_cost_per_audio_token=_model_info.get("input_cost_per_audio_token", None), input_cost_per_image_token=_model_info.get("input_cost_per_image_token", None), diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index 6f513ce1bd4..782f483bc44 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -3517,3 +3517,114 @@ def test_generic_cost_per_token_grok_46_long_context(_local_model_cost_map): ) assert prompt_cost == pytest.approx(200_000 * 4e-06 + 50_000 * 1e-06) assert completion_cost == pytest.approx(1_000 * 1.2e-05) + + +@pytest.fixture +def flat_per_request_model(): + """Register a synthetic model whose only price is a flat per-request rate.""" + from litellm.utils import _invalidate_model_cost_lowercase_map + + model = "openai/flat-rate-test-model" + prev = litellm.model_cost.get(model) + litellm.model_cost[model] = { + "input_cost_per_request": 0.05, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "openai", + "mode": "chat", + } + _invalidate_model_cost_lowercase_map() + try: + yield model + finally: + if prev is None: + litellm.model_cost.pop(model, None) + else: + litellm.model_cost[model] = prev + _invalidate_model_cost_lowercase_map() + + +def test_flat_per_request_cost_applies_once(flat_per_request_model): + """A flat per-request price bills the same regardless of token counts.""" + from litellm.types.utils import Usage + + small_usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15) + large_usage = Usage(prompt_tokens=10_000, completion_tokens=5_000, total_tokens=15_000) + + small_prompt_cost, small_completion_cost = generic_cost_per_token( + model=flat_per_request_model, + usage=small_usage, + custom_llm_provider="openai", + ) + large_prompt_cost, large_completion_cost = generic_cost_per_token( + model=flat_per_request_model, + usage=large_usage, + custom_llm_provider="openai", + ) + + assert small_prompt_cost == pytest.approx(0.05) + assert small_completion_cost == 0.0 + assert large_prompt_cost == pytest.approx(0.05) + assert large_completion_cost == 0.0 + + +def test_flat_per_request_cost_is_added_to_token_cost(): + """The flat rate stacks on top of token pricing rather than replacing it.""" + from litellm.utils import _invalidate_model_cost_lowercase_map + from litellm.types.utils import Usage + + model = "openai/flat-plus-tokens-test-model" + litellm.model_cost[model] = { + "input_cost_per_request": 0.05, + "input_cost_per_token": 1e-05, + "output_cost_per_token": 2e-05, + "litellm_provider": "openai", + "mode": "chat", + } + _invalidate_model_cost_lowercase_map() + try: + usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) + prompt_cost, completion_cost = generic_cost_per_token( + model=model, + usage=usage, + custom_llm_provider="openai", + ) + assert prompt_cost == pytest.approx(0.05 + 100 * 1e-05) + assert completion_cost == pytest.approx(50 * 2e-05) + finally: + litellm.model_cost.pop(model, None) + _invalidate_model_cost_lowercase_map() + + +def test_flat_per_request_cost_routes_through_cost_per_token(): + """The generic cost_per_token dispatch must not skip a model whose only + pricing is a flat per-request rate. + + Uses a provider with no dedicated cost_per_token branch, so the model + genuinely reaches the generic fallback whose guard is under test. + """ + from litellm.cost_calculator import cost_per_token + from litellm.types.utils import Usage + from litellm.utils import _invalidate_model_cost_lowercase_map + + model = "groq/flat-rate-only-test-model" + litellm.model_cost[model] = { + "input_cost_per_request": 0.05, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "groq", + "mode": "chat", + } + _invalidate_model_cost_lowercase_map() + try: + usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15) + prompt_cost, completion_cost = cost_per_token( + model=model, + custom_llm_provider="groq", + usage_object=usage, + ) + assert prompt_cost == pytest.approx(0.05) + assert completion_cost == 0.0 + finally: + litellm.model_cost.pop(model, None) + _invalidate_model_cost_lowercase_map() diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index e0b8cf19159..279122e14f6 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -27572,6 +27572,8 @@ export interface components { input_cost_per_pixel?: number | null; /** Input Cost Per Query */ input_cost_per_query?: number | null; + /** Input Cost Per Request */ + input_cost_per_request?: number | null; /** Input Cost Per Second */ input_cost_per_second?: number | null; /** Input Cost Per Token */ @@ -36792,6 +36794,8 @@ export interface components { input_cost_per_pixel?: number | null; /** Input Cost Per Query */ input_cost_per_query?: number | null; + /** Input Cost Per Request */ + input_cost_per_request?: number | null; /** Input Cost Per Second */ input_cost_per_second?: number | null; /** Input Cost Per Token */