BerriAI/litellm · error · HTTPException

User not found. Only PROXY_ADMIN can create users via /user/

Error message

User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead.

What it means

POST /user/update is update-only for non-admins: if no existing row matches the supplied user_id/user_email and the caller is not proxy_admin, LiteLLM returns 404 'User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead.' This silent-create guard stops non-admins from creating rows through the update path; proxy admins continue into the update flow instead.

Source

Thrown at litellm/proxy/management_endpoints/internal_user_endpoints.py:1274

    user_api_key_dict: UserAPIKeyAuth,
    existing_user_row: BaseModel | None,
) -> None:
    """Authorization checks for /user/update — raises HTTPException on failure."""
    if user_request.user_role is not None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
        raise HTTPException(status_code=403, detail="Only proxy admins can modify user roles.")

    if existing_user_row is not None:
        typed_row: Final = LiteLLM_UserTable.model_validate(existing_user_row.model_dump(exclude_none=True))
        if not can_user_call_user_update(user_api_key_dict=user_api_key_dict, user_info=typed_row):
            raise HTTPException(
                status_code=403,
                detail={
                    "error": "User does not have permission to update this user. Only PROXY_ADMIN can update other users."
                },
            )
    elif user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
        # Silent-create guard: only PROXY_ADMIN may create via /user/update.
        raise HTTPException(
            status_code=404,
            detail={
                "error": "User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead."
            },
        )


async def _invalidate_user_spend_counter_if_changed(
    non_default_values: Mapping[str, object],
) -> None:
    """Invalidate the cross-pod spend counter after a direct ``spend`` change.

    A direct ``spend`` change must also invalidate the cross-pod spend counter
    enforcement reads; the DB write alone leaves a warm counter at the stale
    value. ``non_default_values["user_id"]`` is populated in every branch of the
    caller (incl. the email-new-user insert path, whose response is a bare model
    and not safely subscriptable).
    """

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Create the user first with POST /user/new, then apply the update
  2. Fix the user_id/user_email value (verify against GET /user/list)
  3. Use a proxy_admin key if the admin update path (which allows more) is genuinely intended

Example fix

# before
POST /user/update -H 'Authorization: Bearer sk-user' {"user_id": "u-typo", "user_alias": "x"}  # 404

# after
POST /user/new   -H 'Authorization: Bearer sk-user' {"user_id": "u-typo"}
POST /user/update -H 'Authorization: Bearer sk-user' {"user_id": "u-typo", "user_alias": "x"}  # 200
Defensive patterns

Strategy: validation

Validate before calling

import requests

def ensure_exists_or_create(base_url: str, headers: dict, user_id: str) -> None:
    r = requests.get(f"{base_url}/user/list", headers=headers, timeout=10)
    r.raise_for_status()
    if not any(u.get("user_id") == user_id for u in r.json().get("data", [])):
        requests.post(f"{base_url}/user/new", json={"user_id": user_id}, headers=headers, timeout=10).raise_for_status()

Try / catch

except requests.HTTPError as e:
    if e.response is not None and e.response.status_code == 404 and "use /user/new" in e.response.text:
        requests.post(f"{BASE}/user/new", json={"user_id": payload["user_id"]}, headers=hdrs).raise_for_status()
        return retry_update(payload)
    raise

Prevention

When it happens

Trigger: Non-admin POST /user/update for a user_id or user_email that has no row in LiteLLM_UserTable - a typo, a deleted user, or an attempt to create via update.

Common situations: Upsert-style client code that assumed /user/update creates missing users; typo'd emails; the target user deleted by a cleanup job while an edit was in flight.

Understand the failure class

Background: "User not found", "Invalid user", and "does not exist": what missing-user lookup errors mean across Rocket.Chat, LiteLLM, Phabricator, rustfs, and pnpm — this error's family across 10 libraries.

Related errors


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