{"record":{"id":"ca10a024281f8c1c","repo":"BerriAI/litellm","slug":"user-not-found-only-proxy-admin-can-create-users","errorCode":null,"errorMessage":"User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead.","messagePattern":"User not found\\. Only PROXY_ADMIN can create users via /user/update; use /user/new instead\\.","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"litellm/proxy/management_endpoints/internal_user_endpoints.py","lineNumber":1274,"sourceCode":"    user_api_key_dict: UserAPIKeyAuth,\n    existing_user_row: BaseModel | None,\n) -> None:\n    \"\"\"Authorization checks for /user/update — raises HTTPException on failure.\"\"\"\n    if user_request.user_role is not None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:\n        raise HTTPException(status_code=403, detail=\"Only proxy admins can modify user roles.\")\n\n    if existing_user_row is not None:\n        typed_row: Final = LiteLLM_UserTable.model_validate(existing_user_row.model_dump(exclude_none=True))\n        if not can_user_call_user_update(user_api_key_dict=user_api_key_dict, user_info=typed_row):\n            raise HTTPException(\n                status_code=403,\n                detail={\n                    \"error\": \"User does not have permission to update this user. Only PROXY_ADMIN can update other users.\"\n                },\n            )\n    elif user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:\n        # Silent-create guard: only PROXY_ADMIN may create via /user/update.\n        raise HTTPException(\n            status_code=404,\n            detail={\n                \"error\": \"User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead.\"\n            },\n        )\n\n\nasync def _invalidate_user_spend_counter_if_changed(\n    non_default_values: Mapping[str, object],\n) -> None:\n    \"\"\"Invalidate the cross-pod spend counter after a direct ``spend`` change.\n\n    A direct ``spend`` change must also invalidate the cross-pod spend counter\n    enforcement reads; the DB write alone leaves a warm counter at the stale\n    value. ``non_default_values[\"user_id\"]`` is populated in every branch of the\n    caller (incl. the email-new-user insert path, whose response is a bare model\n    and not safely subscriptable).\n    \"\"\"","sourceCodeStart":1256,"sourceCodeEnd":1292,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/internal_user_endpoints.py#L1256-L1292","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Create the user first with POST /user/new, then apply the update","Fix the user_id/user_email value (verify against GET /user/list)","Use a proxy_admin key if the admin update path (which allows more) is genuinely intended"],"exampleFix":"# before\nPOST /user/update -H 'Authorization: Bearer sk-user' {\"user_id\": \"u-typo\", \"user_alias\": \"x\"}  # 404\n\n# after\nPOST /user/new   -H 'Authorization: Bearer sk-user' {\"user_id\": \"u-typo\"}\nPOST /user/update -H 'Authorization: Bearer sk-user' {\"user_id\": \"u-typo\", \"user_alias\": \"x\"}  # 200","handlingStrategy":"validation","validationCode":"import requests\n\ndef ensure_exists_or_create(base_url: str, headers: dict, user_id: str) -> None:\n    r = requests.get(f\"{base_url}/user/list\", headers=headers, timeout=10)\n    r.raise_for_status()\n    if not any(u.get(\"user_id\") == user_id for u in r.json().get(\"data\", [])):\n        requests.post(f\"{base_url}/user/new\", json={\"user_id\": user_id}, headers=headers, timeout=10).raise_for_status()","typeGuard":null,"tryCatchPattern":"except requests.HTTPError as e:\n    if e.response is not None and e.response.status_code == 404 and \"use /user/new\" in e.response.text:\n        requests.post(f\"{BASE}/user/new\", json={\"user_id\": payload[\"user_id\"]}, headers=hdrs).raise_for_status()\n        return retry_update(payload)\n    raise","preventionTips":["Treat /user/update as update-only: create with /user/new first","Validate ids against /user/list before editing","Avoid upsert assumptions in client code - only PROXY_ADMIN can implicitly upsert"],"tags":["litellm","user-management","not-found","upsert"],"backgroundTag":"user-not-found","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}