Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7e1f44f
feat(proxy): add admin toggle to block requests for models without pr…
mateo-berri Jul 30, 2026
84c41dc
fix(proxy): treat non-token pricing as priced and propagate the unpri…
mateo-berri Jul 31, 2026
074b37b
refactor(proxy): flatten the pricing-metric check to avoid recursion
mateo-berri Jul 31, 2026
6bd21fa
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
web-flow Aug 20, 2026
c551a5c
fix(proxy): treat explicit zero non-token prices as priced
mateo-berri Aug 20, 2026
9c29e11
Merge branch 'litellm_block_unpriced_models' of https://git-manager.d…
mateo-berri Aug 20, 2026
21891b4
fix(proxy): count explicit zero prices on any billed metric as config…
mateo-berri Aug 20, 2026
7f539e3
Merge branch 'litellm_block_unpriced_models' of https://github.com/Be…
mateo-berri Aug 20, 2026
5301872
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
mateo-berri Aug 20, 2026
ab79b8d
fix: count tiered_pricing as a cost mapping when blocking unpriced mo…
mateo-berri Aug 20, 2026
eb8d402
test(proxy): cover a registry model priced only via tiered_pricing
mateo-berri Aug 20, 2026
2b2d6d7
fix(proxy): apply DB-persisted safe litellm settings on every worker'…
mateo-berri Aug 20, 2026
3672fa9
fix(proxy): log block_requests_for_models_without_pricing updates lazily
mateo-berri Aug 20, 2026
3c44f8d
fix(ui): surface toggle failures on the block-unpriced-models setting
mateo-berri Aug 20, 2026
df00c33
fix(proxy): reload the unpriced-model toggle regardless of supported_…
mateo-berri Aug 20, 2026
c73480c
fix(proxy): block every unpriced model a request names
mateo-berri Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def _dev_env_hot_reload_enabled() -> bool:
# backwards compatibility — arbitrary client-supplied identifiers still
# pass through unchanged.
validate_end_user_id_in_db: bool = False
block_requests_for_models_without_pricing: bool = False
disable_end_user_cost_tracking: Optional[bool] = None
disable_end_user_cost_tracking_prometheus_only: Optional[bool] = None
enable_end_user_cost_tracking_prometheus_only: Optional[bool] = None
Expand Down
1 change: 1 addition & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@
"public_model_groups_links",
"cost_discount_config",
"cost_margin_config",
"block_requests_for_models_without_pricing",
"budget_exceeded_throttle_percentage",
# Every field editable from the Admin UI (proxy_server._GENERAL_SETTINGS_UI_LITELLM_FIELDS)
# must be listed here so a DB write from one worker overrides the live litellm attribute on
Expand Down
2 changes: 2 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3721,6 +3721,8 @@ class ProxyErrorTypes(str, enum.Enum):
Project does not have access to the model
"""

model_cost_map_missing = "model_cost_map_missing"

expired_key = "expired_key"
"""
Key has expired
Expand Down
77 changes: 77 additions & 0 deletions litellm/proxy/auth/auth_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,66 @@ def _is_cost_explicitly_configured(model: str, llm_router: "Router") -> bool:
return False


_EMPTY_COST_ENTRY: Final[Mapping[str, object]] = MappingProxyType({})


def _is_positive_cost(value: object) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0


def _entry_has_priced_metric(entry: Mapping[str, object]) -> bool:
for key, value in entry.items():
if "cost_per" not in key:
continue
if _is_positive_cost(value):
return True
if isinstance(value, dict) and any(_is_positive_cost(nested) for nested in value.values()):
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
return True
return False
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved


def _entry_declares_price(entry: Mapping[str, object]) -> bool:
return any("cost_per" in key for key in entry)


def _model_group_has_pricing(model: str, llm_router: "Router") -> bool:
"""
A model group counts as priced when a deployment overrides any *cost_per* field in its
litellm_params, even at zero, or when its resolved model info carries a positive price on
any billed metric (tokens, characters, seconds, pages, images, queries, ...), so models
billed by a non-token metric are not treated as unpriced.
"""
for deployment in llm_router.get_model_list(model_name=model) or ():
litellm_params = deployment.get("litellm_params") or _EMPTY_COST_ENTRY
if _entry_declares_price(litellm_params):
return True

model_id = (deployment.get("model_info") or _EMPTY_COST_ENTRY).get("id")
if model_id is None:
continue

model_info = llm_router.get_deployment_model_info(
model_id=model_id, model_name=litellm_params.get("model") or ""
)
if model_info is not None and _entry_has_priced_metric(model_info):
return True

return False


def model_has_no_cost_mapping(model: str | None, llm_router: Router | None) -> bool:
if not model or llm_router is None:
return False

if llm_router.get_model_group_info(model_group=model) is None:
return False

if _model_group_has_pricing(model=model, llm_router=llm_router):
return False

Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
return not _is_cost_explicitly_configured(model, llm_router)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved


async def _run_project_checks(
project_object: LiteLLM_ProjectTableCachedObj | None,
_model: str | list[str] | None,
Expand Down Expand Up @@ -734,6 +794,23 @@ async def common_checks(
and (route in MODEL_DISCOVERY_ROUTES or not RouteChecks.is_llm_api_route(route=route))
)

if (
litellm.block_requests_for_models_without_pricing
and isinstance(_model, str)
and RouteChecks.is_llm_api_route(route=route)
and model_has_no_cost_mapping(model=_model, llm_router=llm_router)
):
raise ProxyException(
message=(
f"Model '{_model}' has no pricing in the cost map, so its spend would be tracked as $0. "
"Requests for unpriced models are blocked because 'block_requests_for_models_without_pricing' "
"is enabled. Add pricing for this model (input_cost_per_token/output_cost_per_token) to allow it."
),
type=ProxyErrorTypes.model_cost_map_missing,
param="model",
code=status.HTTP_403_FORBIDDEN,
)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

# 1. If team is blocked
if team_object is not None and team_object.blocked is True:
raise Exception(f"Team={team_object.team_id} is blocked. Update via `/team/unblock` if you're an admin.")
Expand Down
71 changes: 71 additions & 0 deletions litellm/proxy/management_endpoints/cost_tracking_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Final

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

import litellm
from litellm._logging import verbose_proxy_logger
Expand Down Expand Up @@ -439,6 +440,76 @@ async def update_cost_margin_config(
)


class BlockUnpricedModelsRequest(BaseModel):
enabled: bool


class BlockUnpricedModelsResponse(BaseModel):
enabled: bool


@router.get(
"/config/block_requests_for_models_without_pricing",
tags=("Cost Tracking",),
dependencies=(Depends(user_api_key_auth),),
response_model=BlockUnpricedModelsResponse,
)
async def get_block_requests_for_models_without_pricing() -> BlockUnpricedModelsResponse:
return BlockUnpricedModelsResponse(enabled=bool(litellm.block_requests_for_models_without_pricing))


@router.patch(
"/config/block_requests_for_models_without_pricing",
tags=("Cost Tracking",),
dependencies=(Depends(user_api_key_auth),),
response_model=BlockUnpricedModelsResponse,
)
async def update_block_requests_for_models_without_pricing(
request: BlockUnpricedModelsRequest,
) -> BlockUnpricedModelsResponse:
from litellm.proxy.proxy_server import (
prisma_client,
proxy_config,
store_model_in_db,
)

if prisma_client is None:
raise HTTPException(
status_code=500,
detail={ # mutable-ok: HTTPException detail must be a plain mapping
"error": CommonProxyErrors.db_not_connected_error.value
},
)

if store_model_in_db is not True:
raise HTTPException(
status_code=500,
detail={ # mutable-ok: HTTPException detail must be a plain mapping
"error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature."
},
)

try:
config = await proxy_config.get_config()
if "litellm_settings" not in config:
config["litellm_settings"] = {} # mutable-ok: config is a plain-dict payload for save_config
config["litellm_settings"]["block_requests_for_models_without_pricing"] = request.enabled
await proxy_config.save_config(new_config=config)

litellm.block_requests_for_models_without_pricing = request.enabled
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
verbose_proxy_logger.info(f"Updated block_requests_for_models_without_pricing: {request.enabled}")

return BlockUnpricedModelsResponse(enabled=request.enabled)
except Exception as e: # noqa: BLE001 # any config persistence failure must surface as a 500 response, not a crash
verbose_proxy_logger.error(f"Error updating block_requests_for_models_without_pricing: {e!s}")
raise HTTPException(
status_code=500,
detail={ # mutable-ok: HTTPException detail must be a plain mapping
"error": f"Failed to update setting: {e!s}"
},
)


@router.post(
"/cost/estimate",
tags=["Cost Tracking"],
Expand Down
Loading
Loading