diff --git a/litellm/litellm_core_utils/ptu_pricing.py b/litellm/litellm_core_utils/ptu_pricing.py index f9a4335bb4c..683ef9a5ba4 100644 --- a/litellm/litellm_core_utils/ptu_pricing.py +++ b/litellm/litellm_core_utils/ptu_pricing.py @@ -229,8 +229,11 @@ def ptu_config_error(model_info: Mapping[str, object], *, model_name: str | None has_count: Final = model_info.get("ptu_count") is not None has_rate: Final = model_info.get("cost_per_ptu_per_hour") is not None - if not has_count and not has_rate: + has_shares: Final = model_info.get("ptu_shares") is not None + if not has_count and not has_rate and not has_shares: return None + if not has_count and not has_rate: + return _named("ptu_count and cost_per_ptu_per_hour are required when ptu_shares is set", model_name) if has_count != has_rate: return _named("ptu_count and cost_per_ptu_per_hour must be set together", model_name) if effective_from is None: diff --git a/tests/unit/litellm_core_utils/test_ptu_pricing.py b/tests/unit/litellm_core_utils/test_ptu_pricing.py index 7dda3432e43..28b57fb328d 100644 --- a/tests/unit/litellm_core_utils/test_ptu_pricing.py +++ b/tests/unit/litellm_core_utils/test_ptu_pricing.py @@ -439,6 +439,21 @@ def test_a_fractional_count_reserves_nothing(): assert ptu_terms({**_SHARED, "ptu_count": 100.5}) is None +@pytest.mark.parametrize( + "shares_only", + [ + {"ptu_shares": {"team-a": 60, "team-b": 40}}, + {"ptu_shares": {"team-a": 60, "team-b": 40}, "ptu_effective_from": "2026-01-01T00:00:00Z"}, + ], + ids=["shares alone", "shares with a window"], +) +def test_shares_without_the_count_and_rate_are_refused_instead_of_registering_unpriced(shares_only): + """Shares split a count, so a split with no count has nothing to add up to and would register a + deployment the share filter and ceiling honour while flat cost never rolls up.""" + assert ptu_config_error(shares_only) == "ptu_count and cost_per_ptu_per_hour are required when ptu_shares is set" + assert ptu_terms(shares_only) is None + + def test_the_split_is_named_after_the_deployment_when_the_caller_supplies_one(): error = ptu_config_error({**_SHARED, "ptu_shares": {"team-a": 1}}, model_name="gpt-4.1-ptu") diff --git a/tests/unit/proxy/management_endpoints/test_ptu_model_settings.py b/tests/unit/proxy/management_endpoints/test_ptu_model_settings.py index ce2e865d69d..8bbde32e756 100644 --- a/tests/unit/proxy/management_endpoints/test_ptu_model_settings.py +++ b/tests/unit/proxy/management_endpoints/test_ptu_model_settings.py @@ -1334,6 +1334,13 @@ def test_validate_helper_refuses_shares_that_do_not_add_up_to_the_count(): assert "4 of 5 allocated" in exc.value.detail +def test_validate_helper_refuses_shares_declared_without_a_count_and_rate(): + with pytest.raises(HTTPException) as exc: + _validate_ptu_model_info({"ptu_shares": {"team-a": 3, "team-b": 2}, "ptu_effective_from": _SHARED_START}) + assert exc.value.status_code == 400 + assert exc.value.detail == "ptu_count and cost_per_ptu_per_hour are required when ptu_shares is set" + + class _TeamLookup: def __init__(self, existing: frozenset[str]) -> None: self.existing: Final = existing