""" Test prompt endpoints for version filtering and history """ from unittest.mock import MagicMock import pytest from litellm.types.prompts.init_prompts import ( PromptInfo, PromptLiteLLMParams, PromptSpec, ) class TestPromptVersioning: """ Test prompt versioning functionality """ def test_get_latest_prompt_versions(self): """ Test that get_latest_prompt_versions returns only the latest version of each prompt """ from litellm.proxy.prompts.prompt_endpoints import get_latest_prompt_versions # Create mock prompts with different versions prompts = [ PromptSpec( prompt_id="jack.v1", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v1 content", ), prompt_info=PromptInfo(prompt_type="db"), ), PromptSpec( prompt_id="jack.v2", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v2 content", ), prompt_info=PromptInfo(prompt_type="db"), ), PromptSpec( prompt_id="jane.v1", litellm_params=PromptLiteLLMParams( prompt_id="jane", prompt_integration="dotprompt", dotprompt_content="jane v1", ), prompt_info=PromptInfo(prompt_type="db"), ), PromptSpec( prompt_id="jack.v3", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v3 content", ), prompt_info=PromptInfo(prompt_type="db"), ), ] # Get latest versions latest = get_latest_prompt_versions(prompts=prompts) # Should return 2 prompts (jack.v3 and jane.v1) assert len(latest) == 2 # Find jack and jane in results jack_prompt = next((p for p in latest if "jack" in p.prompt_id), None) jane_prompt = next((p for p in latest if "jane" in p.prompt_id), None) assert jack_prompt is not None assert jack_prompt.prompt_id == "jack.v3" assert jack_prompt.litellm_params.dotprompt_content == "v3 content" assert jane_prompt is not None assert jane_prompt.prompt_id == "jane.v1" def test_get_version_number(self): """ Test that get_version_number correctly extracts version numbers """ from litellm.proxy.prompts.prompt_endpoints import get_version_number assert get_version_number(prompt_id="jack.v1") == 1 assert get_version_number(prompt_id="jack.v2") == 2 assert get_version_number(prompt_id="jack.v10") == 10 assert get_version_number(prompt_id="jack") == 1 assert get_version_number(prompt_id="jack.vinvalid") == 1 def test_get_base_prompt_id(self): """ Test that get_base_prompt_id correctly strips version suffixes """ from litellm.proxy.prompts.prompt_endpoints import get_base_prompt_id assert get_base_prompt_id(prompt_id="jack.v1") == "jack" assert get_base_prompt_id(prompt_id="jack.v2") == "jack" assert get_base_prompt_id(prompt_id="jack") == "jack" assert get_base_prompt_id(prompt_id="my_prompt.v10") == "my_prompt" def test_get_latest_version_prompt_id(self): """ Test that get_latest_version_prompt_id returns the highest version """ from litellm.proxy.prompts.prompt_endpoints import get_latest_version_prompt_id # Mock prompt IDs dictionary all_prompt_ids = { "jack.v1": {}, "jack.v2": {}, "jack.v3": {}, "jane.v1": {}, "simple_prompt": {}, } # Test with base prompt ID - should return latest version assert ( get_latest_version_prompt_id( prompt_id="jack", all_prompt_ids=all_prompt_ids ) == "jack.v3" ) # Test with versioned prompt ID - should still return latest version assert ( get_latest_version_prompt_id( prompt_id="jack.v1", all_prompt_ids=all_prompt_ids ) == "jack.v3" ) # Test with single version assert ( get_latest_version_prompt_id( prompt_id="jane", all_prompt_ids=all_prompt_ids ) == "jane.v1" ) # Test with non-versioned prompt assert ( get_latest_version_prompt_id( prompt_id="simple_prompt", all_prompt_ids=all_prompt_ids ) == "simple_prompt" ) # Test with non-existent prompt assert ( get_latest_version_prompt_id( prompt_id="nonexistent", all_prompt_ids=all_prompt_ids ) == "nonexistent" ) def test_construct_versioned_prompt_id(self): """ Test that construct_versioned_prompt_id correctly builds versioned IDs """ from litellm.proxy.prompts.prompt_endpoints import construct_versioned_prompt_id # Test with base prompt ID and version assert ( construct_versioned_prompt_id(prompt_id="jack_success", version=4) == "jack_success.v4" ) # Test with None version - should return base ID unchanged assert ( construct_versioned_prompt_id(prompt_id="jack_success", version=None) == "jack_success" ) # Test with existing versioned ID - should replace version assert ( construct_versioned_prompt_id(prompt_id="jack_success.v2", version=4) == "jack_success.v4" ) # Test with hyphenated prompt ID assert ( construct_versioned_prompt_id(prompt_id="my-prompt", version=1) == "my-prompt.v1" ) # Test with double-digit version assert ( construct_versioned_prompt_id(prompt_id="test_prompt", version=10) == "test_prompt.v10" ) class TestPromptVersionsEndpoint: """ Test the /prompts/{prompt_id}/versions endpoint """ @pytest.mark.asyncio async def test_get_prompt_versions_returns_all_versions(self): """ Test that get_prompt_versions returns all versions of a prompt sorted by version number """ from unittest.mock import patch from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.prompts.prompt_endpoints import get_prompt_versions # Mock user with admin role mock_user = UserAPIKeyAuth( api_key="test_key", user_role=LitellmUserRoles.PROXY_ADMIN ) # Create mock prompt registry with multiple versions mock_prompts = { "jack.v1": PromptSpec( prompt_id="jack.v1", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v1", ), prompt_info=PromptInfo(prompt_type="db"), ), "jack.v2": PromptSpec( prompt_id="jack.v2", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v2", ), prompt_info=PromptInfo(prompt_type="db"), ), "jack.v3": PromptSpec( prompt_id="jack.v3", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v3", ), prompt_info=PromptInfo(prompt_type="db"), ), "jane.v1": PromptSpec( prompt_id="jane.v1", litellm_params=PromptLiteLLMParams( prompt_id="jane", prompt_integration="dotprompt", dotprompt_content="jane", ), prompt_info=PromptInfo(prompt_type="db"), ), } # Force the in-memory path so this test is isolated from any leaked prisma mocks. with ( patch("litellm.proxy.proxy_server.prisma_client", None), patch( "litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY" ) as mock_registry, ): mock_registry.IN_MEMORY_PROMPTS = mock_prompts # Test with base prompt ID response = await get_prompt_versions( prompt_id="jack", user_api_key_dict=mock_user ) # Should return 3 versions of jack, sorted newest first assert len(response.prompts) == 3 assert response.prompts[0].prompt_id == "jack" assert response.prompts[0].version == 3 assert response.prompts[1].prompt_id == "jack" assert response.prompts[1].version == 2 assert response.prompts[2].prompt_id == "jack" assert response.prompts[2].version == 1 # Test with versioned prompt ID (should strip version) response = await get_prompt_versions( prompt_id="jack.v1", user_api_key_dict=mock_user ) assert len(response.prompts) == 3 assert response.prompts[0].prompt_id == "jack" assert response.prompts[0].version == 3 @pytest.mark.asyncio async def test_get_prompt_versions_not_found(self): """ Test that get_prompt_versions raises 404 when prompt doesn't exist """ from unittest.mock import patch from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.prompts.prompt_endpoints import get_prompt_versions mock_user = UserAPIKeyAuth( api_key="test_key", user_role=LitellmUserRoles.PROXY_ADMIN ) with ( patch("litellm.proxy.proxy_server.prisma_client", None), patch( "litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY" ) as mock_registry, ): mock_registry.IN_MEMORY_PROMPTS = {} with pytest.raises(HTTPException) as exc_info: await get_prompt_versions( prompt_id="nonexistent", user_api_key_dict=mock_user ) assert exc_info.value.status_code == 404 assert "No versions found" in exc_info.value.detail class TestAdminViewerReadAccess: """ proxy_admin_viewer has READ parity with proxy_admin on the prompt read endpoints """ @pytest.mark.asyncio async def test_list_prompts_returns_all_prompts_for_admin_viewer(self): """A role without admin view falls through to the empty-list branch here.""" from unittest.mock import patch from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.prompts.prompt_endpoints import list_prompts viewer = UserAPIKeyAuth( api_key="test_key", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY ) mock_prompts = { "jack.v1": PromptSpec( prompt_id="jack.v1", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v1", ), prompt_info=PromptInfo(prompt_type="db"), ), "jack.v2": PromptSpec( prompt_id="jack.v2", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v2", ), prompt_info=PromptInfo(prompt_type="db"), ), "jane.v1": PromptSpec( prompt_id="jane.v1", litellm_params=PromptLiteLLMParams( prompt_id="jane", prompt_integration="dotprompt", dotprompt_content="jane", ), prompt_info=PromptInfo(prompt_type="db"), ), } with patch( "litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY" ) as mock_registry: mock_registry.IN_MEMORY_PROMPTS = mock_prompts response = await list_prompts(user_api_key_dict=viewer) assert sorted(p.prompt_id for p in response.prompts) == ["jack", "jane"] jack = next(p for p in response.prompts if p.prompt_id == "jack") assert jack.litellm_params.dotprompt_content == "v2" @pytest.mark.asyncio async def test_get_prompt_versions_allows_admin_viewer(self): """Version history used to 403 anyone who was not exactly proxy_admin.""" from unittest.mock import patch from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.prompts.prompt_endpoints import get_prompt_versions viewer = UserAPIKeyAuth( api_key="test_key", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY ) mock_prompts = { "jack.v1": PromptSpec( prompt_id="jack.v1", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v1", ), prompt_info=PromptInfo(prompt_type="db"), ), "jack.v2": PromptSpec( prompt_id="jack.v2", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v2", ), prompt_info=PromptInfo(prompt_type="db"), ), } with ( patch("litellm.proxy.proxy_server.prisma_client", None), patch( "litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY" ) as mock_registry, ): mock_registry.IN_MEMORY_PROMPTS = mock_prompts response = await get_prompt_versions( prompt_id="jack", user_api_key_dict=viewer ) assert [p.version for p in response.prompts] == [2, 1] @pytest.mark.asyncio async def test_get_prompt_info_allows_admin_viewer(self): """Prompt info used to 403 anyone who was not exactly proxy_admin.""" from unittest.mock import patch from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.prompts.prompt_endpoints import get_prompt_info viewer = UserAPIKeyAuth( api_key="test_key", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY ) with ( patch("litellm.proxy.proxy_server.prisma_client", None), patch( "litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY" ) as mock_registry, ): mock_registry.get_prompt_by_id.return_value = PromptSpec( prompt_id="jack.v2", litellm_params=PromptLiteLLMParams( prompt_id="jack", prompt_integration="dotprompt", dotprompt_content="v2", ), prompt_info=PromptInfo(prompt_type="db"), ) mock_registry.IN_MEMORY_PROMPTS = {"jack.v1": {}, "jack.v2": {}} mock_registry.get_prompt_callback_by_id.return_value = None response = await get_prompt_info(prompt_id="jack", user_api_key_dict=viewer) assert response.prompt_spec.prompt_id == "jack" assert response.prompt_spec.version == 2