BerriAI/litellm · error · HTTPException

Invalid sort order. Must be 'asc' or 'desc'

Error message

Invalid sort order. Must be 'asc' or 'desc'

What it means

Raised by the LiteLLM proxy key-management endpoints when a list request supplies a sort_order value that is not 'asc' or 'desc'. The helper that builds the Prisma order_by clause validates the column first (sort_by) and then lower-cases sort_order and checks membership in ['asc','desc']; anything else gets an HTTP 400 before any query runs. It exists to prevent arbitrary strings from reaching the database layer.

Solutions

  1. Send exactly 'asc' or 'desc' (any letter casing) for sort_order, e.g. /key/list?sort_by=token&sort_order=desc
  2. Fix the UI/SDK mapping so 'ascending'->'asc' and 'descending'->'desc' before the request leaves the client
  3. URL-encode the query string properly and confirm no stray whitespace or newlines are appended
  4. If the 400 mentions the column instead, also verify sort_by is one of the valid columns listed in the error detail (includes token, key_alias, updated_at, etc.)

Example fix

# before
curl 'http://localhost:4000/key/list?sort_by=token&sort_order=ascending'
# after
curl 'http://localhost:4000/key/list?sort_by=token&sort_order=desc'   # 'ASC'/'Desc' also OK; server lower-cases
Defensive patterns

Strategy: validation

Validate before calling

SORT_ORDERS = ('asc', 'desc')

def normalize_list_params(params: dict) -> dict:
    if 'sort_order' in params:
        so = str(params['sort_order']).strip().lower()
        if so not in SORT_ORDERS:
            raise ValueError(f"sort_order must be one of {SORT_ORDERS}, got {params['sort_order']!r}")
        params['sort_order'] = so
    return params

Type guard

def is_valid_sort_order(value: object) -> bool:
    return isinstance(value, str) and value.strip().lower() in ('asc', 'desc')

Try / catch

try:
    r = await client.get('/key/list', params=normalize_list_params(q))
    r.raise_for_status()
except httpx.HTTPStatusError as e:
    if e.response.status_code == 400 and 'sort order' in e.response.text.lower():
        q['sort_order'] = 'desc'          # correct and retry once
        r = await client.get('/key/list', params=q)
    else:
        raise

Prevention

When it happens

Trigger: Calling GET /key/list (or any key-listing route that accepts sort_by/sort_order) with e.g. sort_order=ascending, sort_order=ASCENDING, sort_order='' (empty), sort_order='DESC ' (trailing whitespace), or a URL-mangled value like sort_order=desc%0A. Case-insensitive: 'ASC'/'Desc' pass because the check applies .lower() first.

Common situations: Frontend dropdowns that send 'ascending'/'descending' instead of 'asc'/'desc'; copy-pasted query strings from other APIs (e.g. Grafana-style 'ASC' is fine but '+desc' from unencoded spaces is not); typos like 'ascc' or 'dsc'; SDKs defaulting to 'descending' when no explicit value is set.

Understand the failure class

Background: "Invalid query parameter" / "Failed to parse value of ...": fixing bad query string parameters across APIs — this error's family across 36 libraries.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/2f7f07fa47f8bdb7. Report an issue: GitHub.

Appendix: source

Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:5795

        return None
    # Validate sort_by is a valid column
    valid_columns: Final = [
        "spend",
        "max_budget",
        "created_at",
        "updated_at",
        "token",
        "key_alias",
    ]
    if sort_by not in valid_columns:
        raise HTTPException(
            status_code=400,
            detail={"error": f"Invalid sort column. Must be one of: {', '.join(valid_columns)}"},
        )

    # Validate sort_order
    if sort_order.lower() not in ["asc", "desc"]:
        raise HTTPException(
            status_code=400,
            detail={"error": "Invalid sort order. Must be 'asc' or 'desc'"},
        )

    order_by[sort_by] = sort_order.lower()

    return order_by


def _build_expires_where_clause(expires_filter: str, now: datetime) -> dict[str, object]:
    if expires_filter == "expired":
        return {"AND": [{"expires": {"not": None}}, {"expires": {"lt": now}}]}
    return {"OR": [{"expires": None}, {"expires": {"gte": now}}]}


def _build_key_filter_conditions(
    user_id: str | None,
    team_id: str | None,

View on GitHub (pinned to 77b7c6c40c)