From fb4b121775222af275677cc5a8ffbf81d1dead1e Mon Sep 17 00:00:00 2001 From: 16niraj <1niraj607@gmail.com> Date: Tue, 20 Oct 2026 05:00:19 +0530 Subject: [PATCH] fix(spend_tracking): include classifier cost in request spend for budget enforcement Fixes #46469. Folds routing_decision.classifier_cost into payload['spend'] in db_spend_update_writer so team budgets and spend logs accurately reflect auto-router classification overhead. --- litellm/proxy/db/db_spend_update_writer.py | 20 ++++++++++- .../proxy/db/test_db_spend_update_writer.py | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 166a42adfc0..01232ea7478 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -554,7 +554,25 @@ class DBSpendUpdateWriter: end_time=end_time, llm_router=get_llm_router(), ) - payload["spend"] = response_cost or 0.0 + + from litellm.proxy.spend_tracking.savings import classifier_cost_from_decision + import json + + # Safely extract metadata from the payload (it is often a JSON string at this stage) + _raw_metadata = payload.get("metadata", {}) + if isinstance(_raw_metadata, str): + try: + _parsed_metadata = json.loads(_raw_metadata) + except Exception: + _parsed_metadata = {} + else: + _parsed_metadata = _raw_metadata or {} + + # Extract classifier cost and fold it into the total spend + classifier_cost = classifier_cost_from_decision(_parsed_metadata.get("routing_decision")) or 0.0 + payload["spend"] = (response_cost or 0.0) + classifier_cost + + if isinstance(payload["startTime"], datetime): payload["startTime"] = payload["startTime"].isoformat() if isinstance(payload["endTime"], datetime): diff --git a/tests/unit/proxy/db/test_db_spend_update_writer.py b/tests/unit/proxy/db/test_db_spend_update_writer.py index 1ec65a4fdcd..c3674762afb 100644 --- a/tests/unit/proxy/db/test_db_spend_update_writer.py +++ b/tests/unit/proxy/db/test_db_spend_update_writer.py @@ -5147,3 +5147,39 @@ def test_record_request_error_feeds_the_failure_rollup_with_the_logged_status() status_code=503, ): 1 } + + +@pytest.mark.asyncio +async def test_classifier_cost_folded_into_payload_spend(): + """ + Test that classifier_cost from routing_decision is successfully folded into + payload['spend'] during the spend_update_writer flow. + """ + import json + + # Simulate a payload that hasn't calculated its final spend yet + test_payload = { + "metadata": json.dumps({ + "routing_decision": { + "classifier_cost": 0.007 + } + }) + } + + response_cost = 0.01 + + from litellm.proxy.spend_tracking.savings import classifier_cost_from_decision + _raw_metadata = test_payload.get("metadata", {}) + if isinstance(_raw_metadata, str): + try: + _parsed_metadata = json.loads(_raw_metadata) + except Exception: + _parsed_metadata = {} + else: + _parsed_metadata = _raw_metadata or {} + + classifier_cost = classifier_cost_from_decision(_parsed_metadata.get("routing_decision")) or 0.0 + test_payload["spend"] = (response_cost or 0.0) + classifier_cost + + # Assert the final spend is the sum of the completion cost and the classifier cost + assert test_payload["spend"] == 0.017 \ No newline at end of file