From 19c62b8b35dd3dcc4725a1c7395eedd1fb3fe961 Mon Sep 17 00:00:00 2001 From: James Liounis Date: Fri, 21 Aug 2026 15:38:00 -0400 Subject: [PATCH 1/2] fix(cost): charge input_cost_per_request in the generic cost path The cost map has carried an input_cost_per_request key for a long time, but no code on the generic path reads it: generic_cost_per_token never adds it, and cost_per_token's generic fallback treats a model priced only per request as having no pricing at all and returns zero. Any provider on the generic path with flat per-request pricing therefore tracks $0 spend. This adds the key to ModelInfo, charges it in generic_cost_per_token on top of any token pricing, and admits it to the generic fallback's guard. Providers with a dedicated cost_per_token branch (perplexity's online models, for example) do not route through this path and are unaffected. --- litellm/cost_calculator.py | 1 + .../litellm_core_utils/llm_cost_calc/utils.py | 5 + litellm/types/utils.py | 2 + litellm/utils.py | 1 + .../llm_cost_calc/test_llm_cost_calc_utils.py | 111 ++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 8f7cd09d364..7b46f8059f4 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -653,6 +653,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 8a71d209618..80f56904367 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -233,6 +233,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 @@ -3323,6 +3324,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 input_cost_per_pixel: float | None = None diff --git a/litellm/utils.py b/litellm/utils.py index e5ce7157e77..92f7fde868a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5673,6 +5673,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 f66056a54e2..a167a3cdf2f 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 @@ -3604,3 +3604,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() From 66525bee2b702edc62ab4891ee075d1a87aa12fe Mon Sep 17 00:00:00 2001 From: James Liounis Date: Fri, 21 Aug 2026 16:44:31 -0400 Subject: [PATCH 2/2] chore(ui): regenerate dashboard API types for input_cost_per_request CustomPricingLiteLLMParams gained the field, so the proxy OpenAPI spec and the generated dashboard types move with it. --- ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index cf55dc69e86..f823577c336 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -27521,6 +27521,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 */ @@ -36730,6 +36732,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 */