diff --git a/litellm/proxy/guardrails/usage_tracking.py b/litellm/proxy/guardrails/usage_tracking.py index 4c8222f52ab..b2ba6ec9ec5 100644 --- a/litellm/proxy/guardrails/usage_tracking.py +++ b/litellm/proxy/guardrails/usage_tracking.py @@ -356,12 +356,12 @@ async def process_spend_logs_guardrail_usage( "flagged_count": 0, } ) - index_rows_by_key: Final[dict[tuple[object, object], dict[str, object]]] = {} + index_rows_by_key: Final[dict[tuple[str, str], dict[str, object]]] = {} for payload in logs_to_process: request_id = payload.get("request_id") start_time = _parse_payload_start_time(payload) - if not request_id or start_time is None: + if not isinstance(request_id, str) or not request_id or start_time is None: continue date_key = _date_str(start_time) @@ -370,13 +370,14 @@ async def process_spend_logs_guardrail_usage( { e["guardrail_name"]: e["guardrail_id"] for e in entries - if e.get("guardrail_id") and e.get("guardrail_name") + if e.get("guardrail_id") and isinstance(e.get("guardrail_name"), str) } ) for entry in entries: - guardrail_name = entry.get("guardrail_name") or "" + raw_name = entry.get("guardrail_name") + guardrail_name = raw_name if isinstance(raw_name, str) else "" guardrail_id = entry.get("guardrail_id") or ids_by_name.get(guardrail_name) or guardrail_name - if not guardrail_id: + if not isinstance(guardrail_id, str) or not guardrail_id: continue action = guardrail_status_to_action(entry.get("guardrail_status")) if action != "not_run": diff --git a/tests/test_litellm/proxy/guardrails/test_usage_tracking.py b/tests/test_litellm/proxy/guardrails/test_usage_tracking.py index 58479d3d740..56a8bf2f0b0 100644 --- a/tests/test_litellm/proxy/guardrails/test_usage_tracking.py +++ b/tests/test_litellm/proxy/guardrails/test_usage_tracking.py @@ -405,6 +405,27 @@ async def test_not_run_entry_shares_index_key_with_evaluated_sibling_of_same_nam assert (metrics_create["guardrail_id"], metrics_create["requests_evaluated"]) == ("cf-uuid", 1) +@pytest.mark.asyncio +async def test_malformed_not_run_entry_does_not_drop_the_batch(): + prisma = _prisma() + payload = _payload("r1") + payload["metadata"] = json.dumps( + { + "guardrail_information": [ + {"guardrail_name": ["not", "a", "string"], "guardrail_status": "not_run"}, + {"guardrail_name": "cf", "guardrail_id": "cf-uuid", "guardrail_status": "success"}, + ] + } + ) + + await process_spend_logs_guardrail_usage(prisma, [payload]) + + index_rows = prisma.db.litellm_spendlogguardrailindex.create_many.call_args.kwargs["data"] + assert [row["guardrail_id"] for row in index_rows] == ["cf-uuid"] + metrics_create = prisma.db.litellm_dailyguardrailmetrics.upsert.call_args.kwargs["data"]["create"] + assert metrics_create["requests_evaluated"] == 1 + + @pytest.mark.asyncio async def test_batch_of_only_not_run_entries_writes_no_metrics_row(): prisma = _prisma()