Skip to content

Commit 23b7e53

Browse files
committed
implemented model handling endpoints
1 parent 9c5125d commit 23b7e53

File tree

5 files changed

+409
-16
lines changed

5 files changed

+409
-16
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ requires-python = ">=3.11"
1111
dependencies = [
1212
"aiodocker>=0.24.0",
1313
"fastapi>=0.115.12",
14+
"huggingface-hub>=0.30.1",
1415
"pydantic>=2.11.2",
1516
"pydantic-settings>=2.8.1",
1617
"uvicorn>=0.34.0",
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
from fastapi import APIRouter
2-
from .models import GetModelsResponse, PutModelRequest, DeleteModelRequest, PutModelResponse
3-
from inferadmin.routes.standard_models import BasicResponse
2+
from .models import GetModelsResponse, PutModelRequest, DeleteModelRequest
3+
4+
from .support import scan_hf_models_directory, delete_model, download_hf_model
45

56
router = APIRouter(
67
prefix='/models'
78
)
89

910
@router.get('/')
1011
async def get_models() -> GetModelsResponse:
11-
pass
12+
models = scan_hf_models_directory()
13+
return {'models': models}
1214

1315
@router.put('/')
14-
async def put_models(data: PutModelRequest) -> PutModelResponse:
15-
pass
16+
async def put_models(data: PutModelRequest):
17+
repo_id = data.repo_id
18+
source = data.source
19+
if source == "Huggingface":
20+
download_hf_model(repo_id)
1621

1722
@router.delete('/')
18-
async def delete_models(data: DeleteModelRequest) -> BasicResponse:
19-
pass
23+
async def delete_models(data: DeleteModelRequest):
24+
repo_id = data.repo_id
25+
delete_model(repo_id)
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
from typing import Literal
22
from pydantic import BaseModel
33
from datetime import datetime
4-
from inferadmin.routes.standard_models import BasicResponse
54

65
class Model(BaseModel):
7-
name: str
8-
id: str
9-
is_local: bool
6+
repo_id: str
7+
path: str
8+
size_gb: float
109
last_updated: datetime | None
1110

1211
class GetModelsResponse(BaseModel):
1312
models: list[Model]
1413

1514
class PutModelRequest(BaseModel):
16-
name: str
15+
repo_id: str
1716
source: Literal["Huggingface"]
1817

19-
class PutModelResponse(BasicResponse):
20-
streaming: bool
21-
2218
class DeleteModelRequest(BaseModel):
23-
name: str
19+
repo_id: str
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
2+
import os
3+
from pathlib import Path
4+
from huggingface_hub import HfApi
5+
from huggingface_hub.utils import RepositoryNotFoundError
6+
from datetime import datetime
7+
import time
8+
import shutil
9+
from fastapi import HTTPException
10+
11+
from inferadmin.config.loader import config_manager
12+
13+
def check_hf_model_exists(model_path):
14+
"""
15+
Check if a complete Hugging Face model exists at the given path.
16+
17+
Returns:
18+
bool: True if valid model, False otherwise
19+
"""
20+
path = Path(model_path)
21+
22+
# Check if the directory exists
23+
if not path.exists() or not path.is_dir():
24+
return False
25+
# Check if the directory is empty
26+
if not any(path.iterdir()):
27+
return False
28+
29+
# Required files to check
30+
required_files = [
31+
"*.safetensors", # At least one safetensors file
32+
"generation_config.json",
33+
"config.json",
34+
"tokenizer.json",
35+
"special_tokens_map.json",
36+
"tokenizer_config.json",
37+
"vocab.json"
38+
]
39+
40+
# Check each required file
41+
for file_pattern in required_files:
42+
if "*" in file_pattern:
43+
# Handle wildcard pattern (for safetensors)
44+
if not list(path.glob(file_pattern)):
45+
return False
46+
else:
47+
# Handle exact filename
48+
if not (path / file_pattern).exists():
49+
return False
50+
51+
return True
52+
53+
def get_folder_size_gb(folder_path):
54+
"""
55+
Calculate the total size of all files in a folder and its subfolders in GB.
56+
57+
Args:
58+
folder_path (str): Path to the folder
59+
60+
Returns:
61+
float: Size in gigabytes
62+
"""
63+
path = Path(folder_path)
64+
65+
if not path.exists() or not path.is_dir():
66+
return 0.0
67+
68+
total_size = 0
69+
70+
# Walk through all files in the directory and subdirectories
71+
for dirpath, dirnames, filenames in os.walk(path):
72+
for filename in filenames:
73+
file_path = os.path.join(dirpath, filename)
74+
# Skip if it's a symlink
75+
if not os.path.islink(file_path):
76+
total_size += os.path.getsize(file_path)
77+
78+
# Convert bytes to GB
79+
size_gb = total_size / (1024 ** 3)
80+
81+
return round(size_gb, 2)
82+
83+
def get_most_recent_modified_date(folder_path):
84+
"""
85+
Get the most recent modification date of any file in the directory.
86+
87+
Args:
88+
folder_path (str): Path to the folder
89+
90+
Returns:
91+
datetime
92+
"""
93+
most_recent_time = 0
94+
95+
# Walk through all files in the directory and subdirectories
96+
for dirpath, dirnames, filenames in os.walk(folder_path):
97+
for filename in filenames:
98+
file_path = os.path.join(dirpath, filename)
99+
if os.path.isfile(file_path):
100+
file_mtime = os.path.getmtime(file_path)
101+
if file_mtime > most_recent_time:
102+
most_recent_time = file_mtime
103+
104+
# If no files were found, return current time
105+
if most_recent_time == 0:
106+
most_recent_time = time.time()
107+
108+
# Convert to datetime
109+
modified_date = datetime.fromtimestamp(most_recent_time)
110+
111+
return modified_date
112+
113+
def scan_hf_models_directory():
114+
"""
115+
Scan a directory for Hugging Face models and collect info about each valid model.
116+
117+
Returns:
118+
list: List of dictionaries with model info (repo_id, path, size_gb, last_updated)
119+
"""
120+
base_path = Path(config_manager.get_config().model_storage_path)
121+
122+
if not base_path.exists() or not base_path.is_dir():
123+
return []
124+
125+
model_info_list = []
126+
127+
# Get all subdirectories in the base path
128+
subdirs = [d for d in base_path.iterdir() if d.is_dir()]
129+
130+
for folder in subdirs:
131+
# Get folder name
132+
folder_name = folder.name
133+
folder_path = str(folder)
134+
135+
# Check if it's a valid HF model
136+
is_valid_model = check_hf_model_exists(folder_path)
137+
138+
# Get folder size
139+
size_gb = get_folder_size_gb(folder_path)
140+
141+
# Get the most recent modification date of any file in the directory
142+
last_updated = get_most_recent_modified_date(folder_path)
143+
144+
# Create model info dictionary
145+
146+
model_info = {
147+
"repo_id": folder_name.replace('_', '/'),
148+
"path": folder_path,
149+
"size_gb": size_gb,
150+
"last_updated": last_updated
151+
}
152+
153+
if is_valid_model:
154+
model_info_list.append(model_info)
155+
156+
return model_info_list
157+
158+
def delete_model(repo_id:str):
159+
"""
160+
Delete a model from the storage path and all its contents.
161+
162+
Args:
163+
model_name (str): hf_model name
164+
"""
165+
folder_name = repo_id.replace("/", "_")
166+
folder_path = f"{config_manager.get_config().model_storage_path}/{folder_name}"
167+
168+
path = Path(folder_path)
169+
170+
# Check if the path exists and is a directory
171+
if not path.exists():
172+
raise HTTPException(
173+
status_code=404,
174+
detail=f"Path does not exist: {folder_path}"
175+
)
176+
177+
if not path.is_dir():
178+
raise HTTPException(
179+
status_code=404,
180+
detail=f"Path is not a directory: {folder_path}"
181+
)
182+
183+
try:
184+
# Remove the directory and all its contents
185+
shutil.rmtree(folder_path)
186+
except Exception as e:
187+
raise HTTPException(
188+
status_code=500,
189+
detail=f"Error deleting model: {str(e)}"
190+
)
191+
192+
def download_hf_model(repo_id:str):
193+
"""
194+
Downloads the given model to the local directory from huggingface.
195+
196+
Args:
197+
repo_id (str): the HF repo name of thing to download
198+
"""
199+
hf = HfApi(
200+
token=config_manager.get_config().hf_token
201+
)
202+
203+
base_path = config_manager.get_config().model_storage_path
204+
file_name = repo_id.replace('/', '_')
205+
target_path = f"{base_path}/{file_name}"
206+
207+
try:
208+
hf.snapshot_download(
209+
repo_id=repo_id,
210+
repo_type='model',
211+
local_dir=target_path
212+
)
213+
214+
except RepositoryNotFoundError as e:
215+
raise HTTPException(
216+
status_code=404,
217+
detail=f"HuggingFace Repository not found: {repo_id}"
218+
)
219+
220+
except Exception as e:
221+
raise HTTPException(
222+
status_code=500,
223+
detail=f"Error downloading model: {str(e)}"
224+
)

0 commit comments

Comments
 (0)