From e878941da5e037dc615e0a63557176a09dc8e31e Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Wed, 11 Mar 2026 19:42:29 +0530 Subject: [PATCH] Fix Decimal serialization crash in dry-run endpoint Cast Polars Decimal columns to Float64 before calling .to_dicts() in vantage_dry_run_export so the response contains JSON-serializable float values instead of decimal.Decimal objects that FastAPI cannot encode. Also cast summary totals to float for the same reason. Co-Authored-By: Claude Opus 4.6 --- .../proxy/spend_tracking/vantage_endpoints.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/spend_tracking/vantage_endpoints.py b/litellm/proxy/spend_tracking/vantage_endpoints.py index 19859bfb839..14c0ebcb54d 100644 --- a/litellm/proxy/spend_tracking/vantage_endpoints.py +++ b/litellm/proxy/spend_tracking/vantage_endpoints.py @@ -386,18 +386,35 @@ async def vantage_dry_run_export( database = FocusLiteLLMDatabase() transformer = FocusTransformer() + import polars as pl + data = await database.get_usage_data(limit=request.limit) normalized = transformer.transform(data) - usage_sample = data.head(min(50, len(data))).to_dicts() if not data.is_empty() else [] - normalized_sample = normalized.head(min(50, len(normalized))).to_dicts() if not normalized.is_empty() else [] + def _to_json_safe_dicts(frame: pl.DataFrame) -> list: + """Cast Decimal columns to Float64 so .to_dicts() produces + JSON-serializable float values instead of decimal.Decimal.""" + decimal_cols = [ + col for col, dtype in zip(frame.columns, frame.dtypes) + if isinstance(dtype, pl.Decimal) + ] + if decimal_cols: + frame = frame.with_columns( + [pl.col(c).cast(pl.Float64) for c in decimal_cols] + ) + return frame.to_dicts() + + usage_sample = _to_json_safe_dicts(data.head(min(50, len(data)))) if not data.is_empty() else [] + normalized_sample = _to_json_safe_dicts(normalized.head(min(50, len(normalized)))) if not normalized.is_empty() else [] # Use the same pre-transform column names as # FocusExportEngine.dry_run_export_usage_data for consistency. + total_spend = FocusExportEngine._sum_column(data, "spend") + total_tokens = FocusExportEngine._sum_column(data, "total_tokens") summary = { "total_records": len(normalized), - "total_spend": FocusExportEngine._sum_column(data, "spend"), - "total_tokens": FocusExportEngine._sum_column(data, "total_tokens"), + "total_spend": float(total_spend) if total_spend is not None else 0, + "total_tokens": float(total_tokens) if total_tokens is not None else 0, "unique_teams": FocusExportEngine._count_unique(data, "team_id"), "unique_models": FocusExportEngine._count_unique(data, "model"), }