diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5c0fd36e28ace78c9606115349710516527660d6..da74d6a8241d78244d04d9766f967d5d2a409c74 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,10 +23,12 @@ stages:
 
 .templates:
   adjust-image-names-haz: &adjust-image-names-haz |-
+    DOCKERFILE="Dockerfile";
     IMAGE_NAME=${IMAGE_NAME_HAZ/:master/:latest};
-    INTERNAL_IMAGE_NAME=${CI_REGISTRY_IMAGE}/${IMAGE_NAME_WS};
+    INTERNAL_IMAGE_NAME=${CI_REGISTRY_IMAGE}/${IMAGE_NAME_HAZ};
   adjust-image-names-ws: &adjust-image-names-ws |-
-    IMAGE_NAME=${IMAGE_NAME_HAZ/:master/:latest};
+    DOCKERFILE="ws.Dockerfile";
+    IMAGE_NAME=${IMAGE_NAME_WS/:master/:latest};
     INTERNAL_IMAGE_NAME=${CI_REGISTRY_IMAGE}/${IMAGE_NAME_WS};
   ssh-key: &ssh-key |-
     eval $(ssh-agent -s);
@@ -50,7 +52,11 @@ stages:
     - apk add git;
     - *ssh-key
     - mkdir ${DOCKER_DIR}
-    - docker build --build-arg ssh_private_key="${SSH_PRIVATE_KEY}" -t local/${IMAGE_NAME} .
+    - |
+      docker build \
+        --build-arg ssh_private_key="${SSH_PRIVATE_KEY}" \
+        -f ${DOCKERFILE} \
+        -t local/${IMAGE_NAME} .
     - docker save local/${IMAGE_NAME} > ${DOCKER_TAR}
   artifacts:
     paths:
@@ -64,7 +70,9 @@ stages:
     - master@ghsc/nshmp/nshmp-haz-v2
     - tags@ghsc/nshmp/nshmp-haz-v2
   script:
-    - echo "${CHS_PASSWORD}" | docker login --username ${CHS_USERNAME} --password-stdin ${CODE_REGISTRY}
+    - |
+      echo "${CHS_PASSWORD}" | \
+          docker login --username ${CHS_USERNAME} --password-stdin ${CODE_REGISTRY}
     - docker load -i ${DOCKER_TAR}
     - docker tag local/${IMAGE_NAME} ${INTERNAL_IMAGE_NAME}
     - docker push ${INTERNAL_IMAGE_NAME}
@@ -186,15 +194,11 @@ Build Haz Image:
   extends: .docker-build
   before_script:
     - *adjust-image-names-haz
-  variables:
-    IMAGE_NAME: ${IMAGE_NAME_HAZ}
 
 Build WS Image:
   extends: .docker-build
   before_script:
     - *adjust-image-names-ws
-  variables:
-    IMAGE_NAME: ${IMAGE_NAME_WS}
 
 ####
 # Stage: Publish
diff --git a/scripts/docker-entrypoint.ws.sh b/scripts/docker-entrypoint.ws.sh
index d90d2ce27aacceb5b16a409b90b7f61a0afeb68e..e7e99aaf0639001c91fe92d01dc49b1e8cd39d7a 100644
--- a/scripts/docker-entrypoint.ws.sh
+++ b/scripts/docker-entrypoint.ws.sh
@@ -6,14 +6,14 @@ exit_status=${?};
 [ "${exit_status}" -eq 0 ] || exit "${exit_status}";
 
 # Download models to use
-get_models "${MODEL}" "${NSHM_VERSION}";
+model_path=$(get_models "${MODEL}" "${NSHM_VERSION}");
 exit_status=${?};
 check_exit_status ${exit_status};
 
 # Run web services
 java -jar "${PROJECT}-ws.jar" \
     "-Dmicronaut.server.context-path=${CONTEXT_PATH}" \
-    --model="${MODEL}";
+    --models="${model_path}";
 exit_status=${?};
 check_exit_status ${exit_status};
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/BaseModel.java b/src/main/java/gov/usgs/earthquake/nshmp/www/BaseModel.java
deleted file mode 100644
index 5db87cbbb8431825e75c4a4c1ae44b2197bd7c2a..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/BaseModel.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_1150;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_180;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_2000;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_259;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_360;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_537;
-import static gov.usgs.earthquake.nshmp.calc.Vs30.VS_760;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.PGA;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P1;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P2;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P3;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P5;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA0P75;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA1P0;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA2P0;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA3P0;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA4P0;
-import static gov.usgs.earthquake.nshmp.gmm.Imt.SA5P0;
-
-import java.nio.file.Paths;
-import java.util.EnumSet;
-import java.util.Set;
-
-import com.google.common.collect.Sets;
-
-import gov.usgs.earthquake.nshmp.calc.Vs30;
-import gov.usgs.earthquake.nshmp.gmm.Imt;
-
-public enum BaseModel {
-
-  AK_2007(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA1P0, SA2P0),
-      EnumSet.of(VS_760)),
-
-  CEUS_2008(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA1P0, SA2P0),
-      EnumSet.of(VS_760, VS_2000)),
-
-  WUS_2008(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180)),
-
-  CEUS_2014(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA1P0, SA2P0),
-      EnumSet.of(VS_760, VS_2000)),
-
-  WUS_2014(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180)),
-
-  WUS_2014B(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0, SA4P0, SA5P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180)),
-
-  CEUS_2018(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0, SA4P0, SA5P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180)),
-
-  WUS_2018(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0, SA4P0, SA5P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180)),
-
-  HI_2020(
-      EnumSet.of(PGA, SA0P1, SA0P2, SA0P3, SA0P5, SA0P75, SA1P0, SA2P0, SA3P0, SA5P0),
-      EnumSet.of(VS_1150, VS_760, VS_537, VS_360, VS_259, VS_180));
-
-  private static final String MODEL_DIR = "models";
-
-  public final Set<Imt> imts;
-  public final Set<Vs30> vs30s;
-
-  public final String path;
-  public final String year;
-
-  private BaseModel(Set<Imt> imts, Set<Vs30> vs30s) {
-    this.imts = Sets.immutableEnumSet(imts);
-    this.vs30s = Sets.immutableEnumSet(vs30s);
-    var region = deriveRegion(name());
-    year = name().substring(name().lastIndexOf('_') + 1);
-    path = Paths.get(MODEL_DIR)
-        .resolve(region.toLowerCase())
-        .resolve(year.toLowerCase())
-        .toString();
-  }
-
-  private static String deriveRegion(String s) {
-    return s.startsWith("AK") ? "AK" : s.startsWith("WUS") ? "WUS"
-        : s.startsWith("HI") ? "HI" : "CEUS";
-  }
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/Model.java b/src/main/java/gov/usgs/earthquake/nshmp/www/Model.java
deleted file mode 100644
index d5ad0d9e9272d37b9cd31d55858dfd4d8ed32368..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/Model.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import java.util.Set;
-
-import gov.usgs.earthquake.nshmp.www.meta.Region;
-
-public enum Model {
-
-  AK_2007(Set.of(BaseModel.AK_2007)),
-
-  CONUS_2008(Set.of(BaseModel.CEUS_2008, BaseModel.WUS_2008)),
-
-  CONUS_2014(Set.of(BaseModel.CEUS_2014, BaseModel.WUS_2014)),
-
-  CONUS_2014B(Set.of(BaseModel.CEUS_2014, BaseModel.WUS_2014B)),
-
-  CONUS_2018(Set.of(BaseModel.CEUS_2018, BaseModel.WUS_2018)),
-
-  HI_2020(Set.of(BaseModel.HI_2020));
-
-  private final String label;
-  private final String year;
-  private final Set<BaseModel> models;
-  private final Region region;
-
-  private Model(Set<BaseModel> models) {
-    year = name().substring(name().lastIndexOf("_") + 1);
-    region = deriveRegion(name());
-    label = String.format("%s %s Hazard Model", year, region.label);
-    this.models = models;
-  }
-
-  public String label() {
-    return label;
-  }
-
-  public Set<BaseModel> models() {
-    return Set.copyOf(models);
-  }
-
-  public Region region() {
-    return region;
-  }
-
-  public String year() {
-    return year;
-  }
-
-  private Region deriveRegion(String region) {
-    return region.startsWith("AK") ? Region.AK
-        : region.startsWith("HI") ? Region.HI : Region.CONUS;
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/MetaUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/MetaUtil.java
index fa3f6f87f1dae3647fbc7fe662f22b59d78ebd6f..d5b961201f5bc96f1c8f3814db20581756a669c9 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/MetaUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/MetaUtil.java
@@ -97,6 +97,10 @@ public final class MetaUtil {
   public static final class DoubleSerializer implements JsonSerializer<Double> {
     @Override
     public JsonElement serialize(Double d, Type type, JsonSerializationContext context) {
+      if (Double.isNaN(d)) {
+        return null;
+      }
+
       double dOut = Double.valueOf(String.format("%.8g", d));
       return new JsonPrimitive(dOut);
     }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java
index 1036e20e12320af73e4830aa34ff4da55e72fef0..9722ce1189313ce4e591df33987cad63f296f2f0 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java
@@ -10,7 +10,6 @@ import java.util.List;
 import java.util.Properties;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
-import java.util.logging.Logger;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.Resources;
@@ -19,13 +18,13 @@ import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Deaggregation;
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.Vs30;
+import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
 import gov.usgs.earthquake.nshmp.internal.www.Response;
 import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.DeaggEpsilonController;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
@@ -45,7 +44,6 @@ public final class DeaggEpsilonService {
   /* Developer notes: See HazardService. */
 
   private static final String NAME = "Epsilon Deaggregation";
-  private static final Logger LOGGER = Logger.getLogger(DeaggEpsilonService.class.getName());
   private static URL basinUrl;
 
   public static void init() {
@@ -77,7 +75,6 @@ public final class DeaggEpsilonService {
   public static HttpResponse<String> handleDoGetDeaggEpsilon(Query query, UrlHelper urlHelper) {
     try {
       var timer = ServletUtil.timer();
-      LOGGER.info(NAME + " - Request:\n" + ServletUtil.GSON.toJson(query));
 
       if (query.isNull()) {
         return HazardService.handleDoGetUsage(urlHelper);
@@ -87,10 +84,9 @@ public final class DeaggEpsilonService {
       var data = new RequestData(query, Vs30.fromValue(query.vs30));
       var response = process(data, timer, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, urlHelper);
     }
   }
 
@@ -155,12 +151,10 @@ public final class DeaggEpsilonService {
     }
   }
 
-  static class ConfigFunction implements Function<BaseModel, CalcConfig> {
+  static class ConfigFunction implements Function<HazardModel, CalcConfig> {
     @Override
-    public CalcConfig apply(BaseModel baseModel) {
-      var hazardModel = ServletUtil.hazardModels().get(baseModel);
-      var configBuilder = CalcConfig.copyOf(hazardModel.config());
-      configBuilder.imts(baseModel.imts);
+    public CalcConfig apply(HazardModel model) {
+      var configBuilder = CalcConfig.copyOf(model.config());
       return configBuilder.build();
     }
   }
@@ -206,7 +200,7 @@ public final class DeaggEpsilonService {
 
   @SuppressWarnings("unused")
   private static final class ResponseMetadata {
-    final SourceModel model;
+    final List<SourceModel> models;
     final double longitude;
     final double latitude;
     final String imt;
@@ -218,7 +212,7 @@ public final class DeaggEpsilonService {
     final Object εbins;
 
     ResponseMetadata(Deaggregation deagg, RequestData request, Imt imt) {
-      this.model = request.model;
+      this.models = request.models;
       this.longitude = request.longitude;
       this.latitude = request.latitude;
       this.imt = imt.toString();
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
index 4039651427c294977b67c1a5ba36d224bdc90e52..8b36bc0bd977c3d5e13f53c7b57257cab14d07a3 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
@@ -9,7 +9,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
-import java.util.logging.Logger;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Hazard;
@@ -17,6 +16,7 @@ import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.Vs30;
 import gov.usgs.earthquake.nshmp.data.MutableXySequence;
 import gov.usgs.earthquake.nshmp.data.XySequence;
+import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.eq.model.SourceType;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
@@ -24,7 +24,6 @@ import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
 import gov.usgs.earthquake.nshmp.internal.www.Response;
 import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.HazardController;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
@@ -51,7 +50,6 @@ public final class HazardService {
    */
 
   private static final String NAME = "Hazard Service";
-  private static final Logger LOGGER = Logger.getLogger(HazardService.class.getName());
 
   /**
    * Handler for {@link HazardController#doGetUsage}. Returns the usage for the
@@ -61,14 +59,12 @@ public final class HazardService {
    */
   public static HttpResponse<String> handleDoGetUsage(UrlHelper urlHelper) {
     try {
-      LOGGER.info(NAME + " - Request:\n" + urlHelper.url);
       var usage = new SourceServices.ResponseData();
       var response = new Response<>(Status.USAGE, NAME, urlHelper.url, usage, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, urlHelper);
     }
   }
 
@@ -82,7 +78,6 @@ public final class HazardService {
   public static HttpResponse<String> handleDoGetHazard(Query query, UrlHelper urlHelper) {
     try {
       var timer = ServletUtil.timer();
-      LOGGER.info(NAME + " - Request:\n" + ServletUtil.GSON.toJson(query));
 
       if (query.isNull()) {
         return handleDoGetUsage(urlHelper);
@@ -92,10 +87,9 @@ public final class HazardService {
       var data = new RequestData(query, Vs30.fromValue(query.vs30));
       var response = process(data, timer, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, urlHelper);
     }
   }
 
@@ -115,12 +109,10 @@ public final class HazardService {
         .build();
   }
 
-  static class ConfigFunction implements Function<BaseModel, CalcConfig> {
+  static class ConfigFunction implements Function<HazardModel, CalcConfig> {
     @Override
-    public CalcConfig apply(BaseModel baseModel) {
-      var hazardModel = ServletUtil.hazardModels().get(baseModel);
-      var configBuilder = CalcConfig.copyOf(hazardModel.config());
-      configBuilder.imts(baseModel.imts);
+    public CalcConfig apply(HazardModel model) {
+      var configBuilder = CalcConfig.copyOf(model.config());
       return configBuilder.build();
     }
   }
@@ -173,7 +165,7 @@ public final class HazardService {
 
   @SuppressWarnings("unused")
   private static final class ResponseMetadata {
-    final SourceModel model;
+    final List<SourceModel> models;
     final double latitude;
     final double longitude;
     final Imt imt;
@@ -182,7 +174,7 @@ public final class HazardService {
     final String ylabel = "Annual Frequency of Exceedence";
 
     ResponseMetadata(RequestData request, Imt imt) {
-      model = new SourceModel(ServletUtil.installedModel());
+      models = SourceModel.getList();
       latitude = request.latitude;
       longitude = request.longitude;
       this.imt = imt;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
index d88510f5baf37aa7d3ddd9c55f065365e680ee6c..ada84547e6c40a6e5be1d0173e54f341d7a3e8aa 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
@@ -4,7 +4,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
-import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -48,7 +47,6 @@ public final class RateService {
    * WUS models.
    */
 
-  private static final Logger LOGGER = Logger.getLogger(RateService.class.getName());
   private static final String TOTAL_KEY = "Total";
 
   /**
@@ -64,7 +62,7 @@ public final class RateService {
       var svcResponse = ServletUtil.GSON.toJson(response);
       return HttpResponse.ok(String.format(svcResponse, urlHelper.urlPrefix, urlHelper.urlPrefix));
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, urlHelper);
     }
   }
 
@@ -83,7 +81,6 @@ public final class RateService {
 
     try {
       var timer = ServletUtil.timer();
-      LOGGER.info(service.name + " - Request:\n" + ServletUtil.GSON.toJson(query));
 
       if (query.isNull()) {
         return handleDoGetUsage(service, urlHelper);
@@ -93,10 +90,9 @@ public final class RateService {
       var requestData = new RequestData(query);
       var response = processRequest(service, requestData, urlHelper, timer);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(service.name + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, urlHelper);
     }
   }
 
@@ -128,8 +124,8 @@ public final class RateService {
      * probability service has been called.
      */
 
-    for (var entry : ServletUtil.hazardModels().entrySet()) {
-      var rate = process(service, entry.getValue(), site, data.distance, data.timespan);
+    for (var model : ServletUtil.hazardModels()) {
+      var rate = process(service, model, site, data.distance, data.timespan);
       futureRates.add(rate);
     }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
index f3ef2cb6e46c2893e78789beecd021866731255d..dcc95dd1e77ba202a4de848f88d0ce7ca92cce23 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
@@ -1,9 +1,9 @@
 package gov.usgs.earthquake.nshmp.www.services;
 
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
-import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
 import com.google.gson.GsonBuilder;
@@ -12,11 +12,11 @@ import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Hazard;
 import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
 import gov.usgs.earthquake.nshmp.calc.Site;
+import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
 import gov.usgs.earthquake.nshmp.internal.www.Response;
 import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
 
 import io.micronaut.http.HttpResponse;
@@ -26,25 +26,23 @@ class ServicesUtil {
   static HttpResponse<String> handleError(
       Throwable e,
       String name,
-      Logger logger,
       UrlHelper urlHelper) {
     var msg = e.getMessage() + " (see logs)";
     var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper);
     var gson = new GsonBuilder().setPrettyPrinting().create();
     var response = gson.toJson(svcResponse);
-    logger.severe(name + " -\n" + response);
     e.printStackTrace();
     return HttpResponse.serverError(response);
   }
 
   static Hazard calcHazard(
-      Function<BaseModel, CalcConfig> configFunction,
+      Function<HazardModel, CalcConfig> configFunction,
       Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException {
-    var futuresList = ServletUtil.installedModel().models().stream()
-        .map(baseModel -> {
-          var config = configFunction.apply(baseModel);
+    var futuresList = ServletUtil.hazardModels().stream()
+        .map(model -> {
+          var config = configFunction.apply(model);
           var site = siteFunction.apply(config);
-          return calcHazard(baseModel, config, site);
+          return calcHazard(model, config, site);
         })
         .collect(Collectors.toList());
 
@@ -82,12 +80,12 @@ class ServicesUtil {
   }
 
   static class ServiceRequestData {
-    public final SourceModel model;
+    public final List<SourceModel> models;
     public final double longitude;
     public final double latitude;
 
     public ServiceRequestData(ServiceQueryData query) {
-      model = new SourceModel(ServletUtil.installedModel());
+      models = SourceModel.getList();
       longitude = query.longitude;
       latitude = query.latitude;
     }
@@ -126,13 +124,13 @@ class ServicesUtil {
   }
 
   private static CompletableFuture<Hazard> calcHazard(
-      BaseModel baseModel,
+      HazardModel model,
       CalcConfig config,
       Site site) {
     return CompletableFuture
         .supplyAsync(
             () -> HazardCalcs.hazard(
-                ServletUtil.hazardModels().get(baseModel), config, site, ServletUtil.CALC_EXECUTOR),
+                model, config, site, ServletUtil.CALC_EXECUTOR),
             ServletUtil.TASK_EXECUTOR);
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
index e11d2e22e19cf7f4c0e74453c0d8be5c1d287fdf..aa0ad2b1c0d5c6dc74c640b3519275fc9a65d0e8 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
@@ -2,17 +2,23 @@ package gov.usgs.earthquake.nshmp.www.services;
 
 import static java.lang.Runtime.getRuntime;
 
+import java.io.IOException;
+import java.lang.reflect.Type;
 import java.net.URI;
 import java.net.URL;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystemNotFoundException;
 import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.time.format.DateTimeFormatter;
-import java.util.EnumMap;
+import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -21,6 +27,10 @@ import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
 
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.ValueFormat;
@@ -28,8 +38,6 @@ import gov.usgs.earthquake.nshmp.calc.Vs30;
 import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
-import gov.usgs.earthquake.nshmp.www.BaseModel;
-import gov.usgs.earthquake.nshmp.www.Model;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Region;
 
@@ -59,11 +67,11 @@ public class ServletUtil {
   static long hitCount = 0;
   static long missCount = 0;
 
-  @Value("${nshmp-haz.installed-model}")
-  private Model model;
+  @Value("${nshmp-haz.model-path}")
+  private Path modelPath;
 
-  private static Model INSTALLED_MODEL;
-  private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class);
+  private static List<HazardModel> HAZARD_MODELS = new ArrayList<>();
+  private static final String MODEL_INFO = "model-info.json";
 
   static {
     /* TODO modified for deagg-epsilon branch; should be context var */
@@ -78,18 +86,15 @@ public class ServletUtil {
         .registerTypeAdapter(Double.class, new MetaUtil.DoubleSerializer())
         .registerTypeAdapter(ParamType.class, new MetaUtil.ParamTypeSerializer())
         .registerTypeAdapter(Site.class, new MetaUtil.SiteSerializer())
+        .registerTypeHierarchyAdapter(Path.class, new PathConverter())
         .disableHtmlEscaping()
         .serializeNulls()
         .setPrettyPrinting()
         .create();
   }
 
-  static Model installedModel() {
-    return INSTALLED_MODEL;
-  }
-
-  static Map<BaseModel, HazardModel> hazardModels() {
-    return HAZARD_MODELS;
+  static List<HazardModel> hazardModels() {
+    return List.copyOf(HAZARD_MODELS);
   }
 
   @EventListener
@@ -100,17 +105,16 @@ public class ServletUtil {
 
   @EventListener
   void startup(StartupEvent event) {
-    INSTALLED_MODEL = model;
-
-    model.models().forEach(baseModel -> {
-      HAZARD_MODELS.put(baseModel, loadModel(baseModel));
-    });
-
-    HAZARD_MODELS = Map.copyOf(HAZARD_MODELS);
+    try {
+      var modelFinder = new ModelFinder();
+      Files.walkFileTree(modelPath, modelFinder);
+      modelFinder.paths().forEach(path -> HAZARD_MODELS.add(loadModel(path)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
-  private HazardModel loadModel(BaseModel model) {
-    Path path;
+  private HazardModel loadModel(Path path) {
     URL url;
     URI uri;
     String uriString;
@@ -118,7 +122,7 @@ public class ServletUtil {
     FileSystem fs;
 
     try {
-      url = Paths.get(model.path).toUri().toURL();
+      url = path.toUri().toURL();
       uri = new URI(url.toString().replace(" ", "%20"));
       uriString = uri.toString();
 
@@ -148,7 +152,6 @@ public class ServletUtil {
       }
 
       return HazardModel.load(path);
-
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
@@ -180,4 +183,38 @@ public class ServletUtil {
     }
   }
 
+  private static class ModelFinder extends SimpleFileVisitor<Path> {
+    private List<Path> paths;
+
+    ModelFinder() {
+      paths = new ArrayList<>();
+    }
+
+    List<Path> paths() {
+      return List.copyOf(paths);
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
+      var fileName = path.getFileName();
+
+      if (fileName != null && fileName.toString().equals(MODEL_INFO)) {
+        paths.add(path.getParent());
+      }
+
+      return FileVisitResult.CONTINUE;
+    }
+  }
+
+  private static class PathConverter implements JsonSerializer<Path> {
+
+    @Override
+    public JsonElement serialize(
+        Path path,
+        Type type,
+        JsonSerializationContext context) {
+      return new JsonPrimitive(path.toAbsolutePath().normalize().toString());
+    }
+  }
+
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
index 99c80fb53d9eb1befd88f6ed6f7bd379ededb303..25ad6451a0d28d21c9838a9ea213044653de9b34 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
@@ -4,7 +4,6 @@ import java.lang.reflect.Type;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
-import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
 import com.google.gson.Gson;
@@ -14,15 +13,16 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 
+import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Vs30;
+import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
+import gov.usgs.earthquake.nshmp.gmm.Gmm;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
 import gov.usgs.earthquake.nshmp.internal.www.Response;
 import gov.usgs.earthquake.nshmp.internal.www.meta.EnumParameter;
 import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.BaseModel;
-import gov.usgs.earthquake.nshmp.www.Model;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
@@ -44,8 +44,6 @@ public class SourceServices {
   private static final String SERVICE_DESCRIPTION =
       "Utilities for querying earthquake source models";
 
-  private static final Logger LOGGER = Logger.getLogger(SourceServices.class.getName());
-
   public static final Gson GSON;
 
   static {
@@ -62,14 +60,12 @@ public class SourceServices {
 
   public static HttpResponse<String> handleDoGetUsage(UrlHelper urlHelper) {
     try {
-      LOGGER.info(NAME + "- Request:\n" + urlHelper.url);
       var response = new Response<>(
           Status.USAGE, NAME, urlHelper.url, new ResponseData(), urlHelper);
       var jsonString = GSON.toJson(response);
-      LOGGER.info(NAME + "- Response:\n" + jsonString);
       return HttpResponse.ok(jsonString);
     } catch (Exception e) {
-      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, urlHelper);
     }
   }
 
@@ -90,14 +86,16 @@ public class SourceServices {
   }
 
   static class Parameters {
-    SourceModel model;
+    List<SourceModel> models;
     EnumParameter<Region> region;
     DoubleParameter returnPeriod;
     EnumParameter<Imt> imt;
     EnumParameter<Vs30> vs30;
 
     Parameters() {
-      model = new SourceModel(ServletUtil.installedModel());
+      models = ServletUtil.hazardModels().stream()
+          .map(SourceModel::new)
+          .collect(Collectors.toList());
 
       region = new EnumParameter<>(
           "Region",
@@ -109,72 +107,24 @@ public class SourceServices {
           ParamType.NUMBER,
           100.0,
           1e6);
-
-      imt = new EnumParameter<>(
-          "Intensity measure type",
-          ParamType.STRING,
-          modelUnionImts());
-
-      vs30 = new EnumParameter<>(
-          "Site soil (Vs30)",
-          ParamType.STRING,
-          modelUnionVs30s());
     }
   }
 
-  /* Union of IMTs across all models. */
-  static Set<Imt> modelUnionImts() {
-    return EnumSet.copyOf(ServletUtil.installedModel().models().stream()
-        .flatMap(model -> model.imts.stream())
-        .collect(Collectors.toSet()));
-  }
-
-  /* Union of Vs30s across all models. */
-  static Set<Vs30> modelUnionVs30s() {
-    return EnumSet.copyOf(ServletUtil.installedModel().models().stream()
-        .flatMap(model -> model.vs30s.stream())
-        .collect(Collectors.toSet()));
-  }
-
   public static class SourceModel {
-    String region;
     String display;
-    String value;
-    String year;
-    List<BaseSourceModel> models;
-
-    public SourceModel(Model model) {
-      this.display = model.label();
-      this.region = model.region().name();
-      this.value = model.toString();
-      this.year = model.year();
-      models = model.models().stream()
-          .map(m -> new BaseSourceModel(m))
-          .collect(Collectors.toList());
-    }
-  }
+    Set<Gmm> gmms;
+    CalcConfig config;
 
-  public static class BaseSourceModel {
-    String year;
-    String path;
-    ModelConstraints constraints;
-
-    BaseSourceModel(BaseModel model) {
-      year = model.year;
-      path = model.path;
-      constraints = new ModelConstraints(model);
+    private SourceModel(HazardModel model) {
+      display = model.name();
+      gmms = model.gmms();
+      config = model.config();
     }
-  }
 
-  private static class ModelConstraints {
-    final List<String> imt;
-    final List<String> vs30;
-
-    ModelConstraints(BaseModel model) {
-      this.imt = MetaUtil.enumsToNameList(model.imts);
-      this.vs30 = MetaUtil.enumsToStringList(
-          model.vs30s,
-          vs30 -> vs30.name().substring(3));
+    public static List<SourceModel> getList() {
+      return ServletUtil.hazardModels().stream()
+          .map(SourceModel::new)
+          .collect(Collectors.toList());
     }
   }
 
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9ca633b45994682ad1431edfb7a69d55647cebc1..b31875a05c370d469a7def95a5a80f703cf559b9 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -16,10 +16,7 @@ micronaut:
 
 nshmp-haz:
   ##
-  # The NSHM to use.
-  # Models must reside in the models directory in the root of the project.
+  # The path to the models.
   # To specify the model to use:
-  #     java -jar build/libs/nshmp-haz-v2-all.jar -model=<MODEL>
-  # See nshmp.www.Model for Model enums
-  # See build.gradle for more information on models directory
-  installed-model: ${model:CONUS_2018}
+  #     java -jar build/libs/nshmp-haz-v2-all.jar --models=<path/to/models>
+  model-path: ${models:models}