Skip to content

Commit 9b720dc

Browse files
authored
Ianmacleod/add model delete (#261)
* starting work on model.delete() * adding server implementation * adding client changes to delete based on id, not name of model * changing use case to be based on name, not id * adding client model changes * adding unit tests * updating tests * . * . * fix * fix * pls work * addressing feedback from review * reverting accidental change to conftest * add new test
1 parent c02f0b4 commit 9b720dc

File tree

5 files changed

+179
-7
lines changed

5 files changed

+179
-7
lines changed

clients/python/llmengine/model.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def list(cls) -> ListLLMEndpointsResponse:
334334
return ListLLMEndpointsResponse.parse_obj(response)
335335

336336
@classmethod
337-
def delete(cls, model: str) -> DeleteLLMEndpointResponse:
337+
def delete(cls, model_endpoint_name: str) -> DeleteLLMEndpointResponse:
338338
"""
339339
Deletes an LLM model.
340340
@@ -345,11 +345,11 @@ def delete(cls, model: str) -> DeleteLLMEndpointResponse:
345345
Engine, an error will be thrown.
346346
347347
Args:
348-
model (`str`):
349-
Name of the model
348+
model_endpoint_name (`str`):
349+
Name of the model endpoint to be deleted
350350
351351
Returns:
352-
response: whether the model was successfully deleted
352+
response: whether the model endpoint was successfully deleted
353353
354354
=== "Deleting model in Python"
355355
```python
@@ -366,7 +366,9 @@ def delete(cls, model: str) -> DeleteLLMEndpointResponse:
366366
}
367367
```
368368
"""
369-
response = cls._delete(f"v1/llm/model-endpoints/{model}", timeout=DEFAULT_TIMEOUT)
369+
response = cls._delete(
370+
f"v1/llm/model-endpoints/{model_endpoint_name}", timeout=DEFAULT_TIMEOUT
371+
)
370372
return DeleteLLMEndpointResponse.parse_obj(response)
371373

372374
@classmethod

model-engine/model_engine_server/api/llms_v1.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
CreateFineTuneResponse,
2222
CreateLLMModelEndpointV1Request,
2323
CreateLLMModelEndpointV1Response,
24+
DeleteLLMEndpointResponse,
2425
GetFineTuneEventsResponse,
2526
GetFineTuneResponse,
2627
GetLLMModelEndpointV1Response,
@@ -40,9 +41,11 @@
4041
)
4142
from model_engine_server.core.loggers import filename_wo_ext, make_logger
4243
from model_engine_server.domain.exceptions import (
44+
EndpointDeleteFailedException,
4345
EndpointLabelsException,
4446
EndpointResourceInvalidRequestException,
4547
EndpointUnsupportedInferenceTypeException,
48+
ExistingEndpointOperationInProgressException,
4649
InvalidRequestException,
4750
LLMFineTuningMethodNotImplementedException,
4851
LLMFineTuningQuotaReached,
@@ -59,6 +62,7 @@
5962
CompletionStreamV1UseCase,
6063
CompletionSyncV1UseCase,
6164
CreateLLMModelEndpointV1UseCase,
65+
DeleteLLMEndpointByNameUseCase,
6266
GetLLMModelEndpointByNameV1UseCase,
6367
ListLLMModelEndpointsV1UseCase,
6468
ModelDownloadV1UseCase,
@@ -384,3 +388,41 @@ async def download_model_endpoint(
384388
status_code=404,
385389
detail="The requested fine-tuned model could not be found.",
386390
) from exc
391+
392+
393+
@llm_router_v1.delete(
394+
"/model-endpoints/{model_endpoint_name}", response_model=DeleteLLMEndpointResponse
395+
)
396+
async def delete_llm_model_endpoint(
397+
model_endpoint_name: str,
398+
auth: User = Depends(verify_authentication),
399+
external_interfaces: ExternalInterfaces = Depends(get_external_interfaces),
400+
) -> DeleteLLMEndpointResponse:
401+
add_trace_resource_name("llm_model_endpoints_delete")
402+
logger.info(f"DELETE /model-endpoints/{model_endpoint_name} for {auth}")
403+
try:
404+
use_case = DeleteLLMEndpointByNameUseCase(
405+
llm_model_endpoint_service=external_interfaces.llm_model_endpoint_service,
406+
model_endpoint_service=external_interfaces.model_endpoint_service,
407+
)
408+
return await use_case.execute(user=auth, model_endpoint_name=model_endpoint_name)
409+
except (ObjectNotFoundException) as exc:
410+
raise HTTPException(
411+
status_code=404,
412+
detail="The requested model endpoint could not be found.",
413+
) from exc
414+
except (ObjectNotAuthorizedException) as exc:
415+
raise HTTPException(
416+
status_code=403,
417+
detail="You don't have permission to delete the requested model endpoint.",
418+
) from exc
419+
except ExistingEndpointOperationInProgressException as exc:
420+
raise HTTPException(
421+
status_code=409,
422+
detail="Existing operation on endpoint in progress, try again later.",
423+
) from exc
424+
except EndpointDeleteFailedException as exc: # pragma: no cover
425+
raise HTTPException(
426+
status_code=500,
427+
detail="deletion of endpoint failed.",
428+
) from exc

model-engine/model_engine_server/common/dtos/llms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,7 @@ class ModelDownloadResponse(BaseModel):
228228
urls: Dict[str, str] = Field(
229229
..., description="Dictionary of (file_name, url) pairs to download the model from."
230230
)
231+
232+
233+
class DeleteLLMEndpointResponse(BaseModel):
234+
deleted: bool

model-engine/model_engine_server/domain/use_cases/llm_model_endpoint_use_cases.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
CompletionSyncV1Response,
2222
CreateLLMModelEndpointV1Request,
2323
CreateLLMModelEndpointV1Response,
24+
DeleteLLMEndpointResponse,
2425
GetLLMModelEndpointV1Response,
2526
ListLLMModelEndpointsV1Response,
2627
ModelDownloadRequest,
@@ -779,8 +780,45 @@ async def execute(self, user: User, model_endpoint_name: str) -> GetLLMModelEndp
779780
return _model_endpoint_entity_to_get_llm_model_endpoint_response(model_endpoint)
780781

781782

782-
class DeleteLLMModelEndpointByIdV1UseCase:
783-
pass
783+
class DeleteLLMEndpointByNameUseCase:
784+
"""
785+
Use case for deleting an LLM Model Endpoint of a given user by endpoint name.
786+
"""
787+
788+
def __init__(
789+
self,
790+
model_endpoint_service: ModelEndpointService,
791+
llm_model_endpoint_service: LLMModelEndpointService,
792+
):
793+
self.model_endpoint_service = model_endpoint_service
794+
self.llm_model_endpoint_service = llm_model_endpoint_service
795+
self.authz_module = LiveAuthorizationModule()
796+
797+
async def execute(self, user: User, model_endpoint_name: str) -> DeleteLLMEndpointResponse:
798+
"""
799+
Runs the use case to delete the LLM endpoint owned by the user with the given name.
800+
801+
Args:
802+
user: The owner of the model endpoint.
803+
model_endpoint_name: The name of the model endpoint.
804+
805+
Returns:
806+
A response object that contains a boolean indicating if deletion was successful.
807+
808+
Raises:
809+
ObjectNotFoundException: If a model endpoint with the given name could not be found.
810+
ObjectNotAuthorizedException: If the owner does not own the model endpoint.
811+
"""
812+
model_endpoints = await self.llm_model_endpoint_service.list_llm_model_endpoints(
813+
owner=user.user_id, name=model_endpoint_name, order_by=None
814+
)
815+
if len(model_endpoints) != 1:
816+
raise ObjectNotFoundException
817+
model_endpoint = model_endpoints[0]
818+
if not self.authz_module.check_access_write_owned_entity(user, model_endpoint.record):
819+
raise ObjectNotAuthorizedException
820+
await self.model_endpoint_service.delete_model_endpoint(model_endpoint.record.id)
821+
return DeleteLLMEndpointResponse(deleted=True)
784822

785823

786824
def deepspeed_result_to_tokens(result: Dict[str, Any]) -> List[TokenOutput]:

model-engine/tests/unit/domain/test_llm_use_cases.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
CompletionStreamV1UseCase,
3636
CompletionSyncV1UseCase,
3737
CreateLLMModelEndpointV1UseCase,
38+
DeleteLLMEndpointByNameUseCase,
3839
GetLLMModelEndpointByNameV1UseCase,
3940
ModelDownloadV1UseCase,
4041
)
@@ -869,3 +870,88 @@ async def test_download_nonexistent_model_raises_not_found(
869870
)
870871
with pytest.raises(ObjectNotFoundException):
871872
await use_case.execute(user=user, request=request)
873+
874+
875+
@pytest.mark.asyncio
876+
async def test_delete_model_success(
877+
fake_model_endpoint_service,
878+
fake_llm_model_endpoint_service,
879+
llm_model_endpoint_sync: Tuple[ModelEndpoint, Any],
880+
test_api_key: str,
881+
):
882+
fake_llm_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
883+
fake_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
884+
use_case = DeleteLLMEndpointByNameUseCase(
885+
model_endpoint_service=fake_model_endpoint_service,
886+
llm_model_endpoint_service=fake_llm_model_endpoint_service,
887+
)
888+
user = User(user_id=test_api_key, team_id=test_api_key, is_privileged_user=True)
889+
response = await use_case.execute(
890+
user=user, model_endpoint_name=llm_model_endpoint_sync[0].record.name
891+
)
892+
remaining_endpoint_model_service = await fake_model_endpoint_service.get_model_endpoint(
893+
llm_model_endpoint_sync[0].record.id
894+
)
895+
assert remaining_endpoint_model_service is None
896+
assert response.deleted is True
897+
898+
899+
@pytest.mark.asyncio
900+
async def test_delete_nonexistent_model_raises_not_found(
901+
fake_model_endpoint_service,
902+
fake_llm_model_endpoint_service,
903+
llm_model_endpoint_sync: Tuple[ModelEndpoint, Any],
904+
test_api_key: str,
905+
):
906+
fake_llm_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
907+
fake_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
908+
use_case = DeleteLLMEndpointByNameUseCase(
909+
model_endpoint_service=fake_model_endpoint_service,
910+
llm_model_endpoint_service=fake_llm_model_endpoint_service,
911+
)
912+
user = User(user_id=test_api_key, team_id=test_api_key, is_privileged_user=True)
913+
with pytest.raises(ObjectNotFoundException):
914+
await use_case.execute(user=user, model_endpoint_name="nonexistent-model")
915+
916+
917+
@pytest.mark.asyncio
918+
async def test_delete_unauthorized_model_raises_not_authorized(
919+
fake_model_endpoint_service,
920+
fake_llm_model_endpoint_service,
921+
llm_model_endpoint_sync: Tuple[ModelEndpoint, Any],
922+
):
923+
fake_llm_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
924+
fake_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
925+
use_case = DeleteLLMEndpointByNameUseCase(
926+
model_endpoint_service=fake_model_endpoint_service,
927+
llm_model_endpoint_service=fake_llm_model_endpoint_service,
928+
)
929+
user = User(user_id="fakeapikey", team_id="fakeapikey", is_privileged_user=True)
930+
with pytest.raises(ObjectNotAuthorizedException):
931+
await use_case.execute(
932+
user=user, model_endpoint_name=llm_model_endpoint_sync[0].record.name
933+
)
934+
935+
936+
@pytest.mark.asyncio
937+
async def test_delete_public_inference_model_raises_not_authorized(
938+
fake_model_endpoint_service,
939+
fake_llm_model_endpoint_service,
940+
llm_model_endpoint_sync: Tuple[ModelEndpoint, Any],
941+
test_api_key,
942+
):
943+
fake_llm_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
944+
fake_model_endpoint_service.add_model_endpoint(llm_model_endpoint_sync[0])
945+
use_case = DeleteLLMEndpointByNameUseCase(
946+
model_endpoint_service=fake_model_endpoint_service,
947+
llm_model_endpoint_service=fake_llm_model_endpoint_service,
948+
)
949+
user = User(
950+
user_id="fakeapikey", team_id="faketeam", is_privileged_user=True
951+
) # write access is based on team_id, so team_id != owner's team_id
952+
with pytest.raises(
953+
ObjectNotAuthorizedException
954+
): # user cannot delete public inference model they don't own
955+
await use_case.execute(
956+
user=user, model_endpoint_name=llm_model_endpoint_sync[0].record.name
957+
)

0 commit comments

Comments
 (0)