Skip to content

Commit abd38ca

Browse files
committed
refactor: enhance private AI key creation with error handling and cleanup
- Implemented error handling during the creation of private AI keys to ensure proper cleanup of resources (LiteLLM token and vector database) in case of failures. - Added logging for cleanup operations and preserved original exceptions for better debugging. - Updated tests to cover various failure scenarios, ensuring that resources are cleaned up correctly and appropriate error messages are returned to the user.
1 parent 9deda26 commit abd38ca

File tree

2 files changed

+270
-31
lines changed

2 files changed

+270
-31
lines changed

app/api/private_ai_keys.py

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -191,38 +191,89 @@ async def create_private_ai_key(
191191
Note: You must be authenticated to use this endpoint.
192192
Only admins can create keys for other users or teams.
193193
"""
194-
# First create the LiteLLM token
195-
llm_token = await create_llm_token(private_ai_key, current_user, user_role, db, store_result=False)
196-
197-
# Then create the vector database
198-
vector_db = VectorDBCreate(
199-
region_id=private_ai_key.region_id,
200-
name=private_ai_key.name,
201-
owner_id=private_ai_key.owner_id,
202-
team_id=private_ai_key.team_id
203-
)
204-
db_info = await create_vector_db(vector_db, current_user, user_role, db, store_result=False)
205-
206-
# Store private AI key info in main application database
207-
new_key = DBPrivateAIKey(
208-
database_name=db_info.database_name,
209-
name=db_info.name,
210-
database_host=db_info.database_host,
211-
database_username=db_info.database_username,
212-
database_password=db_info.database_password,
213-
litellm_token=llm_token.litellm_token,
214-
litellm_api_url=llm_token.litellm_api_url,
215-
owner_id=db_info.owner_id,
216-
team_id=db_info.team_id,
217-
region_id=private_ai_key.region_id
218-
)
219-
db.add(new_key)
220-
db.commit()
221-
db.refresh(new_key)
194+
llm_token = None
195+
db_info = None
196+
region = None
222197

223-
key_data = new_key.to_dict()
198+
try:
199+
# Get the region first for cleanup purposes
200+
region = db.query(DBRegion).filter(
201+
DBRegion.id == private_ai_key.region_id,
202+
DBRegion.is_active == True
203+
).first()
204+
if not region:
205+
raise HTTPException(
206+
status_code=status.HTTP_404_NOT_FOUND,
207+
detail="Region not found or inactive"
208+
)
209+
210+
# First create the LiteLLM token
211+
llm_token = await create_llm_token(private_ai_key, current_user, user_role, db, store_result=False)
224212

225-
return PrivateAIKey.model_validate(key_data)
213+
# Then create the vector database
214+
vector_db = VectorDBCreate(
215+
region_id=private_ai_key.region_id,
216+
name=private_ai_key.name,
217+
owner_id=private_ai_key.owner_id,
218+
team_id=private_ai_key.team_id
219+
)
220+
db_info = await create_vector_db(vector_db, current_user, user_role, db, store_result=False)
221+
222+
# Store private AI key info in main application database
223+
new_key = DBPrivateAIKey(
224+
database_name=db_info.database_name,
225+
name=db_info.name,
226+
database_host=db_info.database_host,
227+
database_username=db_info.database_username,
228+
database_password=db_info.database_password,
229+
litellm_token=llm_token.litellm_token,
230+
litellm_api_url=llm_token.litellm_api_url,
231+
owner_id=db_info.owner_id,
232+
team_id=db_info.team_id,
233+
region_id=private_ai_key.region_id
234+
)
235+
db.add(new_key)
236+
db.commit()
237+
db.refresh(new_key)
238+
239+
key_data = new_key.to_dict()
240+
241+
return PrivateAIKey.model_validate(key_data)
242+
243+
except Exception as e:
244+
logger.error(f"Failed to create private AI key: {str(e)}", exc_info=True)
245+
db.rollback()
246+
247+
# Cleanup resources on failure
248+
try:
249+
if llm_token and region:
250+
# Delete LiteLLM token
251+
litellm_service = LiteLLMService(
252+
api_url=region.litellm_api_url,
253+
api_key=region.litellm_api_key
254+
)
255+
await litellm_service.delete_key(llm_token.litellm_token)
256+
logger.info(f"Cleaned up LiteLLM token after failure")
257+
except Exception as cleanup_error:
258+
logger.error(f"Failed to cleanup LiteLLM token: {str(cleanup_error)}", exc_info=True)
259+
260+
try:
261+
if db_info and region:
262+
# Delete vector database
263+
postgres_manager = PostgresManager(region=region)
264+
await postgres_manager.delete_database(db_info.database_name)
265+
logger.info(f"Cleaned up vector database after failure")
266+
except Exception as cleanup_error:
267+
logger.error(f"Failed to cleanup vector database: {str(cleanup_error)}", exc_info=True)
268+
269+
# Re-raise the original exception
270+
if isinstance(e, HTTPException):
271+
raise e
272+
else:
273+
raise HTTPException(
274+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
275+
detail=f"Failed to create private AI key: {str(e)}"
276+
)
226277

227278
@router.post("/token", response_model=LiteLLMToken)
228279
async def create_llm_token(

tests/test_private_ai.py

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import pytest
2-
from unittest.mock import patch
2+
from unittest.mock import patch, Mock
33
from app.db.models import DBPrivateAIKey, DBTeam, DBUser, DBProduct, DBTeamProduct
44
from datetime import datetime, UTC
55
from app.core.security import get_password_hash
66
from requests.exceptions import HTTPError
7+
from fastapi import status, HTTPException
78
from app.core.resource_limits import (
89
DEFAULT_MAX_SPEND,
910
DEFAULT_RPM_PER_KEY,
@@ -1905,3 +1906,190 @@ def test_delete_private_ai_key_litellm_service_unavailable(mock_post, client, ad
19051906
json={"keys": [test_key.litellm_token]}
19061907
)
19071908
db.commit()
1909+
1910+
@patch("app.services.litellm.requests.post")
1911+
@patch("app.db.postgres.PostgresManager.create_database")
1912+
def test_create_private_ai_key_cleanup_on_vector_db_failure(mock_create_db, mock_post, client, test_token, test_region, test_user):
1913+
"""
1914+
Given a user creates a private AI key
1915+
When the vector database creation fails after LiteLLM token is created
1916+
Then the LiteLLM token should be cleaned up and an error returned
1917+
"""
1918+
# Mock successful LiteLLM token creation
1919+
mock_post.return_value.status_code = 200
1920+
mock_post.return_value.json.return_value = {"key": "test-private-key-123"}
1921+
mock_post.return_value.raise_for_status.return_value = None
1922+
1923+
# Mock vector database creation failure
1924+
mock_create_db.side_effect = Exception("Database creation failed")
1925+
1926+
response = client.post(
1927+
"/private-ai-keys/",
1928+
headers={"Authorization": f"Bearer {test_token}"},
1929+
json={
1930+
"region_id": test_region.id,
1931+
"name": "Test AI Key"
1932+
}
1933+
)
1934+
1935+
# Verify the response indicates failure
1936+
assert response.status_code == 500
1937+
assert "Failed to create vector database" in response.json()["detail"]
1938+
1939+
# Verify LiteLLM token was cleaned up (delete API was called)
1940+
# First call is for token creation, second call is for cleanup
1941+
assert mock_post.call_count == 2
1942+
1943+
# Verify the cleanup call
1944+
cleanup_call = mock_post.call_args_list[1]
1945+
assert cleanup_call[0][0] == f"{test_region.litellm_api_url}/key/delete"
1946+
assert cleanup_call[1]["headers"]["Authorization"] == f"Bearer {test_region.litellm_api_key}"
1947+
assert cleanup_call[1]["json"]["keys"] == ["test-private-key-123"]
1948+
1949+
# Verify no key was stored in the database
1950+
stored_keys = client.get(
1951+
"/private-ai-keys/",
1952+
headers={"Authorization": f"Bearer {test_token}"}
1953+
).json()
1954+
assert len([k for k in stored_keys if k["name"] == "Test AI Key"]) == 0
1955+
1956+
@patch("app.services.litellm.requests.post")
1957+
@patch("app.db.postgres.PostgresManager.create_database")
1958+
@patch("app.db.postgres.PostgresManager.delete_database")
1959+
@patch("sqlalchemy.orm.Session.commit")
1960+
def test_create_private_ai_key_cleanup_on_db_storage_failure(mock_commit, mock_delete_db, mock_create_db, mock_post, client, test_token, test_region, test_user):
1961+
"""
1962+
Given a user creates a private AI key
1963+
When the database storage fails after both LiteLLM token and vector DB are created
1964+
Then both resources should be cleaned up and an error returned
1965+
"""
1966+
# Mock successful LiteLLM token creation
1967+
mock_post.return_value.status_code = 200
1968+
mock_post.return_value.json.return_value = {"key": "test-private-key-123"}
1969+
mock_post.return_value.raise_for_status.return_value = None
1970+
1971+
# Mock successful vector database creation
1972+
mock_create_db.return_value = {
1973+
"database_name": "test_db_123",
1974+
"database_host": "test-host",
1975+
"database_username": "test_user",
1976+
"database_password": "test_pass"
1977+
}
1978+
1979+
# Mock successful vector database deletion
1980+
mock_delete_db.return_value = None
1981+
1982+
# Mock database storage failure
1983+
mock_commit.side_effect = Exception("Database storage failed")
1984+
1985+
response = client.post(
1986+
"/private-ai-keys/",
1987+
headers={"Authorization": f"Bearer {test_token}"},
1988+
json={
1989+
"region_id": test_region.id,
1990+
"name": "Test AI Key"
1991+
}
1992+
)
1993+
1994+
# Verify the response indicates failure
1995+
assert response.status_code == 500
1996+
assert "Failed to create private AI key" in response.json()["detail"]
1997+
1998+
# Verify LiteLLM token was cleaned up
1999+
assert mock_post.call_count == 2
2000+
cleanup_call = mock_post.call_args_list[1]
2001+
assert cleanup_call[0][0] == f"{test_region.litellm_api_url}/key/delete"
2002+
assert cleanup_call[1]["json"]["keys"] == ["test-private-key-123"]
2003+
2004+
# Verify vector database was cleaned up
2005+
mock_delete_db.assert_called_once_with("test_db_123")
2006+
2007+
# Verify no key was stored in the database
2008+
stored_keys = client.get(
2009+
"/private-ai-keys/",
2010+
headers={"Authorization": f"Bearer {test_token}"}
2011+
).json()
2012+
assert len([k for k in stored_keys if k["name"] == "Test AI Key"]) == 0
2013+
2014+
@patch("app.services.litellm.requests.post")
2015+
@patch("app.db.postgres.PostgresManager.create_database")
2016+
def test_create_private_ai_key_cleanup_failure_handling(mock_create_db, mock_post, client, test_token, test_region, test_user):
2017+
"""
2018+
Given a user creates a private AI key
2019+
When the cleanup process itself fails
2020+
Then the original error should still be returned to the user
2021+
"""
2022+
# Mock successful LiteLLM token creation
2023+
mock_post.return_value.status_code = 200
2024+
mock_post.return_value.json.return_value = {"key": "test-private-key-123"}
2025+
mock_post.return_value.raise_for_status.return_value = None
2026+
2027+
# Mock vector database creation failure
2028+
mock_create_db.side_effect = Exception("Database creation failed")
2029+
2030+
# Mock cleanup failure - the second call to requests.post will be for cleanup
2031+
# First call succeeds, second call fails
2032+
mock_post.side_effect = [
2033+
Mock(
2034+
status_code=200,
2035+
json=Mock(return_value={"key": "test-private-key-123"}),
2036+
raise_for_status=Mock(return_value=None)
2037+
),
2038+
Mock(
2039+
status_code=500,
2040+
raise_for_status=Mock(side_effect=HTTPError("Cleanup failed"))
2041+
)
2042+
]
2043+
2044+
response = client.post(
2045+
"/private-ai-keys/",
2046+
headers={"Authorization": f"Bearer {test_token}"},
2047+
json={
2048+
"region_id": test_region.id,
2049+
"name": "Test AI Key"
2050+
}
2051+
)
2052+
2053+
# Verify the response indicates the original failure, not cleanup failure
2054+
assert response.status_code == 500
2055+
assert "Failed to create vector database" in response.json()["detail"]
2056+
assert "Database creation failed" in response.json()["detail"]
2057+
2058+
# Verify cleanup was attempted
2059+
assert mock_post.call_count == 2
2060+
2061+
@patch("app.services.litellm.requests.post")
2062+
@patch("app.db.postgres.PostgresManager.create_database")
2063+
def test_create_private_ai_key_http_exception_preservation(mock_create_db, mock_post, client, test_token, test_region, test_user):
2064+
"""
2065+
Given a user creates a private AI key
2066+
When an HTTPException is raised during creation
2067+
Then the original HTTPException should be preserved and returned
2068+
"""
2069+
# Mock successful LiteLLM token creation
2070+
mock_post.return_value.status_code = 200
2071+
mock_post.return_value.json.return_value = {"key": "test-private-key-123"}
2072+
mock_post.return_value.raise_for_status.return_value = None
2073+
2074+
# Mock vector database creation failure with HTTPException
2075+
mock_create_db.side_effect = HTTPException(
2076+
status_code=status.HTTP_400_BAD_REQUEST,
2077+
detail="Invalid database configuration"
2078+
)
2079+
2080+
response = client.post(
2081+
"/private-ai-keys/",
2082+
headers={"Authorization": f"Bearer {test_token}"},
2083+
json={
2084+
"region_id": test_region.id,
2085+
"name": "Test AI Key"
2086+
}
2087+
)
2088+
2089+
# Verify the original HTTPException is preserved
2090+
assert response.status_code == 500
2091+
assert "Failed to create vector database" in response.json()["detail"]
2092+
assert "Invalid database configuration" in response.json()["detail"]
2093+
2094+
# Verify cleanup was attempted
2095+
assert mock_post.call_count == 2

0 commit comments

Comments
 (0)