diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index cf7bedfdc71..0bf35352d5f 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1624,7 +1624,7 @@ async def ui_view_spend_logs( ), session_id: str | None = fastapi.Query( default=None, - description="Filter spend logs by session_id", + description="Filter spend logs by session_id (partial string match)", ), team_id: str | None = fastapi.Query( default=None, @@ -1776,9 +1776,6 @@ async def ui_view_spend_logs( if request_id is not None: where_conditions["request_id"] = request_id - if session_id is not None: - where_conditions["session_id"] = session_id - if model is not None: where_conditions["model"] = model @@ -1894,7 +1891,6 @@ async def ui_view_spend_logs( ('"user"', "user"), ("api_key", "api_key"), ("request_id", "request_id"), - ("session_id", "session_id"), ("model", "model"), ("model_id", "model_id"), ("model_group", "model_group"), @@ -1914,6 +1910,12 @@ async def ui_view_spend_logs( p += 2 sql_conditions.append(or_clause) + if session_id is not None: + like_escaped_session_id = session_id.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + sql_conditions.append(f"session_id LIKE ${p}") + sql_params.append(f"%{like_escaped_session_id}%") + p += 1 + # Status filter if status_filter is not None: if status_filter == "success": diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 69ffc2275ff..c3899883d58 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -86,7 +86,6 @@ def _reconstruct_ui_where_from_sql(sql_query, params): '"user"': "user", "api_key": "api_key", "request_id": "request_id", - "session_id": "session_id", "model": "model", "model_id": "model_id", "model_group": "model_group", @@ -100,6 +99,7 @@ def _reconstruct_ui_where_from_sql(sql_query, params): alias = re.search(r"user_api_key_alias' LIKE \$(\d+)", cond) code = re.search(r"error_code' = \$(\d+)", cond) msg = re.search(r"error_message' LIKE \$(\d+)", cond) + sess = re.fullmatch(r"session_id LIKE \$(\d+)", cond) status = re.fullmatch(r"status = \$(\d+)", cond) if gte: date_bounds["gte"] = _iso(params[int(gte.group(1)) - 1]) @@ -109,6 +109,8 @@ def _reconstruct_ui_where_from_sql(sql_query, params): where["OR"] = where.get("OR", []) + [{"multi_team": True}] elif "status = 'success'" in cond: where["OR"] = where.get("OR", []) + [{"status": "success"}] + elif sess: + where["session_id"] = {"contains": str(params[int(sess.group(1)) - 1]).strip("%")} elif status: where["status"] = {"equals": params[int(status.group(1)) - 1]} elif alias: @@ -165,16 +167,14 @@ def make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_fn, team_lookup_fn=No return len(filter_fn(kwargs.get("where", {}))) async def group_by(self, by, where, count): - allowed = set(where["session_id"]["in"]) + col = by[0] + allowed = where.get(col, {}).get("in") tallied = collections.Counter( - log["session_id"] + log[col] for log in mock_spend_logs - if log.get("session_id") in allowed + if log.get(col) is not None and (allowed is None or log[col] in allowed) ) - return [ - {"session_id": sid, "_count": {"session_id": n}} - for sid, n in tallied.items() - ] + return [{col: value, "_count": {col: n}} for value, n in tallied.items()] async def query_raw(self, sql_query, *params): if "mcp_tool_call_count" in sql_query: @@ -614,48 +614,47 @@ async def test_ui_view_spend_logs_with_user_id(client, monkeypatch): @pytest.mark.asyncio -async def test_ui_view_spend_logs_with_session_id(client, monkeypatch): - mock_spend_logs = [ - { - "id": "log1", - "request_id": "req1", +@pytest.mark.parametrize( + "session_id_query,expected_request_ids", + [ + ("session-filter-demo-1", {"req1", "req2"}), + ("session-filter-demo-2", {"req3"}), + ("session-filter", {"req1", "req2", "req3"}), + ("demo", {"req1", "req2", "req3"}), + ("no-such-session", set()), + ], +) +async def test_ui_view_spend_logs_with_session_id( + client, monkeypatch, session_id_query, expected_request_ids +): + def make_log(request_id, session_id): + return { + "id": f"log-{request_id}", + "request_id": request_id, "api_key": "sk-test-key", "user": "test_user_1", - "session_id": "session-abc", + "session_id": session_id, "spend": 0.05, "startTime": datetime.datetime.now(timezone.utc).isoformat(), "model": "gpt-4", - }, - { - "id": "log2", - "request_id": "req2", - "api_key": "sk-test-key", - "user": "test_user_1", - "session_id": "session-abc", - "spend": 0.10, - "startTime": datetime.datetime.now(timezone.utc).isoformat(), - "model": "gpt-4", - }, - { - "id": "log3", - "request_id": "req3", - "api_key": "sk-test-key", - "user": "test_user_1", - "session_id": "session-other", - "spend": 0.02, - "startTime": datetime.datetime.now(timezone.utc).isoformat(), - "model": "gpt-4", - }, + } + + mock_spend_logs = [ + make_log("req1", "session-filter-demo-1"), + make_log("req2", "session-filter-demo-1"), + make_log("req3", "session-filter-demo-2"), + make_log("req4", "unrelated-abc"), ] def filter_by_session(where): - if "session_id" in where: - return [ - log - for log in mock_spend_logs - if log["session_id"] == where["session_id"] - ] - return mock_spend_logs + session_filter = where.get("session_id") + if session_filter is None: + return mock_spend_logs + return [ + log + for log in mock_spend_logs + if session_filter["contains"] in log["session_id"] + ] monkeypatch.setattr( "litellm.proxy.proxy_server.prisma_client", @@ -667,7 +666,7 @@ async def test_ui_view_spend_logs_with_session_id(client, monkeypatch): response = client.get( "/spend/logs/ui", params={ - "session_id": "session-abc", + "session_id": session_id_query, "start_date": start_date, "end_date": end_date, }, @@ -676,9 +675,9 @@ async def test_ui_view_spend_logs_with_session_id(client, monkeypatch): assert response.status_code == 200 data = response.json() - assert data["total"] == 2 - assert {log["request_id"] for log in data["data"]} == {"req1", "req2"} - assert all(log["session_id"] == "session-abc" for log in data["data"]) + assert data["total"] == len(expected_request_ids) + assert {log["request_id"] for log in data["data"]} == expected_request_ids + assert all(session_id_query in log["session_id"] for log in data["data"]) # Mock spend logs with distinct values for sorting tests. diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 8e26729aa18..4ca2f85be2b 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -48633,7 +48633,7 @@ export interface operations { user_id?: string | null; /** @description request_id to get spend logs for specific request_id */ request_id?: string | null; - /** @description Filter spend logs by session_id */ + /** @description Filter spend logs by session_id (partial string match) */ session_id?: string | null; /** @description Filter spend logs by team_id */ team_id?: string | null; @@ -48741,7 +48741,7 @@ export interface operations { user_id?: string | null; /** @description request_id to get spend logs for specific request_id */ request_id?: string | null; - /** @description Filter spend logs by session_id */ + /** @description Filter spend logs by session_id (partial string match) */ session_id?: string | null; /** @description Filter spend logs by team_id */ team_id?: string | null;