Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions litellm/proxy/spend_tracking/spend_management_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,10 @@ async def ui_view_spend_logs(
default=None,
description="request_id to get spend logs for specific request_id",
),
session_id: str | None = fastapi.Query(
default=None,
description="Filter spend logs by session_id (partial string match)",
),
team_id: str | None = fastapi.Query(
default=None,
description="Filter spend logs by team_id",
Expand Down Expand Up @@ -1906,6 +1910,12 @@ def parse_date(date_str: str) -> datetime:
p += 2
sql_conditions.append(or_clause)

if session_id is not None and isinstance(session_id, str):
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}%")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIKE escape clause missing

Medium Severity

The session_id filter escapes \, %, and _ in the search text but uses session_id LIKE $p without an ESCAPE clause. In PostgreSQL, % and _ stay wildcard metacharacters in that form, so filters containing those characters (or a lone %) can return far more rows than a substring match should.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8a44fdd. Configure here.

p += 1

# Status filter
if status_filter is not None:
if status_filter == "success":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import collections
import datetime
import json
import os
Expand Down Expand Up @@ -98,6 +99,7 @@ def _iso(value):
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])
Expand All @@ -107,6 +109,8 @@ def _iso(value):
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:
Expand Down Expand Up @@ -162,7 +166,19 @@ class MockDB:
async def count(self, *args, **kwargs):
return len(filter_fn(kwargs.get("where", {})))

async def group_by(self, by, where, count):
col = by[0]
allowed = where.get(col, {}).get("in")
tallied = collections.Counter(
log[col]
for log in mock_spend_logs
if log.get(col) is not None and (allowed is None or log[col] in allowed)
)
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:
return []
filtered = filter_fn(_reconstruct_ui_where_from_sql(sql_query, params))
total = len(filtered)
if "COUNT(*)" in sql_query:
Expand Down Expand Up @@ -597,6 +613,73 @@ def filter_by_user(where):
assert data["data"][0]["user"] == "test_user_1"


@pytest.mark.asyncio
@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_id,
"spend": 0.05,
"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):
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",
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_session),
)

start_date, end_date = _default_date_range()

response = client.get(
"/spend/logs/ui",
params={
"session_id": session_id_query,
"start_date": start_date,
"end_date": end_date,
},
headers={"Authorization": "Bearer sk-test"},
)

assert response.status_code == 200
data = response.json()
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.
# req_a: spend=0.10, tokens=500, start/end earliest
# req_b: spend=0.05, tokens=200, start/end 2nd
Expand Down
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/eslint-metrics.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"@typescript-eslint/no-explicit-any": 1982,
"complexity": 128,
"local/no-large-inline-object-arg": 512,
"local/no-large-inline-object-arg": 519,
"local/no-long-condition-chain": 233,
"max-depth": 59,
"no-console": 15
Expand Down
1 change: 1 addition & 0 deletions ui/litellm-dashboard/src/components/networking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
})();
export let proxyBaseUrl: string | null = _initialWorkerUrl ?? defaultProxyBaseUrl;
if (isLocal != true) {
console.log = function () {};

Check warning on line 72 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected console statement. Only these console methods are allowed: warn, error

Check warning on line 72 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected console statement. Only these console methods are allowed: warn, error

Check warning on line 72 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected console statement. Only these console methods are allowed: warn, error
}

const getWindowLocation = () => {
Expand Down Expand Up @@ -167,7 +167,7 @@
export interface PromptTemplateBase {
litellm_prompt_id: string;
content: string;
metadata?: Record<string, any> | null;

Check warning on line 170 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 170 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 170 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
}

interface PromptInfoResponse {
Expand All @@ -184,7 +184,7 @@
organization_id: string;
organization_alias: string;
budget_id: string;
metadata: Record<string, any>;

Check warning on line 187 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 187 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 187 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
models: string[];
spend: number;
model_spend: Record<string, number>;
Expand All @@ -192,10 +192,10 @@
created_by: string;
updated_at: string;
updated_by: string;
litellm_budget_table: any; // Simplified to any since we don't need the detailed structure

Check warning on line 195 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 195 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 195 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
teams: any[] | null;

Check warning on line 196 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 196 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 196 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
users: any[] | null;

Check warning on line 197 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 197 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 197 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
members: any[] | null;

Check warning on line 198 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 198 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 198 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
object_permission?: {
object_permission_id: string;
mcp_servers: string[];
Expand All @@ -206,7 +206,7 @@

export interface CredentialItem {
credential_name: string;
credential_values: any;

Check warning on line 209 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 209 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 209 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
credential_info: {
custom_llm_provider?: string;
description?: string;
Expand Down Expand Up @@ -287,7 +287,7 @@

let lastErrorTime = 0;

export const handleError = async (errorData: string | any) => {

Check warning on line 290 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 290 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 290 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const currentTime = Date.now();
if (currentTime - lastErrorTime > 60000) {
// 60000 milliseconds = 60 seconds
Expand Down Expand Up @@ -592,7 +592,7 @@

export const budgetCreateCall = async (
accessToken: string,
formValues: Record<string, any>, // Assuming formValues is an object

Check warning on line 595 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 595 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 595 in ui/litellm-dashboard/src/components/networking.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
) => {
try {
const data = await apiClient.post(`/budget/new`, {
Expand Down Expand Up @@ -1929,6 +1929,7 @@
api_key?: string;
team_id?: string;
request_id?: string;
session_id?: string;
user_id?: string;
end_user?: string;
status_filter?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] {
label: "Key Hash",
isSearchable: false,
},
{
name: FILTER_KEYS.SESSION_ID,
label: "Session ID",
isSearchable: false,
},
{
name: "Model",
label: "Model",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ describe("useLogFilterLogic", () => {
{ filterKey: "Team ID", paramName: "team_id", value: "team-a" },
{ filterKey: "Key Hash", paramName: "api_key", value: "key-x" },
{ filterKey: "Request ID", paramName: "request_id", value: "req-xyz" },
{ filterKey: "Session ID", paramName: "session_id", value: "sess-42" },
{ filterKey: "User ID", paramName: "user_id", value: "user-123" },
{ filterKey: "End User", paramName: "end_user", value: "user-a" },
{ filterKey: "Status", paramName: "status_filter", value: "error" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const FILTER_KEYS = {
TEAM_ID: "Team ID",
KEY_HASH: "Key Hash",
REQUEST_ID: "Request ID",
SESSION_ID: "Session ID",
MODEL: "Model",
/** Exact match on LiteLLM_SpendLogs.model — use for search tools and public model names. */
PUBLIC_MODEL_OR_SEARCH_TOOL: "Public model / search tool",
Expand All @@ -49,6 +50,7 @@ const TEXT_FILTER_KEYS: readonly (keyof LogFilterState)[] = [
FILTER_KEYS.KEY_HASH,
FILTER_KEYS.ERROR_MESSAGE,
FILTER_KEYS.REQUEST_ID,
FILTER_KEYS.SESSION_ID,
FILTER_KEYS.USER_ID,
FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL,
];
Expand All @@ -62,6 +64,7 @@ export const defaultFilters: LogFilterState = {
[FILTER_KEYS.TEAM_ID]: "",
[FILTER_KEYS.KEY_HASH]: "",
[FILTER_KEYS.REQUEST_ID]: "",
[FILTER_KEYS.SESSION_ID]: "",
[FILTER_KEYS.MODEL]: "",
[FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL]: "",
[FILTER_KEYS.USER_ID]: "",
Expand Down Expand Up @@ -160,6 +163,7 @@ export function useLogFilterLogic({
api_key: effectiveFilters[FILTER_KEYS.KEY_HASH] || undefined,
team_id: effectiveFilters[FILTER_KEYS.TEAM_ID] || undefined,
request_id: effectiveFilters[FILTER_KEYS.REQUEST_ID] || undefined,
session_id: effectiveFilters[FILTER_KEYS.SESSION_ID] || undefined,
user_id: effectiveFilters[FILTER_KEYS.USER_ID] || (filterByCurrentUser ? userID ?? undefined : undefined),
end_user: effectiveFilters[FILTER_KEYS.END_USER] || undefined,
status_filter: effectiveFilters[FILTER_KEYS.STATUS] || undefined,
Expand Down
4 changes: 4 additions & 0 deletions ui/litellm-dashboard/src/lib/http/schema.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading