diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index a7f327c2e59..ec0b46990cb 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -5559,6 +5559,16 @@ _LOCK_TEAMS_BY_ID_SQL: Final = ( ) +def _user_unique_conflict_message(metadata: object, user_id: object) -> str: + match metadata: + case {"target": ["user_id"]}: + return f"User with id {user_id} already exists" + case {"target": ["sso_user_id"]}: + return "User with this sso_user_id already exists" + case _: + return "User conflicts with an existing unique value" + + class PrismaClient: spend_log_transactions: list = [] _spend_log_transactions_lock = asyncio.Lock() @@ -6446,6 +6456,7 @@ class PrismaClient: max_tries=3, # maximum number of retries max_time=10, # maximum total time to retry for on_backoff=on_backoff, # specifying the function to call on backoff + giveup=lambda exc: isinstance(exc, HTTPException) and exc.status_code == 409, ) @log_db_metrics async def insert_data( @@ -6504,7 +6515,7 @@ class PrismaClient: except UniqueViolationError as e: raise HTTPException( status_code=409, - detail={"error": f"User with id {db_data['user_id']} already exists"}, + detail={"error": _user_unique_conflict_message(e.meta, db_data["user_id"])}, ) from e except Exception as e: if ( diff --git a/tests/unit/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/unit/proxy/management_endpoints/test_internal_user_endpoints.py index a7c9703e693..b96a5b606eb 100644 --- a/tests/unit/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_internal_user_endpoints.py @@ -73,7 +73,10 @@ async def test_new_user_returns_conflict_when_id_is_taken_after_duplicate_check( table: Final = SimpleNamespace( find_first=AsyncMock(return_value=None), count=AsyncMock(return_value=0), - create=AsyncMock(side_effect=UniqueViolationError({}, message="Unique constraint failed on user_id")), + create=AsyncMock(side_effect=UniqueViolationError( + {"user_facing_error": {"meta": {"target": ["user_id"]}}}, + message="Unique constraint failed on user_id", + )), upsert=AsyncMock(return_value=LiteLLM_UserTable(user_id="employee-42")), ) mocker.patch("prisma.Prisma", return_value=SimpleNamespace(litellm_usertable=table)) diff --git a/tests/unit/proxy/utils/prisma_and_spend/test_prisma_client_writes.py b/tests/unit/proxy/utils/prisma_and_spend/test_prisma_client_writes.py index e335b08f040..a9950a9d8e3 100644 --- a/tests/unit/proxy/utils/prisma_and_spend/test_prisma_client_writes.py +++ b/tests/unit/proxy/utils/prisma_and_spend/test_prisma_client_writes.py @@ -18,8 +18,10 @@ from types import SimpleNamespace from typing import Final from unittest.mock import AsyncMock, MagicMock, patch +import httpx import pytest from fastapi import HTTPException +from prisma.errors import UniqueViolationError from litellm._service_logger import ServiceTypes from litellm.models.user import LiteLLM_UserTable @@ -192,6 +194,48 @@ async def test_insert_data_team_serializes_members(prisma_client: PrismaClient) assert create_payload["team_id"] == "t1" +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("metadata", "message"), + [ + ({"target": ["user_id"]}, "User with id requested-user already exists"), + ({"target": ["sso_user_id"]}, "User with this sso_user_id already exists"), + (None, "User conflicts with an existing unique value"), + ({"target": ["unknown_field"]}, "User conflicts with an existing unique value"), + ], +) +async def test_user_conflict_names_constraint_and_does_not_retry( + prisma_client: PrismaClient, + metadata: Mapping[str, object] | None, + message: str, +) -> None: + create: Final = AsyncMock(side_effect=UniqueViolationError({"user_facing_error": {"meta": metadata}})) + prisma_client.db.litellm_usertable.create = create + with pytest.raises(HTTPException) as error: + await prisma_client.insert_data( + data={"user_id": "requested-user", "sso_user_id": "sso-subject"}, + table_name="user", allow_existing_user=False, + ) + assert error.value.status_code == 409 + assert error.value.detail == {"error": message} + assert create.await_count == 1 + + +@pytest.mark.asyncio +async def test_user_insert_still_retries_transient_connection_failure( + prisma_client: PrismaClient, monkeypatch: pytest.MonkeyPatch +) -> None: + row: Final = LiteLLM_UserTable(user_id="requested-user") + create: Final = AsyncMock(side_effect=[httpx.ConnectError("connection unavailable"), row]) + prisma_client.db.litellm_usertable.create = create + monkeypatch.setattr("asyncio.sleep", AsyncMock()) + result: Final = await prisma_client.insert_data( + data={"user_id": row.user_id}, table_name="user", allow_existing_user=False + ) + assert result is row + assert create.await_count == 2 + + @pytest.mark.asyncio async def test_insert_data_user_organization_fk_raises_400( prisma_client: PrismaClient,