mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(spend-logs): match short error codes on the indexed prefix alone
A code under 128 characters is its own prefix, so the prefix comparison is already exact and Postgres no longer reads each matching row's metadata to recheck the full code. Codes of 128 characters or more keep the full-code check. The integration test now seeds long codes that share a prefix, and error_code is declared with Annotated so direct calls default to None.
This commit is contained in:
parent
f4d31a5401
commit
29691d6ddd
3 changed files with 67 additions and 6 deletions
|
|
@ -96,6 +96,9 @@ _SESSION_REPRESENTATIVE_ORDER_SQL: Final = (
|
|||
f'call_type IN {_MCP_CALL_TYPES_SQL}, "startTime" DESC, request_id'
|
||||
)
|
||||
_BATCH_CALL_TYPES_SQL: Final = "('acreate_batch', 'create_batch', 'aretrieve_batch', 'retrieve_batch')"
|
||||
_ERROR_CODE_SQL: Final = "metadata->'error_information'->>'error_code'"
|
||||
_INDEXED_ERROR_CODE_PREFIX_LENGTH: Final = 128
|
||||
_INDEXED_ERROR_CODE_PREFIX_SQL: Final = f"left({_ERROR_CODE_SQL}, {_INDEXED_ERROR_CODE_PREFIX_LENGTH})"
|
||||
_SPAN_TYPE_SQL_CONDITIONS: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
"mcp": f"call_type IN {_MCP_CALL_TYPES_SQL}",
|
||||
|
|
@ -226,6 +229,15 @@ class _SessionSpendRow(TypedDict):
|
|||
session_models: ReadOnly[Sequence[str]]
|
||||
|
||||
|
||||
def _error_code_condition(placeholder: str, error_code: str) -> str:
|
||||
if len(error_code) < _INDEXED_ERROR_CODE_PREFIX_LENGTH:
|
||||
return f"{_INDEXED_ERROR_CODE_PREFIX_SQL} = {placeholder}"
|
||||
return (
|
||||
f"{_INDEXED_ERROR_CODE_PREFIX_SQL} = left({placeholder}, {_INDEXED_ERROR_CODE_PREFIX_LENGTH}) "
|
||||
f"AND {_ERROR_CODE_SQL} = {placeholder}"
|
||||
)
|
||||
|
||||
|
||||
_SESSION_MODELS_LIMIT: Final = 10
|
||||
_SESSION_MODEL_NAME_MAX_LEN: Final = 256
|
||||
|
||||
|
|
@ -2577,7 +2589,9 @@ async def ui_view_spend_logs(
|
|||
model_group: str | None = fastapi.Query(default=None, description="Filter logs by model group"),
|
||||
key_alias: str | None = fastapi.Query(default=None, description="Filter logs by key alias"),
|
||||
end_user: str | None = fastapi.Query(default=None, description="Filter logs by end user"),
|
||||
error_code: str | None = fastapi.Query(default=None, description="Filter logs by error code (e.g., '404', '500')"),
|
||||
error_code: Annotated[
|
||||
str | None, fastapi.Query(description="Filter logs by error code (e.g., '404', '500')")
|
||||
] = None,
|
||||
error_message: str | None = fastapi.Query(
|
||||
default=None, description="Filter logs by error message (partial string match)"
|
||||
),
|
||||
|
|
@ -2933,10 +2947,7 @@ async def ui_view_spend_logs(
|
|||
sql_params.append(f"%{key_alias}%")
|
||||
p += 1
|
||||
if error_code is not None:
|
||||
sql_conditions.append(
|
||||
f"left(metadata->'error_information'->>'error_code', 128) = left(${p}, 128) "
|
||||
f"AND metadata->'error_information'->>'error_code' = ${p}"
|
||||
)
|
||||
sql_conditions.append(_error_code_condition(f"${p}", error_code))
|
||||
sql_params.append(error_code)
|
||||
p += 1
|
||||
if error_message is not None:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import secrets
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -22,6 +23,10 @@ INDEXES_IN_PLACE: Final = "Request-log indexes are all in place"
|
|||
SUCCESS_ROWS: Final = 5000
|
||||
STATS_FLUSH_WINDOW_SECONDS: Final = 11.0
|
||||
|
||||
LONG_CODE_PREFIX: Final = secrets.token_hex(64)
|
||||
LONG_CODE: Final = LONG_CODE_PREFIX + secrets.token_hex(1500)
|
||||
LONG_CODE_SIBLING: Final = LONG_CODE_PREFIX + secrets.token_hex(1500)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ErrorRow:
|
||||
|
|
@ -41,11 +46,16 @@ ERROR_ROWS: Final = (
|
|||
ErrorRow("failed-500-timeout", "failure", "500", "upstream timed out"),
|
||||
ErrorRow("failed-429", "failure", "429", "rate limited by upstream"),
|
||||
ErrorRow("client-disconnected", "success", "499", "Client disconnected the request"),
|
||||
ErrorRow("failed-long-code", "failure", LONG_CODE, "provider error with a long code"),
|
||||
ErrorRow("failed-long-code-sibling", "failure", LONG_CODE_SIBLING, "provider error with a long code"),
|
||||
)
|
||||
ERROR_CODES: Final = MappingProxyType(
|
||||
{
|
||||
"500": frozenset({"failed-500-overloaded", "failed-500-timeout"}),
|
||||
"499": frozenset({"client-disconnected"}),
|
||||
LONG_CODE: frozenset({"failed-long-code"}),
|
||||
LONG_CODE_PREFIX: frozenset(),
|
||||
LONG_CODE_PREFIX[:-1]: frozenset(),
|
||||
}
|
||||
)
|
||||
SESSION_VIEWS: Final = ("false", "true")
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ def _reconstruct_ui_where_from_sql(sql_query, params):
|
|||
gte = re.search(r'"startTime" >= \(\$(\d+)', cond)
|
||||
lte = re.search(r'"startTime" <= \(\$(\d+)', cond)
|
||||
alias = re.search(r"user_api_key_alias' LIKE \$(\d+)", cond)
|
||||
code = re.search(r"error_code' = \$(\d+)", cond)
|
||||
code = re.search(r"error_code'(?:, 128\))? = \$(\d+)", cond)
|
||||
msg = re.search(r"error_message' LIKE \$(\d+)", cond)
|
||||
credential = re.fullmatch(r"metadata->>'used_client_oauth_token' = \$(\d+)", cond)
|
||||
sess = re.fullmatch(r"session_id LIKE \$(\d+)", cond)
|
||||
|
|
@ -4770,6 +4770,46 @@ async def test_ui_view_spend_logs_with_error_code(client):
|
|||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_a_long_error_code_returns_only_that_exact_code(client):
|
||||
prefix = "E" * 128
|
||||
mock_spend_logs = [
|
||||
{
|
||||
"id": f"log-{suffix}",
|
||||
"request_id": f"req-{suffix}",
|
||||
"api_key": "sk-test-key",
|
||||
"spend": 0.05,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-4",
|
||||
"metadata": json.dumps({"error_information": {"error_code": prefix + suffix}}),
|
||||
}
|
||||
for suffix in ("first", "second")
|
||||
]
|
||||
|
||||
def filter_by_exact_error_code(where):
|
||||
wanted = str(where.get("metadata", {}).get("equals", "")).strip('"')
|
||||
return [log for log in mock_spend_logs if json.loads(log["metadata"])["error_information"]["error_code"] == wanted]
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user"
|
||||
)
|
||||
try:
|
||||
with patch.object(
|
||||
ps, "prisma_client", make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_exact_error_code)
|
||||
):
|
||||
start_date, end_date = _default_date_range()
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={"error_code": prefix + "first", "start_date": start_date, "end_date": end_date},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert [row["id"] for row in response.json()["data"]] == ["log-first"]
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_error_message(client):
|
||||
"""Test filtering spend logs by error message"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue