Skip to content

Commit c2b3c4b

Browse files
feat(ptu): accrue flat cost for PTU deployments declared in config.yaml (#37556)
* feat(ptu): accrue flat cost for PTU deployments declared in config.yaml The flat-cost rollup reads deployments from LiteLLM_ProxyModelTable, and config.yaml models never reach that table by design, so a PTU deployment declared there accrued no flat cost at all while still billing its traffic per token. The provider bills the reservation whichever file declared it. The rollup now also reads the deployments the router holds that no database row owns, identified by db_model, skipping the per-request credential clones that carry original_model_id and reuse their source's PTU config under a fresh id. Registering such a deployment zeroes its pricing, since reserved capacity already pays for the traffic it serves, and leaving a rate unset falls back to the public cost map, which makes the double charge the default rather than an opt-in. The rules both halves apply now live in one module. The rollup's test for what it will charge and the router's test for what to zero have to agree, or a deployment one accepts and the other declines serves its traffic for free. That module also owns the fields the write endpoints already zero, so the two paths cannot drift: tiered_pricing is emptied rather than zeroed because its tiers outrank the rates beside them, the search context table is written zeroed because an absent one means the provider default, and any further rate the deployment itself declares is zeroed alongside the standing set. The prune is bounded to the deployments a run scanned, but only for a run that priced a config-declared deployment. Deciding a row is garbage on staleness alone stays correct while every run derives its charges from the same table, so a database-only run sweeps exactly as it did before; once one host's charges come from a file the others cannot read, a row it never considered is not evidence of anything. Behaviour change worth calling out: a zeroed deployment sorts ahead of an unpriced sibling in QualityRouter's cost tiebreak, where an unset rate previously sorted last. Reserved capacity really is the cheaper choice, but the ordering moves. * refactor(ptu): drop a Final rebind and two redundant isinstance guards The basedpyright budget rejected reassigning a Final in the datetime coercion and two isinstance calls the router entry's own type already guarantees. Filtering the built records rather than the raw entries removes both guards and leaves _router_deployment as the single validator.
1 parent a0f367f commit c2b3c4b

8 files changed

Lines changed: 890 additions & 117 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Which deployments accrue PTU flat cost, and what that costs them per token.
2+
3+
Reserved provisioned throughput is billed by the hour whether or not requests are sent, so
4+
a deployment that accrues flat cost must not also bill per token. The two halves live here
5+
together because they have to agree: a deployment the rollup declines to charge but the
6+
router prices at zero serves its traffic for free.
7+
"""
8+
9+
from collections.abc import Mapping
10+
from dataclasses import dataclass
11+
from datetime import datetime, timezone
12+
from types import MappingProxyType
13+
from typing import Final
14+
15+
from litellm.secret_managers.main import get_secret_bool
16+
from litellm.types.router import ModelInfo
17+
from litellm.types.utils import CustomPricingLiteLLMParams, MirroredPricingParams
18+
19+
PTU_COST_ATTRIBUTION_ENV_VAR: Final = "LITELLM_ENABLE_PTU_COST_ATTRIBUTION"
20+
21+
22+
def is_ptu_cost_attribution_enabled() -> bool:
23+
"""Whether PTU flat-cost attribution is turned on for this process."""
24+
return get_secret_bool(PTU_COST_ATTRIBUTION_ENV_VAR, False) is True
25+
26+
27+
PTU_ZEROED_PRICING_FIELDS: Final = tuple(f for f in MirroredPricingParams.model_fields if f != "tiered_pricing") + (
28+
"cache_creation_input_token_cost_above_1hr",
29+
"cache_creation_input_token_cost_above_200k_tokens",
30+
"cache_read_input_token_cost_above_200k_tokens",
31+
)
32+
# tiered_pricing is emptied rather than zeroed: its tiers outrank the zeros written beside
33+
# them, so a zero here would leave the cost map's tiers billing the traffic the reserved
34+
# capacity already covers.
35+
PTU_EMPTIED_PRICING_FIELDS: Final = frozenset(("tiered_pricing",))
36+
# search_context_cost_per_query holds its rates in a table keyed by context size, and an
37+
# absent table means the provider's own default rather than free, so it is zeroed in place
38+
# and written on every PTU deployment rather than only where a table is already stored.
39+
PTU_ZEROED_TABLE_FIELDS: Final = frozenset(("search_context_cost_per_query",))
40+
SEARCH_CONTEXT_SIZES: Final = ("search_context_size_low", "search_context_size_medium", "search_context_size_high")
41+
# Rate fields only. CustomPricingLiteLLMParams also carries settings that are not charges,
42+
# and zeroing one of those would destroy the deployment's configuration rather than stop a
43+
# charge.
44+
CUSTOM_PRICING_FIELDS: Final = frozenset(f for f in CustomPricingLiteLLMParams.model_fields if "cost" in f)
45+
PTU_ZEROED_PRICING: Final[Mapping[str, float | tuple[()] | Mapping[str, float]]] = MappingProxyType(
46+
{
47+
**dict.fromkeys(PTU_ZEROED_PRICING_FIELDS, 0.0),
48+
**dict.fromkeys(PTU_EMPTIED_PRICING_FIELDS, ()),
49+
**dict.fromkeys(PTU_ZEROED_TABLE_FIELDS, MappingProxyType(dict.fromkeys(SEARCH_CONTEXT_SIZES, 0.0))),
50+
}
51+
)
52+
53+
54+
@dataclass(frozen=True, slots=True)
55+
class PTUTerms:
56+
"""The reservation a deployment declares, once every field has been validated."""
57+
58+
team_id: str
59+
ptu_count: int
60+
cost_per_ptu_per_hour: float
61+
effective_from: datetime
62+
effective_to: datetime | None
63+
64+
65+
def _to_utc(parsed: datetime) -> datetime:
66+
"""``parsed`` as UTC, reading a naive value as UTC rather than local time."""
67+
return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc)
68+
69+
70+
def _as_utc(value: object) -> datetime | None:
71+
"""A model_info datetime as UTC, parsing an ISO string, else None."""
72+
if isinstance(value, datetime):
73+
return _to_utc(value)
74+
if not isinstance(value, str):
75+
return None
76+
try:
77+
return _to_utc(datetime.fromisoformat(value.replace("Z", "+00:00")))
78+
except ValueError:
79+
return None
80+
81+
82+
def ptu_terms(model_info: Mapping[str, object]) -> PTUTerms | None:
83+
"""The reservation this deployment accrues flat cost for, else None.
84+
85+
A start is required rather than inferred because flat cost accrues from it, and a
86+
present but unparseable bound would read as no bound and widen the window to the whole
87+
day, so either one leaves the deployment unpriced until the config is fixed.
88+
"""
89+
ptu_count: Final = model_info.get("ptu_count")
90+
cost_per_hour: Final = model_info.get("cost_per_ptu_per_hour")
91+
team_id: Final = model_info.get("team_id")
92+
if ptu_count is None or cost_per_hour is None or not team_id:
93+
return None
94+
try:
95+
ptu_count_int: Final = int(ptu_count)
96+
cost_per_hour_float: Final = float(cost_per_hour)
97+
except (TypeError, ValueError, OverflowError):
98+
return None
99+
if not 0 < ptu_count_int <= ModelInfo.MAX_PTU_COUNT:
100+
return None
101+
if not 0 <= cost_per_hour_float <= ModelInfo.MAX_COST_PER_PTU_PER_HOUR:
102+
return None
103+
104+
raw_from: Final = model_info.get("ptu_effective_from")
105+
raw_to: Final = model_info.get("ptu_effective_to")
106+
effective_from: Final = _as_utc(raw_from)
107+
effective_to: Final = _as_utc(raw_to)
108+
if effective_from is None or (raw_to is not None and effective_to is None):
109+
return None
110+
if effective_to is not None and effective_to <= effective_from:
111+
return None
112+
return PTUTerms(
113+
team_id=str(team_id),
114+
ptu_count=ptu_count_int,
115+
cost_per_ptu_per_hour=cost_per_hour_float,
116+
effective_from=effective_from,
117+
effective_to=effective_to,
118+
)
119+
120+
121+
def zeroed_ptu_pricing(
122+
model_info: Mapping[str, object], declared: Mapping[str, object]
123+
) -> Mapping[str, float | tuple[()] | Mapping[str, float]] | None:
124+
"""The pricing a deployment accruing flat cost must carry, else None.
125+
126+
Both conditions hold or nothing is zeroed. Without the flag no flat cost accrues, so
127+
zeroing would leave the deployment serving for free with nothing charged in its place,
128+
which is what an SDK user who happens to carry ptu_count would otherwise get. The terms
129+
are checked first only because they are a few dict reads, while the flag can resolve
130+
through a configured secret manager, and this runs for every deployment registered.
131+
132+
Any further rate the deployment itself declares is zeroed alongside the standing set,
133+
since one left standing bills the traffic the reserved capacity already paid for.
134+
"""
135+
if ptu_terms(model_info) is None:
136+
return None
137+
if not is_ptu_cost_attribution_enabled():
138+
return None
139+
return MappingProxyType(
140+
{
141+
**PTU_ZEROED_PRICING,
142+
**dict.fromkeys(
143+
CUSTOM_PRICING_FIELDS.intersection(declared)
144+
.difference(PTU_ZEROED_TABLE_FIELDS)
145+
.difference(PTU_EMPTIED_PRICING_FIELDS),
146+
0.0,
147+
),
148+
}
149+
)

litellm/proxy/management_endpoints/model_management_endpoints.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
from litellm._logging import verbose_proxy_logger
2525
from litellm._uuid import uuid
2626
from litellm.constants import LITELLM_PROXY_ADMIN_NAME
27+
from litellm.litellm_core_utils.ptu_pricing import (
28+
CUSTOM_PRICING_FIELDS,
29+
PTU_EMPTIED_PRICING_FIELDS,
30+
PTU_ZEROED_PRICING_FIELDS,
31+
PTU_ZEROED_TABLE_FIELDS,
32+
SEARCH_CONTEXT_SIZES,
33+
)
2734
from litellm.proxy._types import (
2835
BlockModelRequest,
2936
CommonProxyErrors,
@@ -89,7 +96,6 @@
8996
ModelInfo,
9097
updateDeployment,
9198
)
92-
from litellm.types.utils import CustomPricingLiteLLMParams
9399
from litellm.utils import get_utc_datetime
94100

95101
router: Final = APIRouter()
@@ -346,12 +352,8 @@ def _validate_ptu_model_info(model_info: Mapping[str, object]) -> None:
346352
# tiered_pricing is the one mirrored field that is a table of ranges, not a rate, so it is stored
347353
# empty (see _PTU_EMPTIED_PRICING_FIELDS): its tiers outrank the zeros written beside them, so
348354
# dropping it would leave the cost map's tiers billing the traffic the reserved capacity covers.
349-
_PTU_ZEROED_PRICING_FIELDS: Final = tuple(f for f in SPECIAL_MODEL_INFO_PARAMS if f != "tiered_pricing") + (
350-
"cache_creation_input_token_cost_above_1hr",
351-
"cache_creation_input_token_cost_above_200k_tokens",
352-
"cache_read_input_token_cost_above_200k_tokens",
353-
)
354-
_PTU_EMPTIED_PRICING_FIELDS: Final = frozenset({"tiered_pricing"})
355+
_PTU_ZEROED_PRICING_FIELDS: Final = PTU_ZEROED_PRICING_FIELDS
356+
_PTU_EMPTIED_PRICING_FIELDS: Final = PTU_EMPTIED_PRICING_FIELDS
355357
_PTU_ZEROED_PRICING: Final[Mapping[str, float | tuple[()]]] = MappingProxyType(
356358
{
357359
**dict.fromkeys(_PTU_ZEROED_PRICING_FIELDS, 0.0),
@@ -363,13 +365,13 @@ def _validate_ptu_model_info(model_info: Mapping[str, object]) -> None:
363365
# Rate fields only. CustomPricingLiteLLMParams also carries settings that are not charges
364366
# (an embedding's output_vector_size, the regional uplift multipliers), and zeroing one of
365367
# those would destroy the deployment's configuration rather than stop a charge.
366-
_CUSTOM_PRICING_FIELDS: Final = frozenset(f for f in CustomPricingLiteLLMParams.model_fields if "cost" in f)
368+
_CUSTOM_PRICING_FIELDS: Final = CUSTOM_PRICING_FIELDS
367369
# search_context_cost_per_query holds its rates in a table keyed by context size, and an absent
368370
# table means the provider's own default rate rather than free (litellm/llms/gemini/cost_calculator
369371
# falls back to $0.035), so it is zeroed in place rather than emptied like tiered_pricing, and
370372
# written on every PTU deployment rather than only where a table is already stored.
371-
_PTU_ZEROED_TABLE_FIELDS: Final = frozenset({"search_context_cost_per_query"})
372-
_SEARCH_CONTEXT_SIZES: Final = ("search_context_size_low", "search_context_size_medium", "search_context_size_high")
373+
_PTU_ZEROED_TABLE_FIELDS: Final = PTU_ZEROED_TABLE_FIELDS
374+
_SEARCH_CONTEXT_SIZES: Final = SEARCH_CONTEXT_SIZES
373375

374376

375377
def _is_nonzero_rate(value: object) -> bool:
Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
"""Opt-in flag for PTU (provisioned throughput unit) flat-cost attribution.
1+
"""Re-exported from ``litellm.litellm_core_utils.ptu_pricing``.
22
3-
The whole feature is inert unless an operator sets
4-
``LITELLM_ENABLE_PTU_COST_ATTRIBUTION``: the daily rollup is not scheduled, the
5-
model endpoints reject PTU config, the daily activity read path reports zero flat
6-
cost, and the model form hides the PTU inputs.
3+
The flag lives in core because the router reads it while registering a deployment, and
4+
router code cannot import from the proxy.
75
"""
86

9-
from typing import Final
7+
from litellm.litellm_core_utils.ptu_pricing import (
8+
PTU_COST_ATTRIBUTION_ENV_VAR,
9+
is_ptu_cost_attribution_enabled,
10+
)
1011

11-
from litellm.secret_managers.main import get_secret_bool
12-
13-
PTU_COST_ATTRIBUTION_ENV_VAR: Final = "LITELLM_ENABLE_PTU_COST_ATTRIBUTION"
14-
15-
16-
def is_ptu_cost_attribution_enabled() -> bool:
17-
"""Report whether this deployment opted into PTU flat-cost attribution."""
18-
return get_secret_bool(PTU_COST_ATTRIBUTION_ENV_VAR, False) is True
12+
__all__ = ("PTU_COST_ATTRIBUTION_ENV_VAR", "is_ptu_cost_attribution_enabled")

0 commit comments

Comments
 (0)