mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(guardrails): store an unpriced Bedrock counter as unknown, not free
A counter missing from the cost map entry was priced at 0.0 per unit, so the rollup recorded it as known-free usage. It now stamps None for that counter and the rollup writes NULL, while the per-request guardrail_cost that feeds spend and budgets still sums only the known prices. Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
This commit is contained in:
parent
4914914801
commit
caa1ab0e60
5 changed files with 77 additions and 19 deletions
|
|
@ -36,16 +36,17 @@ class GuardrailCostByUnitEntry(BaseModel):
|
|||
|
||||
model_config = ConfigDict(extra="ignore", frozen=True)
|
||||
|
||||
guardrail_cost_by_unit: Mapping[str, Annotated[float, Field(ge=0, allow_inf_nan=False)]] | None = None
|
||||
guardrail_cost_by_unit: Mapping[str, Annotated[float, Field(ge=0, allow_inf_nan=False)] | None] | None = None
|
||||
guardrail_cost_in_spend: bool | None = True
|
||||
|
||||
|
||||
_GUARDRAIL_COST_BY_UNIT_ADAPTER: Final[TypeAdapter[GuardrailCostByUnitEntry]] = TypeAdapter(GuardrailCostByUnitEntry)
|
||||
|
||||
|
||||
def billed_guardrail_cost_by_unit(raw: object) -> Mapping[str, float] | None:
|
||||
def billed_guardrail_cost_by_unit(raw: object) -> Mapping[str, float | None] | None:
|
||||
"""Per-counter USD the daily rollup may record for one raw ``guardrail_information``
|
||||
entry; None when the entry is unpriced, report-only, or malformed."""
|
||||
entry; None when the entry is unpriced, report-only, or malformed, and None per
|
||||
counter the hook had no price for."""
|
||||
try:
|
||||
entry: Final = _GUARDRAIL_COST_BY_UNIT_ADAPTER.validate_python(raw)
|
||||
except ValidationError as e:
|
||||
|
|
@ -66,20 +67,28 @@ def _bedrock_guardrail_pricing(aws_region_name: str | None) -> GuardrailPricing
|
|||
return None
|
||||
|
||||
|
||||
def _priced_units(units: int, price_per_unit: float | None) -> float | None:
|
||||
return None if price_per_unit is None else units * price_per_unit
|
||||
|
||||
|
||||
def bedrock_guardrail_cost_by_unit(
|
||||
usage_units: Mapping[str, int], aws_region_name: str | None
|
||||
) -> Mapping[str, float] | None:
|
||||
"""USD per counter, keyed like ``usage_units``; None when no pricing entry exists."""
|
||||
) -> Mapping[str, float | None] | None:
|
||||
"""USD per counter, keyed like ``usage_units``; None when no pricing entry exists,
|
||||
and None for a counter the entry has no price for, since only an explicit 0.0 means free."""
|
||||
pricing: Final = _bedrock_guardrail_pricing(aws_region_name)
|
||||
if pricing is None:
|
||||
return None
|
||||
return { # mutable-ok: stamped into guardrail_information, which safe_dumps only serializes as a plain dict
|
||||
counter: units * pricing.guardrail_cost_per_unit.get(counter, 0.0) for counter, units in usage_units.items()
|
||||
counter: _priced_units(units, pricing.guardrail_cost_per_unit.get(counter))
|
||||
for counter, units in usage_units.items()
|
||||
}
|
||||
|
||||
|
||||
def guardrail_cost_total(cost_by_unit: Mapping[str, float] | None) -> float:
|
||||
return sum(cost_by_unit.values()) if cost_by_unit is not None else 0.0
|
||||
def guardrail_cost_total(cost_by_unit: Mapping[str, float | None] | None) -> float:
|
||||
"""The scalar the spend path bills: unknown-priced counters count as 0 here, the
|
||||
rollup keeps them unknown."""
|
||||
return sum(cost for cost in cost_by_unit.values() if cost is not None) if cost_by_unit is not None else 0.0
|
||||
|
||||
|
||||
def bedrock_guardrail_cost(usage_units: Mapping[str, int], aws_region_name: str | None) -> float:
|
||||
|
|
|
|||
|
|
@ -3142,10 +3142,11 @@ class StandardLoggingGuardrailInformation(TypedDict, total=False):
|
|||
provider hook. Summed into the request's ``response_cost`` so it counts against
|
||||
spend and budgets like token cost, unless ``guardrail_cost_in_spend`` is False."""
|
||||
|
||||
guardrail_cost_by_unit: ReadOnly[Mapping[str, float] | None]
|
||||
guardrail_cost_by_unit: ReadOnly[Mapping[str, float | None] | None]
|
||||
"""``guardrail_cost`` split per ``guardrail_usage`` counter, so the daily
|
||||
per-counter usage rollup can carry cost at its own grain. Absent when the
|
||||
hook had no pricing for the invocation."""
|
||||
hook had no pricing for the invocation; a counter is None when the pricing
|
||||
entry has no price for it, which the rollup stores as unknown rather than $0."""
|
||||
|
||||
guardrail_cost_in_spend: ReadOnly[bool | None]
|
||||
"""Whether ``guardrail_cost`` participates in the request's ``response_cost`` and
|
||||
|
|
@ -3198,7 +3199,7 @@ class GuardrailTracingDetail(TypedDict, total=False):
|
|||
guardrail_action: str | None
|
||||
guardrail_usage: ReadOnly[Mapping[str, int] | None]
|
||||
guardrail_cost: ReadOnly[float | None]
|
||||
guardrail_cost_by_unit: ReadOnly[Mapping[str, float] | None]
|
||||
guardrail_cost_by_unit: ReadOnly[Mapping[str, float | None] | None]
|
||||
guardrail_cost_in_spend: ReadOnly[bool | None]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import (
|
|||
bedrock_guardrail_cost_by_unit,
|
||||
billed_guardrail_cost_by_unit,
|
||||
cost_breakdown_with_guardrail,
|
||||
guardrail_cost_total,
|
||||
guardrail_information_cost,
|
||||
)
|
||||
|
||||
|
|
@ -60,16 +61,19 @@ def test_bedrock_guardrail_cost_no_pricing_entry(monkeypatch):
|
|||
|
||||
def test_bedrock_guardrail_cost_by_unit_prices_every_counter_it_was_given(synthetic_cost_map):
|
||||
"""LIT-5652: the daily rollup stores one row per counter, so pricing must come
|
||||
back at that grain, keyed exactly like the usage (free and unknown counters
|
||||
included at 0.0) and summing to the scalar the spend path bills."""
|
||||
back at that grain, keyed exactly like the usage. An explicit 0.0 in the cost
|
||||
map is free; a counter the map does not list is unknown (None), never free,
|
||||
while the scalar the spend path bills still sums only the known prices."""
|
||||
usage = {"contentPolicyUnits": 2, "topicPolicyUnits": 1, "wordPolicyUnits": 5, "someFutureCounter": 3}
|
||||
by_unit = bedrock_guardrail_cost_by_unit(usage_units=usage, aws_region_name="us-east-1")
|
||||
assert by_unit is not None
|
||||
assert by_unit.keys() == usage.keys()
|
||||
assert by_unit["contentPolicyUnits"] == pytest.approx(0.0003)
|
||||
assert by_unit["topicPolicyUnits"] == pytest.approx(0.00015)
|
||||
assert (by_unit["wordPolicyUnits"], by_unit["someFutureCounter"]) == (0.0, 0.0)
|
||||
assert sum(by_unit.values()) == pytest.approx(
|
||||
assert by_unit["wordPolicyUnits"] == 0.0
|
||||
assert by_unit["someFutureCounter"] is None
|
||||
assert guardrail_cost_total(by_unit) == pytest.approx(0.00045)
|
||||
assert guardrail_cost_total(by_unit) == pytest.approx(
|
||||
bedrock_guardrail_cost(usage_units=usage, aws_region_name="us-east-1")
|
||||
)
|
||||
|
||||
|
|
@ -83,8 +87,15 @@ def test_bedrock_guardrail_cost_by_unit_is_none_without_pricing_so_unpriced_is_n
|
|||
|
||||
|
||||
def test_billed_guardrail_cost_by_unit_reads_the_hook_stamp():
|
||||
entry = {"guardrail_name": "bedrock", "guardrail_cost_by_unit": {"contentPolicyUnits": 0.15, "wordPolicyUnits": 0}}
|
||||
assert billed_guardrail_cost_by_unit(entry) == {"contentPolicyUnits": 0.15, "wordPolicyUnits": 0.0}
|
||||
entry = {
|
||||
"guardrail_name": "bedrock",
|
||||
"guardrail_cost_by_unit": {"contentPolicyUnits": 0.15, "wordPolicyUnits": 0, "someFutureCounter": None},
|
||||
}
|
||||
assert billed_guardrail_cost_by_unit(entry) == {
|
||||
"contentPolicyUnits": 0.15,
|
||||
"wordPolicyUnits": 0.0,
|
||||
"someFutureCounter": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
|
|
@ -5095,18 +5095,31 @@ def test_build_tracing_detail_surfaces_usage_counters_and_cost(monkeypatch):
|
|||
detail = guardrail._build_tracing_detail(
|
||||
{
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"usage": {"topicPolicyUnits": 1, "contentPolicyUnits": 2, "wordPolicyUnits": 0, "oddball": "not-an-int"},
|
||||
"usage": {
|
||||
"topicPolicyUnits": 1,
|
||||
"contentPolicyUnits": 2,
|
||||
"wordPolicyUnits": 0,
|
||||
"someFutureCounter": 3,
|
||||
"oddball": "not-an-int",
|
||||
},
|
||||
},
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
assert detail["guardrail_usage"] == {"topicPolicyUnits": 1, "contentPolicyUnits": 2, "wordPolicyUnits": 0}
|
||||
assert detail["guardrail_usage"] == {
|
||||
"topicPolicyUnits": 1,
|
||||
"contentPolicyUnits": 2,
|
||||
"wordPolicyUnits": 0,
|
||||
"someFutureCounter": 3,
|
||||
}
|
||||
assert detail["guardrail_cost"] == pytest.approx(0.00045)
|
||||
by_unit = detail["guardrail_cost_by_unit"]
|
||||
assert by_unit is not None and by_unit.keys() == detail["guardrail_usage"].keys()
|
||||
assert by_unit["topicPolicyUnits"] == pytest.approx(0.00015)
|
||||
assert by_unit["contentPolicyUnits"] == pytest.approx(0.0003)
|
||||
assert by_unit["wordPolicyUnits"] == 0.0
|
||||
assert by_unit["someFutureCounter"] is None
|
||||
assert by_unit["wordPolicyUnits"] == 0.0
|
||||
|
||||
|
||||
def test_build_tracing_detail_omits_cost_by_unit_when_unpriced_but_keeps_scalar_zero(monkeypatch):
|
||||
|
|
|
|||
|
|
@ -373,6 +373,30 @@ async def test_cost_rolled_up_per_counter_alongside_units():
|
|||
assert costs["wordPolicyUnits"] == (0.0, {"increment": 0.0})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_counter_the_hook_could_not_price_is_stored_unknown_not_free():
|
||||
"""A counter the cost map does not list arrives stamped as None. Its row must
|
||||
carry NULL, while the priced counter on the same request keeps its cost."""
|
||||
prisma = _prisma()
|
||||
logs = [
|
||||
_payload(
|
||||
"r1",
|
||||
usage={"contentPolicyUnits": 1000, "someFutureCounter": 3},
|
||||
cost_by_unit={"contentPolicyUnits": 0.15, "someFutureCounter": None},
|
||||
)
|
||||
]
|
||||
|
||||
await process_spend_logs_guardrail_usage(prisma, logs)
|
||||
|
||||
assert _units_upserts(prisma) == {
|
||||
("bedrock-guard", "2026-08-17", "team-a", "hashed-key-1", "contentPolicyUnits"): 1000,
|
||||
("bedrock-guard", "2026-08-17", "team-a", "hashed-key-1", "someFutureCounter"): 3,
|
||||
}
|
||||
costs = _cost_upserts(prisma)
|
||||
assert costs["contentPolicyUnits"] == (pytest.approx(0.15), {"increment": pytest.approx(0.15)})
|
||||
assert costs["someFutureCounter"] == (None, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unpriced_increment_makes_the_rows_cost_unknown_not_partial():
|
||||
"""A payload with usage but no per-counter cost (a hook without pricing, a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue