From 1581bcf9853dae64462ebb1d1ecbe600c65a45cd Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 27 Jan 2026 16:54:52 -0800 Subject: [PATCH] add sortBy and sortOrder params for /v2/model/info --- litellm/proxy/proxy_server.py | 135 ++++- tests/test_litellm/proxy/test_proxy_server.py | 273 +++++++++ .../components/molecules/models/columns.tsx | 561 +++++++++--------- 3 files changed, 682 insertions(+), 287 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 183c25ed463..fb4a59fd04c 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7807,6 +7807,7 @@ async def _apply_search_filter_to_models( size: int, prisma_client: Optional[Any], proxy_config: Any, + sort_by: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], Optional[int]]: """ Apply search filter to models, querying database for additional matching models. @@ -7818,6 +7819,7 @@ async def _apply_search_filter_to_models( size: Page size prisma_client: Prisma client for database queries proxy_config: Proxy config for decrypting models + sort_by: Optional sort field - if provided, fetch all matching models instead of paginating at DB level Returns: Tuple of (filtered_models, total_count). total_count is None if not searching. @@ -7881,17 +7883,15 @@ async def _apply_search_filter_to_models( # Calculate total count for search results search_total_count = router_models_count + db_models_total_count - # Fetch database models if we need more for the current page - if router_models_count < models_needed_for_page: - models_to_fetch = min( - models_needed_for_page - router_models_count, db_models_total_count - ) - - if models_to_fetch > 0: + # If sorting is requested, we need to fetch ALL matching models to sort correctly + # Otherwise, we can optimize by only fetching what's needed for the current page + if sort_by: + # Fetch all matching database models for sorting + if db_models_total_count > 0: db_models_raw = ( await prisma_client.db.litellm_proxymodeltable.find_many( where=db_where_condition, - take=models_to_fetch, + take=db_models_total_count, # Fetch all matching models ) ) @@ -7902,6 +7902,28 @@ async def _apply_search_filter_to_models( ) if decrypted_models: db_models.extend(decrypted_models) + else: + # Fetch database models if we need more for the current page + if router_models_count < models_needed_for_page: + models_to_fetch = min( + models_needed_for_page - router_models_count, db_models_total_count + ) + + if models_to_fetch > 0: + db_models_raw = ( + await prisma_client.db.litellm_proxymodeltable.find_many( + where=db_where_condition, + take=models_to_fetch, + ) + ) + + # Convert database models to router format + for db_model in db_models_raw: + decrypted_models = proxy_config.decrypt_model_list_from_db( + [db_model] + ) + if decrypted_models: + db_models.extend(decrypted_models) except Exception as e: verbose_proxy_logger.exception( f"Error querying database models with search: {str(e)}" @@ -7917,6 +7939,80 @@ async def _apply_search_filter_to_models( return filtered_models, search_total_count +def _sort_models( + all_models: List[Dict[str, Any]], + sort_by: Optional[str], + sort_order: str = "asc", +) -> List[Dict[str, Any]]: + """ + Sort models by the specified field and order. + + Args: + all_models: List of models to sort + sort_by: Field to sort by (model_name, created_at, updated_at, costs, status) + sort_order: Sort order (asc or desc) + + Returns: + Sorted list of models + """ + if not sort_by or sort_by not in ["model_name", "created_at", "updated_at", "costs", "status"]: + return all_models + + reverse = sort_order.lower() == "desc" + + def get_sort_key(model: Dict[str, Any]) -> Any: + model_info = model.get("model_info", {}) + + if sort_by == "model_name": + return model.get("model_name", "").lower() + + elif sort_by == "created_at": + created_at = model_info.get("created_at") + if created_at is None: + # Put None values at the end for asc, at the start for desc + return (datetime.max if not reverse else datetime.min) + if isinstance(created_at, str): + try: + return datetime.fromisoformat(created_at.replace("Z", "+00:00")) + except (ValueError, AttributeError): + return datetime.min if not reverse else datetime.max + return created_at + + elif sort_by == "updated_at": + updated_at = model_info.get("updated_at") + if updated_at is None: + return (datetime.max if not reverse else datetime.min) + if isinstance(updated_at, str): + try: + return datetime.fromisoformat(updated_at.replace("Z", "+00:00")) + except (ValueError, AttributeError): + return datetime.min if not reverse else datetime.max + return updated_at + + elif sort_by == "costs": + input_cost = model_info.get("input_cost_per_token", 0) or 0 + output_cost = model_info.get("output_cost_per_token", 0) or 0 + total_cost = input_cost + output_cost + # Put 0 or None costs at the end for asc, at the start for desc + if total_cost == 0: + return (float("inf") if not reverse else float("-inf")) + return total_cost + + elif sort_by == "status": + # False (config) comes before True (db) for asc + db_model = model_info.get("db_model", False) + return db_model + + return None + + try: + sorted_models = sorted(all_models, key=get_sort_key, reverse=reverse) + return sorted_models + except Exception as e: + verbose_proxy_logger.exception(f"Error sorting models by {sort_by}: {str(e)}") + return all_models + + def _paginate_models_response( all_models: List[Dict[str, Any]], page: int, @@ -8109,6 +8205,14 @@ async def model_info_v2( None, description="Filter models by team ID. Returns models with direct_access=True or teamId in access_via_team_ids", ), + sortBy: Optional[str] = fastapi.Query( + None, + description="Field to sort by. Options: model_name, created_at, updated_at, costs, status", + ), + sortOrder: Optional[str] = fastapi.Query( + "asc", + description="Sort order. Options: asc, desc", + ), ): """ BETA ENDPOINT. Might change unexpectedly. Use `/v1/model/info` for now. @@ -8193,6 +8297,7 @@ async def model_info_v2( size=size, prisma_client=prisma_client, proxy_config=proxy_config, + sort_by=sortBy, ) if user_models_only: @@ -8236,6 +8341,20 @@ async def model_info_v2( if modelId is not None: search_total_count = len(all_models) + # Apply sorting before pagination + if sortBy: + # Validate sortOrder + if sortOrder and sortOrder.lower() not in ["asc", "desc"]: + raise HTTPException( + status_code=400, + detail=f"Invalid sortOrder: {sortOrder}. Must be 'asc' or 'desc'", + ) + all_models = _sort_models( + all_models=all_models, + sort_by=sortBy, + sort_order=sortOrder or "asc", + ) + verbose_proxy_logger.debug("all_models: %s", all_models) return _paginate_models_response( diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 2f6eccaf04d..22a60f220ee 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -4013,6 +4013,279 @@ async def test_model_info_v2_filter_by_team_id(monkeypatch): app.dependency_overrides = original_overrides +@pytest.mark.asyncio +@pytest.mark.parametrize( + "sort_by,sort_order,expected_order", + [ + # Test model_name sorting + ("model_name", "asc", ["a-model", "b-model", "z-model"]), + ("model_name", "desc", ["z-model", "b-model", "a-model"]), + # Test created_at sorting + ("created_at", "asc", ["old-model", "mid-model", "new-model"]), + ("created_at", "desc", ["new-model", "mid-model", "old-model"]), + # Test updated_at sorting + ("updated_at", "asc", ["old-updated", "mid-updated", "new-updated"]), + ("updated_at", "desc", ["new-updated", "mid-updated", "old-updated"]), + # Test costs sorting + ("costs", "asc", ["low-cost", "mid-cost", "high-cost"]), + ("costs", "desc", ["high-cost", "mid-cost", "low-cost"]), + # Test status sorting (False/config models come before True/db models in asc) + ("status", "asc", ["config-model-1", "config-model-2", "db-model"]), + ("status", "desc", ["db-model", "config-model-1", "config-model-2"]), + ], +) +async def test_model_info_v2_sorting(monkeypatch, sort_by, sort_order, expected_order): + """ + Test sorting functionality for /v2/model/info endpoint. + Tests all sortBy fields (model_name, created_at, updated_at, costs, status) + with both asc and desc sort orders. + """ + from datetime import datetime, timedelta + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth + + # Create base time for date comparisons + base_time = datetime(2024, 1, 1, 12, 0, 0) + + # Create mock models with different values for each sort field + mock_models = [] + + if sort_by == "model_name": + # Models with different names + mock_models = [ + { + "model_name": "z-model", + "litellm_params": {"model": "z-model"}, + "model_info": {"id": "z-model"}, + }, + { + "model_name": "a-model", + "litellm_params": {"model": "a-model"}, + "model_info": {"id": "a-model"}, + }, + { + "model_name": "b-model", + "litellm_params": {"model": "b-model"}, + "model_info": {"id": "b-model"}, + }, + ] + elif sort_by == "created_at": + # Models with different created_at timestamps + mock_models = [ + { + "model_name": "new-model", + "litellm_params": {"model": "new-model"}, + "model_info": { + "id": "new-model", + "created_at": (base_time + timedelta(days=3)).isoformat(), + }, + }, + { + "model_name": "old-model", + "litellm_params": {"model": "old-model"}, + "model_info": { + "id": "old-model", + "created_at": (base_time - timedelta(days=3)).isoformat(), + }, + }, + { + "model_name": "mid-model", + "litellm_params": {"model": "mid-model"}, + "model_info": { + "id": "mid-model", + "created_at": base_time.isoformat(), + }, + }, + ] + elif sort_by == "updated_at": + # Models with different updated_at timestamps + mock_models = [ + { + "model_name": "new-updated", + "litellm_params": {"model": "new-updated"}, + "model_info": { + "id": "new-updated", + "updated_at": (base_time + timedelta(days=3)).isoformat(), + }, + }, + { + "model_name": "old-updated", + "litellm_params": {"model": "old-updated"}, + "model_info": { + "id": "old-updated", + "updated_at": (base_time - timedelta(days=3)).isoformat(), + }, + }, + { + "model_name": "mid-updated", + "litellm_params": {"model": "mid-updated"}, + "model_info": { + "id": "mid-updated", + "updated_at": base_time.isoformat(), + }, + }, + ] + elif sort_by == "costs": + # Models with different costs (input_cost + output_cost) + mock_models = [ + { + "model_name": "high-cost", + "litellm_params": {"model": "high-cost"}, + "model_info": { + "id": "high-cost", + "input_cost_per_token": 0.00005, + "output_cost_per_token": 0.00015, + }, + }, + { + "model_name": "low-cost", + "litellm_params": {"model": "low-cost"}, + "model_info": { + "id": "low-cost", + "input_cost_per_token": 0.00001, + "output_cost_per_token": 0.00003, + }, + }, + { + "model_name": "mid-cost", + "litellm_params": {"model": "mid-cost"}, + "model_info": { + "id": "mid-cost", + "input_cost_per_token": 0.00003, + "output_cost_per_token": 0.00007, + }, + }, + ] + elif sort_by == "status": + # Models with different db_model status (False = config, True = db) + mock_models = [ + { + "model_name": "db-model", + "litellm_params": {"model": "db-model"}, + "model_info": {"id": "db-model", "db_model": True}, + }, + { + "model_name": "config-model-1", + "litellm_params": {"model": "config-model-1"}, + "model_info": {"id": "config-model-1", "db_model": False}, + }, + { + "model_name": "config-model-2", + "litellm_params": {"model": "config-model-2"}, + "model_info": {"id": "config-model-2", "db_model": False}, + }, + ] + + # Mock llm_router + mock_router = MagicMock() + mock_router.model_list = mock_models + + # Mock prisma_client + mock_prisma_client = MagicMock() + + # Mock proxy_config.get_config + mock_get_config = AsyncMock(return_value={}) + + # Mock user authentication + mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + mock_user_api_key_dict.user_id = "test-user" + mock_user_api_key_dict.api_key = "test-key" + mock_user_api_key_dict.team_models = [] + mock_user_api_key_dict.models = [] + + # Apply monkeypatches + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr(proxy_config, "get_config", mock_get_config) + + # Override auth dependency + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[user_api_key_auth] = lambda: mock_user_api_key_dict + + client = TestClient(app) + try: + # Test sorting with specified sortBy and sortOrder + response = client.get( + "/v2/model/info", params={"sortBy": sort_by, "sortOrder": sort_order} + ) + assert response.status_code == 200 + data = response.json() + assert len(data["data"]) == len(expected_order) + + # Verify models are in expected order + actual_order = [m["model_name"] for m in data["data"]] + assert actual_order == expected_order, ( + f"Sorting failed for sortBy={sort_by}, sortOrder={sort_order}. " + f"Expected: {expected_order}, Got: {actual_order}" + ) + + finally: + app.dependency_overrides = original_overrides + + +@pytest.mark.asyncio +async def test_model_info_v2_sorting_invalid_sort_order(monkeypatch): + """ + Test that invalid sortOrder values return a 400 error. + """ + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth + + # Create mock models + mock_models = [ + { + "model_name": "test-model", + "litellm_params": {"model": "test-model"}, + "model_info": {"id": "test-model"}, + } + ] + + # Mock llm_router + mock_router = MagicMock() + mock_router.model_list = mock_models + + # Mock prisma_client + mock_prisma_client = MagicMock() + + # Mock proxy_config.get_config + mock_get_config = AsyncMock(return_value={}) + + # Mock user authentication + mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + mock_user_api_key_dict.user_id = "test-user" + mock_user_api_key_dict.api_key = "test-key" + mock_user_api_key_dict.team_models = [] + mock_user_api_key_dict.models = [] + + # Apply monkeypatches + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr(proxy_config, "get_config", mock_get_config) + + # Override auth dependency + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[user_api_key_auth] = lambda: mock_user_api_key_dict + + client = TestClient(app) + try: + # Test invalid sortOrder + response = client.get( + "/v2/model/info", params={"sortBy": "model_name", "sortOrder": "invalid"} + ) + assert response.status_code == 400 + data = response.json() + assert "Invalid sortOrder" in data["detail"] + + finally: + app.dependency_overrides = original_overrides + + @pytest.mark.asyncio async def test_apply_search_filter_to_models(monkeypatch): """ diff --git a/ui/litellm-dashboard/src/components/molecules/models/columns.tsx b/ui/litellm-dashboard/src/components/molecules/models/columns.tsx index 43813852f8f..1fc08e502ac 100644 --- a/ui/litellm-dashboard/src/components/molecules/models/columns.tsx +++ b/ui/litellm-dashboard/src/components/molecules/models/columns.tsx @@ -17,297 +17,300 @@ export const columns = ( expandedRows: Set, setExpandedRows: (expandedRows: Set) => void, ): ColumnDef[] => [ - { - header: () => Model ID, - accessorKey: "model_info.id", - cell: ({ row }) => { - const model = row.original; - return ( - -
setSelectedModelId(model.model_info.id)} - > - {model.model_info.id} -
-
- ); - }, - }, - { - header: () => Model Information, - accessorKey: "model_name", - size: 250, // Fixed column width - cell: ({ row }) => { - const model = row.original; - const displayName = getDisplayModelName(row.original) || "-"; - const tooltipContent = ( -
-
- Provider: {model.provider || "-"} -
-
- Public Model Name: {displayName} -
-
- LiteLLM Model Name: {model.litellm_model_name || "-"} -
-
- ); - - return ( - -
- {/* Provider Icon */} -
- {model.provider ? ( - - ) : ( -
-
- )} -
- - {/* Model Names Container */} -
- {/* Public Model Name */} -
{displayName}
- {/* LiteLLM Model Name */} -
- {model.litellm_model_name || "-"} -
-
-
-
- ); - }, - }, - { - header: () => Credentials, - accessorKey: "litellm_credential_name", - size: 180, // Fixed column width - cell: ({ row }) => { - const model = row.original; - const credentialName = model.litellm_params?.litellm_credential_name; - - return credentialName ? ( - -
- - - {credentialName} - -
-
- ) : ( -
- - No credentials -
- ); - }, - }, - { - header: () => Created By, - accessorKey: "model_info.created_by", - sortingFn: "datetime", - size: 160, // Fixed column width - cell: ({ row }) => { - const model = row.original; - const isConfigModel = !model.model_info?.db_model; - const createdBy = model.model_info.created_by; - const createdAt = model.model_info.created_at ? new Date(model.model_info.created_at).toLocaleDateString() : null; - - return ( -
- {/* Created By - Primary */} -
- {isConfigModel ? "Defined in config" : createdBy || "Unknown"} -
- {/* Created At - Secondary */} -
- {isConfigModel ? "-" : createdAt || "Unknown date"} -
-
- ); - }, - }, - { - header: () => Updated At, - accessorKey: "model_info.updated_at", - sortingFn: "datetime", - cell: ({ row }) => { - const model = row.original; - return ( - - {model.model_info.updated_at ? new Date(model.model_info.updated_at).toLocaleDateString() : "-"} - - ); - }, - }, - { - header: () => Costs, - accessorKey: "input_cost", - size: 120, // Fixed column width - cell: ({ row }) => { - const model = row.original; - const inputCost = model.input_cost; - const outputCost = model.output_cost; - - // If both costs are missing or undefined, show "-" - if (!inputCost && !outputCost) { + { + header: () => Model ID, + accessorKey: "model_info.id", + enableSorting: false, + cell: ({ row }) => { + const model = row.original; return ( -
- - + +
setSelectedModelId(model.model_info.id)} + > + {model.model_info.id} +
+
+ ); + }, + }, + { + header: () => Model Information, + accessorKey: "model_name", + size: 250, // Fixed column width + cell: ({ row }) => { + const model = row.original; + const displayName = getDisplayModelName(row.original) || "-"; + const tooltipContent = ( +
+
+ Provider: {model.provider || "-"} +
+
+ Public Model Name: {displayName} +
+
+ LiteLLM Model Name: {model.litellm_model_name || "-"} +
); - } - return ( - -
- {/* Input Cost - Primary */} - {inputCost &&
In: ${inputCost}
} - {/* Output Cost - Secondary */} - {outputCost &&
Out: ${outputCost}
} -
-
- ); - }, - }, - { - header: () => Team ID, - accessorKey: "model_info.team_id", - cell: ({ row }) => { - const model = row.original; - return model.model_info.team_id ? ( -
- - + return ( + +
+ {/* Provider Icon */} +
+ {model.provider ? ( + + ) : ( +
-
+ )} +
+ + {/* Model Names Container */} +
+ {/* Public Model Name */} +
{displayName}
+ {/* LiteLLM Model Name */} +
+ {model.litellm_model_name || "-"} +
+
+
-
- ) : ( - "-" - ); + ); + }, }, - }, - { - header: () => Model Access Group, - accessorKey: "model_info.model_access_group", - enableSorting: false, - cell: ({ row }) => { - const model = row.original; - const accessGroups = model.model_info.access_groups; + { + header: () => Credentials, + accessorKey: "litellm_credential_name", + enableSorting: false, + size: 180, // Fixed column width + cell: ({ row }) => { + const model = row.original; + const credentialName = model.litellm_params?.litellm_credential_name; - if (!accessGroups || accessGroups.length === 0) { - return "-"; - } + return credentialName ? ( + +
+ + + {credentialName} + +
+
+ ) : ( +
+ + No credentials +
+ ); + }, + }, + { + header: () => Created By, + accessorKey: "model_info.created_by", + sortingFn: "datetime", + size: 160, // Fixed column width + cell: ({ row }) => { + const model = row.original; + const isConfigModel = !model.model_info?.db_model; + const createdBy = model.model_info.created_by; + const createdAt = model.model_info.created_at ? new Date(model.model_info.created_at).toLocaleDateString() : null; - const modelId = model.model_info.id; - const isExpanded = expandedRows.has(modelId); - const shouldShowExpandButton = accessGroups.length > 1; - - const toggleExpanded = () => { - const newExpanded = new Set(expandedRows); - if (isExpanded) { - newExpanded.delete(modelId); - } else { - newExpanded.add(modelId); - } - setExpandedRows(newExpanded); - }; - - return ( -
- - {accessGroups[0]} - - - {(isExpanded || (!shouldShowExpandButton && accessGroups.length === 2)) && - accessGroups.slice(1).map((group: string, index: number) => ( - - {group} - - ))} - - {shouldShowExpandButton && ( - - )} -
- ); + {isConfigModel ? "Defined in config" : createdBy || "Unknown"} +
+ {/* Created At - Secondary */} +
+ {isConfigModel ? "-" : createdAt || "Unknown date"} +
+ + ); + }, }, - }, - { - header: () => Status, - accessorKey: "model_info.db_model", - cell: ({ row }) => { - const model = row.original; - return ( -
Updated At, + accessorKey: "model_info.updated_at", + sortingFn: "datetime", + cell: ({ row }) => { + const model = row.original; + return ( + + {model.model_info.updated_at ? new Date(model.model_info.updated_at).toLocaleDateString() : "-"} + + ); + }, + }, + { + header: () => Costs, + accessorKey: "input_cost", + size: 120, // Fixed column width + cell: ({ row }) => { + const model = row.original; + const inputCost = model.input_cost; + const outputCost = model.output_cost; + + // If both costs are missing or undefined, show "-" + if (!inputCost && !outputCost) { + return ( +
+ - +
+ ); + } + + return ( + +
+ {/* Input Cost - Primary */} + {inputCost &&
In: ${inputCost}
} + {/* Output Cost - Secondary */} + {outputCost &&
Out: ${outputCost}
} +
+
+ ); + }, + }, + { + header: () => Team ID, + accessorKey: "model_info.team_id", + enableSorting: false, + cell: ({ row }) => { + const model = row.original; + return model.model_info.team_id ? ( +
+ + + +
+ ) : ( + "-" + ); + }, + }, + { + header: () => Model Access Group, + accessorKey: "model_info.model_access_group", + enableSorting: false, + cell: ({ row }) => { + const model = row.original; + const accessGroups = model.model_info.access_groups; + + if (!accessGroups || accessGroups.length === 0) { + return "-"; + } + + const modelId = model.model_info.id; + const isExpanded = expandedRows.has(modelId); + const shouldShowExpandButton = accessGroups.length > 1; + + const toggleExpanded = () => { + const newExpanded = new Set(expandedRows); + if (isExpanded) { + newExpanded.delete(modelId); + } else { + newExpanded.add(modelId); + } + setExpandedRows(newExpanded); + }; + + return ( +
+ + {accessGroups[0]} + + + {(isExpanded || (!shouldShowExpandButton && accessGroups.length === 2)) && + accessGroups.slice(1).map((group: string, index: number) => ( + + {group} + + ))} + + {shouldShowExpandButton && ( + + )} +
+ ); + }, + }, + { + header: () => Status, + accessorKey: "model_info.db_model", + cell: ({ row }) => { + const model = row.original; + return ( +
- {model.model_info.db_model ? "DB Model" : "Config Model"} -
- ); + > + {model.model_info.db_model ? "DB Model" : "Config Model"} +
+ ); + }, }, - }, - { - id: "actions", - header: () => Actions, - cell: ({ row }) => { - const model = row.original; - const canEditModel = userRole === "Admin" || model.model_info?.created_by === userID; - const isConfigModel = !model.model_info?.db_model; - return ( -
- {isConfigModel ? ( - - - - ) : ( - - { - if (canEditModel) { - setSelectedModelId(model.model_info.id); - } - }} - className={!canEditModel ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:text-red-600"} - /> - - )} -
- ); + { + id: "actions", + header: () => Actions, + cell: ({ row }) => { + const model = row.original; + const canEditModel = userRole === "Admin" || model.model_info?.created_by === userID; + const isConfigModel = !model.model_info?.db_model; + return ( +
+ {isConfigModel ? ( + + + + ) : ( + + { + if (canEditModel) { + setSelectedModelId(model.model_info.id); + } + }} + className={!canEditModel ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:text-red-600"} + /> + + )} +
+ ); + }, }, - }, -]; + ];