-
-
Notifications
You must be signed in to change notification settings - Fork 11.6k
feat(ptu): accrue flat cost for PTU deployments declared in config.yaml #37556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yucheng-berri
merged 2 commits into
litellm_internal_staging
from
litellm_lit5809_config_yaml_ptu
Aug 20, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| """Which deployments accrue PTU flat cost, and what that costs them per token. | ||
|
|
||
| Reserved provisioned throughput is billed by the hour whether or not requests are sent, so | ||
| a deployment that accrues flat cost must not also bill per token. The two halves live here | ||
| together because they have to agree: a deployment the rollup declines to charge but the | ||
| router prices at zero serves its traffic for free. | ||
| """ | ||
|
|
||
| from collections.abc import Mapping | ||
| from dataclasses import dataclass | ||
| from datetime import datetime, timezone | ||
| from types import MappingProxyType | ||
| from typing import Final | ||
|
|
||
| from litellm.secret_managers.main import get_secret_bool | ||
| from litellm.types.router import ModelInfo | ||
| from litellm.types.utils import CustomPricingLiteLLMParams, MirroredPricingParams | ||
|
|
||
| PTU_COST_ATTRIBUTION_ENV_VAR: Final = "LITELLM_ENABLE_PTU_COST_ATTRIBUTION" | ||
|
|
||
|
|
||
| def is_ptu_cost_attribution_enabled() -> bool: | ||
| """Whether PTU flat-cost attribution is turned on for this process.""" | ||
| return get_secret_bool(PTU_COST_ATTRIBUTION_ENV_VAR, False) is True | ||
|
|
||
|
|
||
| PTU_ZEROED_PRICING_FIELDS: Final = tuple(f for f in MirroredPricingParams.model_fields if f != "tiered_pricing") + ( | ||
| "cache_creation_input_token_cost_above_1hr", | ||
| "cache_creation_input_token_cost_above_200k_tokens", | ||
| "cache_read_input_token_cost_above_200k_tokens", | ||
| ) | ||
| # tiered_pricing is emptied rather than zeroed: its tiers outrank the zeros written beside | ||
| # them, so a zero here would leave the cost map's tiers billing the traffic the reserved | ||
| # capacity already covers. | ||
| PTU_EMPTIED_PRICING_FIELDS: Final = frozenset(("tiered_pricing",)) | ||
| # search_context_cost_per_query holds its rates in a table keyed by context size, and an | ||
| # absent table means the provider's own default rather than free, so it is zeroed in place | ||
| # and written on every PTU deployment rather than only where a table is already stored. | ||
| PTU_ZEROED_TABLE_FIELDS: Final = frozenset(("search_context_cost_per_query",)) | ||
| SEARCH_CONTEXT_SIZES: Final = ("search_context_size_low", "search_context_size_medium", "search_context_size_high") | ||
| # Rate fields only. CustomPricingLiteLLMParams also carries settings that are not charges, | ||
| # and zeroing one of those would destroy the deployment's configuration rather than stop a | ||
| # charge. | ||
| CUSTOM_PRICING_FIELDS: Final = frozenset(f for f in CustomPricingLiteLLMParams.model_fields if "cost" in f) | ||
| PTU_ZEROED_PRICING: Final[Mapping[str, float | tuple[()] | Mapping[str, float]]] = MappingProxyType( | ||
| { | ||
| **dict.fromkeys(PTU_ZEROED_PRICING_FIELDS, 0.0), | ||
| **dict.fromkeys(PTU_EMPTIED_PRICING_FIELDS, ()), | ||
| **dict.fromkeys(PTU_ZEROED_TABLE_FIELDS, MappingProxyType(dict.fromkeys(SEARCH_CONTEXT_SIZES, 0.0))), | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class PTUTerms: | ||
| """The reservation a deployment declares, once every field has been validated.""" | ||
|
|
||
| team_id: str | ||
| ptu_count: int | ||
| cost_per_ptu_per_hour: float | ||
| effective_from: datetime | ||
| effective_to: datetime | None | ||
|
|
||
|
|
||
| def _to_utc(parsed: datetime) -> datetime: | ||
| """``parsed`` as UTC, reading a naive value as UTC rather than local time.""" | ||
| return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) | ||
|
|
||
|
|
||
| def _as_utc(value: object) -> datetime | None: | ||
| """A model_info datetime as UTC, parsing an ISO string, else None.""" | ||
| if isinstance(value, datetime): | ||
| return _to_utc(value) | ||
| if not isinstance(value, str): | ||
| return None | ||
| try: | ||
| return _to_utc(datetime.fromisoformat(value.replace("Z", "+00:00"))) | ||
| except ValueError: | ||
| return None | ||
|
|
||
|
|
||
| def ptu_terms(model_info: Mapping[str, object]) -> PTUTerms | None: | ||
| """The reservation this deployment accrues flat cost for, else None. | ||
|
|
||
| A start is required rather than inferred because flat cost accrues from it, and a | ||
| present but unparseable bound would read as no bound and widen the window to the whole | ||
| day, so either one leaves the deployment unpriced until the config is fixed. | ||
| """ | ||
| ptu_count: Final = model_info.get("ptu_count") | ||
| cost_per_hour: Final = model_info.get("cost_per_ptu_per_hour") | ||
| team_id: Final = model_info.get("team_id") | ||
| if ptu_count is None or cost_per_hour is None or not team_id: | ||
| return None | ||
| try: | ||
| ptu_count_int: Final = int(ptu_count) | ||
| cost_per_hour_float: Final = float(cost_per_hour) | ||
| except (TypeError, ValueError, OverflowError): | ||
| return None | ||
| if not 0 < ptu_count_int <= ModelInfo.MAX_PTU_COUNT: | ||
| return None | ||
| if not 0 <= cost_per_hour_float <= ModelInfo.MAX_COST_PER_PTU_PER_HOUR: | ||
| return None | ||
|
|
||
| raw_from: Final = model_info.get("ptu_effective_from") | ||
| raw_to: Final = model_info.get("ptu_effective_to") | ||
| effective_from: Final = _as_utc(raw_from) | ||
| effective_to: Final = _as_utc(raw_to) | ||
| if effective_from is None or (raw_to is not None and effective_to is None): | ||
| return None | ||
| if effective_to is not None and effective_to <= effective_from: | ||
| return None | ||
| return PTUTerms( | ||
| team_id=str(team_id), | ||
| ptu_count=ptu_count_int, | ||
| cost_per_ptu_per_hour=cost_per_hour_float, | ||
| effective_from=effective_from, | ||
| effective_to=effective_to, | ||
| ) | ||
|
|
||
|
|
||
| def zeroed_ptu_pricing( | ||
| model_info: Mapping[str, object], declared: Mapping[str, object] | ||
| ) -> Mapping[str, float | tuple[()] | Mapping[str, float]] | None: | ||
| """The pricing a deployment accruing flat cost must carry, else None. | ||
|
|
||
| Both conditions hold or nothing is zeroed. Without the flag no flat cost accrues, so | ||
| zeroing would leave the deployment serving for free with nothing charged in its place, | ||
| which is what an SDK user who happens to carry ptu_count would otherwise get. The terms | ||
| are checked first only because they are a few dict reads, while the flag can resolve | ||
| through a configured secret manager, and this runs for every deployment registered. | ||
|
|
||
| Any further rate the deployment itself declares is zeroed alongside the standing set, | ||
| since one left standing bills the traffic the reserved capacity already paid for. | ||
| """ | ||
| if ptu_terms(model_info) is None: | ||
| return None | ||
| if not is_ptu_cost_attribution_enabled(): | ||
| return None | ||
| return MappingProxyType( | ||
| { | ||
| **PTU_ZEROED_PRICING, | ||
| **dict.fromkeys( | ||
| CUSTOM_PRICING_FIELDS.intersection(declared) | ||
| .difference(PTU_ZEROED_TABLE_FIELDS) | ||
| .difference(PTU_EMPTIED_PRICING_FIELDS), | ||
| 0.0, | ||
| ), | ||
| } | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,12 @@ | ||
| """Opt-in flag for PTU (provisioned throughput unit) flat-cost attribution. | ||
| """Re-exported from ``litellm.litellm_core_utils.ptu_pricing``. | ||
|
|
||
| The whole feature is inert unless an operator sets | ||
| ``LITELLM_ENABLE_PTU_COST_ATTRIBUTION``: the daily rollup is not scheduled, the | ||
| model endpoints reject PTU config, the daily activity read path reports zero flat | ||
| cost, and the model form hides the PTU inputs. | ||
| The flag lives in core because the router reads it while registering a deployment, and | ||
| router code cannot import from the proxy. | ||
| """ | ||
|
|
||
| from typing import Final | ||
| from litellm.litellm_core_utils.ptu_pricing import ( | ||
| PTU_COST_ATTRIBUTION_ENV_VAR, | ||
| is_ptu_cost_attribution_enabled, | ||
| ) | ||
|
|
||
| from litellm.secret_managers.main import get_secret_bool | ||
|
|
||
| PTU_COST_ATTRIBUTION_ENV_VAR: Final = "LITELLM_ENABLE_PTU_COST_ATTRIBUTION" | ||
|
|
||
|
|
||
| def is_ptu_cost_attribution_enabled() -> bool: | ||
| """Report whether this deployment opted into PTU flat-cost attribution.""" | ||
| return get_secret_bool(PTU_COST_ATTRIBUTION_ENV_VAR, False) is True | ||
| __all__ = ("PTU_COST_ATTRIBUTION_ENV_VAR", "is_ptu_cost_attribution_enabled") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
馃煛 Reserved-capacity deployment serves traffic for free before its start date
A PTU deployment whose reservation start is in the future has its per-token pricing zeroed at registration (
zeroed_ptu_pricingatlitellm/litellm_core_utils/ptu_pricing.py:118-146) even though flat cost only begins accruing from that future start, so the team is charged nothing at all for traffic served in the gap between when the proxy boots and when the reservation begins.Impact: A team can send requests through a not-yet-active reserved deployment and be billed neither per token nor for reserved capacity, silently underbilling until the start date arrives.
How the future-dated window escapes both charge paths
ptu_termsaccepts a reservation with aneffective_fromin the future (litellm/litellm_core_utils/ptu_pricing.py:101-115only rejects an inverted or unparseable window, not a future one), sozeroed_ptu_pricingreturns the zeroed pricing map and the router registers the deployment priced at 0. Meanwhile the rollup's_active_hours_on_day(litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:213-221) clampsstarttoeffective_from, so any day before the start yields 0 active hours and no flat charge. The result is that requests served beforeeffective_fromcost the team nothing on either path. This extends the pre-existing DB-path behavior to config.yaml deployments and is acknowledged as a caveat in the PR description.Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, and as you note this is the database path's existing behaviour from #36829. Changing it here alone would split the two paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed deliberate: #36829 demonstrated a closed window live and documented it as "A closed window alerts; it does not resume per-token billing."