@@ -456,6 +456,103 @@ def _is_cost_explicitly_configured(model: str, llm_router: "Router") -> bool:
456456 return False
457457
458458
459+ _EMPTY_COST_ENTRY : Final [Mapping [str , object ]] = MappingProxyType ({})
460+
461+
462+ def _is_positive_cost (value : object ) -> bool :
463+ return isinstance (value , (int , float )) and not isinstance (value , bool ) and value > 0
464+
465+
466+ def _entry_has_priced_metric (entry : Mapping [str , object ]) -> bool :
467+ if entry .get ("tiered_pricing" ) is not None :
468+ return True
469+ for key , value in entry .items ():
470+ if "cost_per" not in key :
471+ continue
472+ if _is_positive_cost (value ):
473+ return True
474+ if isinstance (value , dict ) and any (_is_positive_cost (nested ) for nested in value .values ()):
475+ return True
476+ return False
477+
478+
479+ def _entry_declares_price (entry : Mapping [str , object ]) -> bool :
480+ return any ("cost_per" in key or key == "tiered_pricing" for key in entry )
481+
482+
483+ def _model_group_has_pricing (model : str , llm_router : "Router" ) -> bool :
484+ """
485+ A model group counts as priced when a deployment overrides any *cost_per* field or
486+ tiered_pricing in its litellm_params, even at zero, or when its resolved model info carries
487+ tiered_pricing or a positive price on any billed metric (tokens, characters, seconds, pages,
488+ images, queries, ...), so models billed by a non-token metric are not treated as unpriced.
489+ """
490+ for deployment in llm_router .get_model_list (model_name = model ) or ():
491+ litellm_params = deployment .get ("litellm_params" ) or _EMPTY_COST_ENTRY
492+ if _entry_declares_price (litellm_params ):
493+ return True
494+
495+ model_id = (deployment .get ("model_info" ) or _EMPTY_COST_ENTRY ).get ("id" )
496+ if model_id is None :
497+ continue
498+
499+ model_info = llm_router .get_deployment_model_info (
500+ model_id = model_id , model_name = litellm_params .get ("model" ) or ""
501+ )
502+ if model_info is not None and _entry_has_priced_metric (model_info ):
503+ return True
504+
505+ return False
506+
507+
508+ def _group_declares_explicit_cost (model : str , llm_router : "Router" ) -> bool :
509+ """
510+ Alias-aware counterpart to ``_is_cost_explicitly_configured``, which resolves the model group
511+ the same way ``_model_group_has_pricing`` does. A deployment that prices itself through its
512+ ``model_info`` block lands in the cost map under its deployment id rather than in its
513+ litellm_params, and reaching that entry through the router's own resolution keeps an alias
514+ pointing at such a group from being read as unpriced.
515+ """
516+ for deployment in llm_router .get_model_list (model_name = model ) or ():
517+ model_id = (deployment .get ("model_info" ) or _EMPTY_COST_ENTRY ).get ("id" )
518+ if model_id is None :
519+ continue
520+ raw_entry = litellm .model_cost .get (model_id , _EMPTY_COST_ENTRY )
521+ if "input_cost_per_token" in raw_entry or "output_cost_per_token" in raw_entry :
522+ return True
523+ return False
524+
525+
526+ def model_has_no_cost_mapping (model : str | None , llm_router : Router | None ) -> bool :
527+ if not model or llm_router is None :
528+ return False
529+
530+ if llm_router .get_model_group_info (model_group = model ) is None :
531+ return False
532+
533+ if _model_group_has_pricing (model = model , llm_router = llm_router ):
534+ return False
535+
536+ return not _group_declares_explicit_cost (model = model , llm_router = llm_router )
537+
538+
539+ def _unpriced_models_in_request (model : str | list [str ] | None , llm_router : Router | None ) -> tuple [str , ...]:
540+ candidates : Final = (model ,) if isinstance (model , str ) else tuple (model or ())
541+ return tuple (
542+ candidate for candidate in candidates if model_has_no_cost_mapping (model = candidate , llm_router = llm_router )
543+ )
544+
545+
546+ def _unpriced_models_block_message (models : tuple [str , ...]) -> str :
547+ names : Final = ", " .join (f"'{ model } '" for model in models )
548+ subject : Final = f"Model { names } has" if len (models ) == 1 else f"Models { names } have"
549+ return (
550+ f"{ subject } no pricing in the cost map, so litellm cannot price the request. "
551+ "Requests for unpriced models are blocked because 'block_requests_for_models_without_pricing' "
552+ "is enabled. Add pricing (input_cost_per_token/output_cost_per_token) to allow the request."
553+ )
554+
555+
459556async def _run_project_checks (
460557 project_object : LiteLLM_ProjectTableCachedObj | None ,
461558 _model : str | list [str ] | None ,
@@ -726,6 +823,19 @@ async def common_checks(
726823 and (route in MODEL_DISCOVERY_ROUTES or not RouteChecks .is_llm_api_route (route = route ))
727824 )
728825
826+ unpriced_models : Final = (
827+ _unpriced_models_in_request (model = _model , llm_router = llm_router )
828+ if litellm .block_requests_for_models_without_pricing and RouteChecks .is_llm_api_route (route = route )
829+ else ()
830+ )
831+ if unpriced_models :
832+ raise ProxyException (
833+ message = _unpriced_models_block_message (unpriced_models ),
834+ type = ProxyErrorTypes .model_cost_map_missing ,
835+ param = "model" ,
836+ code = status .HTTP_403_FORBIDDEN ,
837+ )
838+
729839 # 1. If team is blocked
730840 if team_object is not None and team_object .blocked is True :
731841 raise Exception (f"Team={ team_object .team_id } is blocked. Update via `/team/unblock` if you're an admin." )
0 commit comments