diff --git a/litellm/__init__.py b/litellm/__init__.py index 1252ee3d6fb..35051c65a50 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -65,8 +65,8 @@ from litellm.proxy._types import ( KeyManagementSystem, KeyManagementSettings, LiteLLM_UpperboundKeyGenerateParams, - NewTeamRequest, ) +from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams from litellm.types.utils import StandardKeyGenerationConfig, LlmProviders from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.logging_callback_manager import LoggingCallbackManager @@ -277,7 +277,7 @@ default_key_generate_params: Optional[Dict] = None upperbound_key_generate_params: Optional[LiteLLM_UpperboundKeyGenerateParams] = None key_generation_settings: Optional[StandardKeyGenerationConfig] = None default_internal_user_params: Optional[Dict] = None -default_team_params: Optional[Union[NewTeamRequest, Dict]] = None +default_team_params: Optional[Union[DefaultTeamSSOParams, Dict]] = None default_team_settings: Optional[List] = None max_user_budget: Optional[float] = None default_max_internal_user_budget: Optional[float] = None diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 1804aa9f569..6e50bdd5a36 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -941,7 +941,7 @@ class SSOAuthenticationHandler: @staticmethod def _cast_and_deepcopy_litellm_default_team_params( - default_team_params: Union[NewTeamRequest, Dict], + default_team_params: Union[DefaultTeamSSOParams, Dict], team_request: NewTeamRequest, litellm_team_id: str, litellm_team_name: Optional[str] = None, @@ -949,23 +949,20 @@ class SSOAuthenticationHandler: """ Casts and deepcopies the litellm.default_team_params to a NewTeamRequest object - - Ensures we create a new NewTeamRequest object - - Handle the case where litellm.default_team_params is a dict or a NewTeamRequest object - - Adds the litellm_team_id and litellm_team_name to the NewTeamRequest object + - Ensures we create a new DefaultTeamSSOParams object + - Handle the case where litellm.default_team_params is a dict or a DefaultTeamSSOParams object + - Adds the litellm_team_id and litellm_team_name to the DefaultTeamSSOParams object """ if isinstance(default_team_params, dict): _team_request = deepcopy(default_team_params) _team_request["team_id"] = litellm_team_id _team_request["team_alias"] = litellm_team_name team_request = NewTeamRequest(**_team_request) - elif isinstance(litellm.default_team_params, NewTeamRequest): - team_request = litellm.default_team_params.model_copy( - deep=True, - update={ - "team_id": litellm_team_id, - "team_alias": litellm_team_name, - }, - ) + elif isinstance(litellm.default_team_params, DefaultTeamSSOParams): + _default_team_params = deepcopy(litellm.default_team_params) + _new_team_request = team_request.model_dump() + _new_team_request.update(_default_team_params) + team_request = NewTeamRequest(**_new_team_request) return team_request diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 504ba9537aa..6f1b2bfb8c9 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -1,5 +1,5 @@ #### CRUD ENDPOINTS for UI Settings ##### -from typing import List +from typing import Any, List, Union from fastapi import APIRouter, Depends, HTTPException @@ -7,6 +7,7 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams router = APIRouter() @@ -111,34 +112,31 @@ async def delete_allowed_ip(ip_address: IPAddress): return {"message": f"IP {ip_address.ip} deleted successfully", "status": "success"} -@router.get( - "/get/internal_user_settings", - tags=["SSO Settings"], - dependencies=[Depends(user_api_key_auth)], -) -async def get_sso_settings(): +async def _get_settings_with_schema( + settings_key: str, + settings_class: Any, + config: dict, +) -> dict: """ - Get all SSO settings from the litellm_settings configuration. - Returns a structured object with values and descriptions for UI display. + Common utility function to get settings with schema information. + + Args: + settings_key: The key in litellm_settings to get + settings_class: The Pydantic class to use for schema + config: The config dictionary """ from pydantic import TypeAdapter - from litellm.proxy.proxy_server import proxy_config - - # Load existing config - config = await proxy_config.get_config() litellm_settings = config.get("litellm_settings", {}) or {} - default_internal_user_params = ( - litellm_settings.get("default_internal_user_params", {}) or {} - ) + settings_data = litellm_settings.get(settings_key, {}) or {} - # Create the settings object first - sso_settings = DefaultInternalUserParams(**(default_internal_user_params)) - # Get the schema for UISSOSettings - schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True) + # Create the settings object + settings = settings_class(**(settings_data)) + # Get the schema + schema = TypeAdapter(settings_class).json_schema(by_alias=True) # Convert to dict for response - settings_dict = sso_settings.model_dump() + settings_dict = settings.model_dump() # Add descriptions to the response result = { @@ -166,6 +164,89 @@ async def get_sso_settings(): return result +@router.get( + "/get/internal_user_settings", + tags=["SSO Settings"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_sso_settings(): + """ + Get all SSO settings from the litellm_settings configuration. + Returns a structured object with values and descriptions for UI display. + """ + from litellm.proxy.proxy_server import proxy_config + + # Load existing config + config = await proxy_config.get_config() + + return await _get_settings_with_schema( + settings_key="default_internal_user_params", + settings_class=DefaultInternalUserParams, + config=config, + ) + + +@router.get( + "/get/default_team_settings", + tags=["SSO Settings"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_default_team_settings(): + """ + Get all SSO settings from the litellm_settings configuration. + Returns a structured object with values and descriptions for UI display. + """ + from litellm.proxy.proxy_server import proxy_config + + # Load existing config + config = await proxy_config.get_config() + + return await _get_settings_with_schema( + settings_key="default_team_params", + settings_class=DefaultTeamSSOParams, + config=config, + ) + + +async def _update_litellm_setting( + settings: Union[DefaultInternalUserParams, DefaultTeamSSOParams], + settings_key: str, + in_memory_var: Any, + success_message: str, +): + """ + Common utility function to update `litellm_settings` in both memory and config. + + Args: + settings: The settings object to update + settings_key: The key in litellm_settings to update + in_memory_var: The in-memory variable to update + success_message: Message to return on success + """ + from litellm.proxy.proxy_server import proxy_config + + # Update the in-memory settings + in_memory_var = settings.model_dump(exclude_none=True) + + # Load existing config + config = await proxy_config.get_config() + + # Update config with new settings + if "litellm_settings" not in config: + config["litellm_settings"] = {} + + config["litellm_settings"][settings_key] = settings.model_dump(exclude_none=True) + + # Save the updated config + await proxy_config.save_config(new_config=config) + + return { + "message": success_message, + "status": "success", + "settings": in_memory_var, + } + + @router.patch( "/update/internal_user_settings", tags=["SSO Settings"], @@ -176,27 +257,27 @@ async def update_internal_user_settings(settings: DefaultInternalUserParams): Update the default internal user parameters for SSO users. These settings will be applied to new users who sign in via SSO. """ - from litellm.proxy.proxy_server import proxy_config - - # Update the in-memory settings - litellm.default_internal_user_params = settings.model_dump(exclude_none=True) - - # Load existing config - config = await proxy_config.get_config() - - # Update config with new settings - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - config["litellm_settings"]["default_internal_user_params"] = settings.model_dump( - exclude_none=True + return await _update_litellm_setting( + settings=settings, + settings_key="default_internal_user_params", + in_memory_var=litellm.default_internal_user_params, + success_message="Internal user settings updated successfully", ) - # Save the updated config - await proxy_config.save_config(new_config=config) - return { - "message": "Internal user settings updated successfully", - "status": "success", - "settings": litellm.default_internal_user_params, - } +@router.patch( + "/update/default_team_settings", + tags=["SSO Settings"], + dependencies=[Depends(user_api_key_auth)], +) +async def update_default_team_settings(settings: DefaultTeamSSOParams): + """ + Update the default team parameters for SSO users. + These settings will be applied to new teams created from SSO. + """ + return await _update_litellm_setting( + settings=settings, + settings_key="default_team_params", + in_memory_var=litellm.default_team_params, + success_message="Default team settings updated successfully", + ) diff --git a/litellm/types/proxy/management_endpoints/ui_sso.py b/litellm/types/proxy/management_endpoints/ui_sso.py index ca17c47006a..0e6f8739fa7 100644 --- a/litellm/types/proxy/management_endpoints/ui_sso.py +++ b/litellm/types/proxy/management_endpoints/ui_sso.py @@ -1,4 +1,8 @@ -from typing import List, Optional, TypedDict +from typing import List, Literal, Optional, TypedDict + +from pydantic import Field + +from litellm.proxy._types import LiteLLMPydanticObjectBase, LitellmUserRoles class MicrosoftGraphAPIUserGroupDirectoryObject(TypedDict, total=False): @@ -25,3 +29,30 @@ class MicrosoftServicePrincipalTeam(TypedDict, total=False): principalDisplayName: Optional[str] principalId: Optional[str] + + +class DefaultTeamSSOParams(LiteLLMPydanticObjectBase): + """ + Default parameters to apply when a new team is automatically created by LiteLLM via SSO Groups + """ + + models: List[str] = Field( + default=[], + description="Default list of models that new automatically created teams can access", + ) + max_budget: Optional[float] = Field( + default=None, + description="Default maximum budget (in USD) for new automatically created teams", + ) + budget_duration: Optional[str] = Field( + default=None, + description="Default budget duration for new automatically created teams (e.g. 'daily', 'weekly', 'monthly')", + ) + tpm_limit: Optional[int] = Field( + default=None, + description="Default tpm limit for new automatically created teams", + ) + rpm_limit: Optional[int] = Field( + default=None, + description="Default rpm limit for new automatically created teams", + ) diff --git a/tests/litellm/proxy/management_endpoints/test_ui_sso.py b/tests/litellm/proxy/management_endpoints/test_ui_sso.py index 6683c2cc914..d266cd0f613 100644 --- a/tests/litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/litellm/proxy/management_endpoints/test_ui_sso.py @@ -19,6 +19,7 @@ from litellm.proxy._types import NewTeamRequest from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.management_endpoints.types import CustomOpenID from litellm.proxy.management_endpoints.ui_sso import ( + DefaultTeamSSOParams, GoogleSSOHandler, MicrosoftSSOHandler, SSOAuthenticationHandler, @@ -421,8 +422,10 @@ def test_get_group_ids_from_graph_api_response(): @pytest.mark.parametrize( "team_params", [ - # Test case 1: Using NewTeamRequest - NewTeamRequest(max_budget=10, budget_duration="1d", models=["special-gpt-5"]), + # Test case 1: Using DefaultTeamSSOParams + DefaultTeamSSOParams( + max_budget=10, budget_duration="1d", models=["special-gpt-5"] + ), # Test case 2: Using Dict {"max_budget": 10, "budget_duration": "1d", "models": ["special-gpt-5"]}, ], diff --git a/tests/litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index e2c37048d3b..0b29ef6c7a6 100644 --- a/tests/litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -11,6 +11,7 @@ sys.path.insert( from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles from litellm.proxy.proxy_server import app +from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams client = TestClient(app) @@ -25,7 +26,14 @@ def mock_proxy_config(monkeypatch): "max_budget": 100.0, "budget_duration": "30d", "models": ["gpt-3.5-turbo", "gpt-4"], - } + }, + "default_team_params": { + "models": ["gpt-3.5-turbo"], + "max_budget": 50.0, + "budget_duration": "14d", + "tpm_limit": 100, + "rpm_limit": 10, + }, } } @@ -138,3 +146,76 @@ class TestProxySettingEndpoints: # Verify save_config was called exactly once assert mock_proxy_config["save_call_count"]() == 1 + + def test_get_default_team_settings(self, mock_proxy_config, mock_auth): + """Test getting the default team settings""" + response = client.get("/get/default_team_settings") + + assert response.status_code == 200 + data = response.json() + + # Check structure of response + assert "values" in data + assert "schema" in data + + # Check values match our mock config + values = data["values"] + mock_params = mock_proxy_config["config"]["litellm_settings"][ + "default_team_params" + ] + assert values["models"] == mock_params["models"] + assert values["max_budget"] == mock_params["max_budget"] + assert values["budget_duration"] == mock_params["budget_duration"] + assert values["tpm_limit"] == mock_params["tpm_limit"] + assert values["rpm_limit"] == mock_params["rpm_limit"] + + # Check schema contains descriptions + assert "properties" in data["schema"] + assert "models" in data["schema"]["properties"] + assert "description" in data["schema"]["properties"]["models"] + + def test_update_default_team_settings( + self, mock_proxy_config, mock_auth, monkeypatch + ): + """Test updating the default team settings""" + # Mock litellm.default_team_params + import litellm + + monkeypatch.setattr(litellm, "default_team_params", {}) + + # New settings to update + new_settings = { + "models": ["gpt-4", "claude-3"], + "max_budget": 150.0, + "budget_duration": "30d", + "tpm_limit": 200, + "rpm_limit": 20, + } + + response = client.patch("/update/default_team_settings", json=new_settings) + + assert response.status_code == 200 + data = response.json() + + # Check response structure + assert data["status"] == "success" + assert "settings" in data + + # Verify settings were updated + settings = data["settings"] + assert settings["models"] == new_settings["models"] + assert settings["max_budget"] == new_settings["max_budget"] + assert settings["budget_duration"] == new_settings["budget_duration"] + assert settings["tpm_limit"] == new_settings["tpm_limit"] + assert settings["rpm_limit"] == new_settings["rpm_limit"] + + # Verify the config was updated + updated_config = mock_proxy_config["config"]["litellm_settings"][ + "default_team_params" + ] + assert updated_config["models"] == new_settings["models"] + assert updated_config["max_budget"] == new_settings["max_budget"] + assert updated_config["tpm_limit"] == new_settings["tpm_limit"] + + # Verify save_config was called exactly once + assert mock_proxy_config["save_call_count"]() == 1 diff --git a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx new file mode 100644 index 00000000000..4ee4aae5650 --- /dev/null +++ b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx @@ -0,0 +1,307 @@ +import React, { useState, useEffect } from "react"; +import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react"; +import { Typography, Spin, message, Switch, Select, Form } from "antd"; +import { getDefaultTeamSettings, updateDefaultTeamSettings, modelAvailableCall } from "./networking"; +import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_components/budget_duration_dropdown"; +import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; + +interface TeamSSOSettingsProps { + accessToken: string | null; + userID: string; + userRole: string; +} + +const TeamSSOSettings: React.FC = ({ accessToken, userID, userRole }) => { + const [loading, setLoading] = useState(true); + const [settings, setSettings] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [editedValues, setEditedValues] = useState({}); + const [saving, setSaving] = useState(false); + const [availableModels, setAvailableModels] = useState([]); + const { Paragraph } = Typography; + const { Option } = Select; + + useEffect(() => { + const fetchTeamSSOSettings = async () => { + if (!accessToken) { + setLoading(false); + return; + } + + try { + const data = await getDefaultTeamSettings(accessToken); + setSettings(data); + setEditedValues(data.values || {}); + + // Fetch available models + if (accessToken) { + try { + const modelResponse = await modelAvailableCall(accessToken, userID, userRole); + if (modelResponse && modelResponse.data) { + const modelNames = modelResponse.data.map((model: { id: string }) => model.id); + setAvailableModels(modelNames); + } + } catch (error) { + console.error("Error fetching available models:", error); + } + } + } catch (error) { + console.error("Error fetching team SSO settings:", error); + message.error("Failed to fetch team settings"); + } finally { + setLoading(false); + } + }; + + fetchTeamSSOSettings(); + }, [accessToken]); + + const handleSaveSettings = async () => { + if (!accessToken) return; + + setSaving(true); + try { + const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues); + setSettings({...settings, values: updatedSettings.settings}); + setIsEditing(false); + message.success("Default team settings updated successfully"); + } catch (error) { + console.error("Error updating team settings:", error); + message.error("Failed to update team settings"); + } finally { + setSaving(false); + } + }; + + const handleTextInputChange = (key: string, value: any) => { + setEditedValues((prev: Record) => ({ + ...prev, + [key]: value + })); + }; + + const renderEditableField = (key: string, property: any, value: any) => { + const type = property.type; + + if (key === "budget_duration") { + return ( + handleTextInputChange(key, value)} + className="mt-2" + /> + ); + } else if (type === "boolean") { + return ( +
+ handleTextInputChange(key, checked)} + /> +
+ ); + } else if (type === "array" && property.items?.enum) { + return ( + + ); + } else if (key === "models") { + return ( + + ); + } else if (type === "string" && property.enum) { + return ( + + ); + } else { + return ( + handleTextInputChange(key, e.target.value)} + placeholder={property.description || ""} + className="mt-2" + /> + ); + } + }; + + const renderValue = (key: string, value: any): JSX.Element => { + if (value === null || value === undefined) return Not set; + + if (key === "budget_duration") { + return {getBudgetDurationLabel(value)}; + } + + if (typeof value === "boolean") { + return {value ? "Enabled" : "Disabled"}; + } + + if (key === "models" && Array.isArray(value)) { + if (value.length === 0) return None; + + return ( +
+ {value.map((model, index) => ( + + {getModelDisplayName(model)} + + ))} +
+ ); + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + if (value.length === 0) return None; + + return ( +
+ {value.map((item, index) => ( + + {typeof item === "object" ? JSON.stringify(item) : String(item)} + + ))} +
+ ); + } + + return ( +
+          {JSON.stringify(value, null, 2)}
+        
+ ); + } + + return {String(value)}; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (!settings) { + return ( + + No team settings available or you do not have permission to view them. + + ); + } + + // Dynamically render settings based on the schema + const renderSettings = () => { + const { values, schema } = settings; + + if (!schema || !schema.properties) { + return No schema information available; + } + + return Object.entries(schema.properties).map(([key, property]: [string, any]) => { + const value = values[key]; + const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + + return ( +
+ {displayName} + + {property.description || "No description available"} + + + {isEditing ? ( +
+ {renderEditableField(key, property, value)} +
+ ) : ( +
+ {renderValue(key, value)} +
+ )} +
+ ); + }); + }; + + return ( + +
+ Default Team Settings + {!loading && settings && ( + isEditing ? ( +
+ + +
+ ) : ( + + ) + )} +
+ + + These settings will be applied by default when creating new teams. + + + {settings?.schema?.description && ( + {settings.schema.description} + )} + + +
+ {renderSettings()} +
+
+ ); +}; + +export default TeamSSOSettings; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index f41222f3575..9fbff17aa50 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -4344,4 +4344,71 @@ export const tagDeleteCall = async ( console.error("Error deleting tag:", error); throw error; } +}; + +export const getDefaultTeamSettings = async (accessToken: string) => { + try { + // Construct base URL + let url = proxyBaseUrl + ? `${proxyBaseUrl}/get/default_team_settings` + : `/get/default_team_settings`; + + console.log("Fetching default team settings from:", url); + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Fetched default team settings:", data); + return data; + } catch (error) { + console.error("Failed to fetch default team settings:", error); + throw error; + } +}; + + +export const updateDefaultTeamSettings = async (accessToken: string, settings: Record) => { + try { + // Construct base URL + let url = proxyBaseUrl + ? `${proxyBaseUrl}/update/default_team_settings` + : `/update/default_team_settings`; + + console.log("Updating default team settings:", settings); + + const response = await fetch(url, { + method: "PATCH", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Updated default team settings:", data); + message.success("Default team settings updated successfully"); + return data; + } catch (error) { + console.error("Failed to update default team settings:", error); + throw error; + } }; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 7e3b607267c..1ce94573f35 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -27,6 +27,8 @@ import { Select, SelectItem } from "@tremor/react"; import { InfoCircleOutlined } from '@ant-design/icons'; import { getGuardrailsList } from "./networking"; import TeamInfoView from "@/components/team/team_info"; +import TeamSSOSettings from "@/components/TeamSSOSettings"; +import { isAdminRole } from "@/utils/roles"; import { Table, TableBody, @@ -354,6 +356,7 @@ const Teams: React.FC = ({
Your Teams Available Teams + {isAdminRole(userRole || "") && Default Team Settings}
{lastRefreshed && Last Refreshed: {lastRefreshed}} @@ -797,6 +800,15 @@ const Teams: React.FC = ({ userID={userID} /> + {isAdminRole(userRole || "") && ( + + + + )} )} diff --git a/ui/litellm-dashboard/src/utils/roles.ts b/ui/litellm-dashboard/src/utils/roles.ts index a50f06697b6..0bf928a6cc2 100644 --- a/ui/litellm-dashboard/src/utils/roles.ts +++ b/ui/litellm-dashboard/src/utils/roles.ts @@ -5,4 +5,10 @@ export const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names]; export const internalUserRoles = ["Internal User", "Internal Viewer"]; export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"]; -export const rolesWithWriteAccess = ["Internal User", "Admin"]; \ No newline at end of file +export const rolesWithWriteAccess = ["Internal User", "Admin"]; + +// Helper function to check if a role is in all_admin_roles +export const isAdminRole = (role: string): boolean => { + return all_admin_roles.includes(role); +}; +