diff --git a/deprecated/fabric-models-v0/build.gradle b/deprecated/fabric-models-v0/build.gradle
new file mode 100644
index 000000000..c5841fda4
--- /dev/null
+++ b/deprecated/fabric-models-v0/build.gradle
@@ -0,0 +1,7 @@
+archivesBaseName = "fabric-models-v0"
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, [
+	'fabric-api-base',
+	'fabric-model-loading-api-v1'
+])
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
similarity index 89%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
index 3eea16ad1..d13bec8c8 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
@@ -23,8 +23,12 @@ import net.minecraft.client.render.model.BakedModelManager;
 import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.util.Identifier;
 
-import net.fabricmc.fabric.impl.client.model.BakedModelManagerHooks;
+import net.fabricmc.fabric.api.client.model.loading.v1.FabricBakedModelManager;
 
+/**
+ * @deprecated Use {@link FabricBakedModelManager#getModel(Identifier)} instead.
+ */
+@Deprecated
 public final class BakedModelManagerHelper {
 	/**
 	 * An alternative to {@link BakedModelManager#getModel(ModelIdentifier)} that accepts an
@@ -42,7 +46,7 @@ public final class BakedModelManagerHelper {
 	 */
 	@Nullable
 	public static BakedModel getModel(BakedModelManager manager, Identifier id) {
-		return ((BakedModelManagerHooks) manager).fabric_getModel(id);
+		return manager.getModel(id);
 	}
 
 	private BakedModelManagerHelper() { }
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
similarity index 90%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
index 829316653..0a6afcbed 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
@@ -22,6 +22,12 @@ import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.resource.ResourceManager;
 import net.minecraft.util.Identifier;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
+/**
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
+ */
+@Deprecated
 @FunctionalInterface
 public interface ExtraModelProvider {
 	/**
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
similarity index 100%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
index b0f041da0..8f72a5d04 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
@@ -21,8 +21,13 @@ import java.util.function.Function;
 import net.minecraft.resource.ResourceManager;
 import net.minecraft.util.Identifier;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
 import net.fabricmc.fabric.impl.client.model.ModelLoadingRegistryImpl;
 
+/**
+ * @deprecated Register a {@link ModelLoadingPlugin} instead.
+ */
+@Deprecated
 public interface ModelLoadingRegistry {
 	ModelLoadingRegistry INSTANCE = new ModelLoadingRegistryImpl();
 
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
similarity index 88%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
index 3aafc7014..96b097d28 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
@@ -20,9 +20,13 @@ import net.minecraft.client.render.model.UnbakedModel;
 import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.util.Identifier;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
 /**
  * The model loading context used during model providing.
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
  */
+@Deprecated
 public interface ModelProviderContext {
 	/**
 	 * Load a model using a {@link Identifier}, {@link ModelIdentifier}, ...
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
similarity index 83%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
index 1d7899927..41805578c 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
@@ -16,6 +16,12 @@
 
 package net.fabricmc.fabric.api.client.model;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
+/**
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
+ */
+@Deprecated
 public class ModelProviderException extends Exception {
 	public ModelProviderException(String s) {
 		super(s);
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
index b7e7eb60b..caf7b1a49 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
@@ -21,6 +21,8 @@ import org.jetbrains.annotations.Nullable;
 import net.minecraft.client.render.model.UnbakedModel;
 import net.minecraft.util.Identifier;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
 /**
  * Interface for model resource providers.
  *
@@ -40,7 +42,10 @@ import net.minecraft.util.Identifier;
  *
  * <ul><li>Only load files with a mod-suffixed name, such as .architect.obj,
  * <li>Only load files from an explicit list of namespaces, registered elsewhere.</ul>
+ *
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
  */
+@Deprecated
 @FunctionalInterface
 public interface ModelResourceProvider {
 	/**
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
index b2ab8e6c5..1fdf970b2 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
@@ -21,6 +21,8 @@ import org.jetbrains.annotations.Nullable;
 import net.minecraft.client.render.model.UnbakedModel;
 import net.minecraft.client.util.ModelIdentifier;
 
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
 /**
  * Interface for model variant providers.
  *
@@ -37,7 +39,10 @@ import net.minecraft.client.util.ModelIdentifier;
  *
  * <p>Keep in mind that only *one* ModelVariantProvider may respond to a given model
  * at any time.
+ *
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
  */
+@Deprecated
 @FunctionalInterface
 public interface ModelVariantProvider {
 	/**
diff --git a/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
new file mode 100644
index 000000000..6ab871a58
--- /dev/null
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.ExtraModelProvider;
+import net.fabricmc.fabric.api.client.model.ModelAppender;
+import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
+import net.fabricmc.fabric.api.client.model.ModelProviderContext;
+import net.fabricmc.fabric.api.client.model.ModelProviderException;
+import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
+import net.fabricmc.fabric.api.client.model.ModelVariantProvider;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderPluginContextImpl;
+
+public class ModelLoadingRegistryImpl implements ModelLoadingRegistry {
+	private final List<ExtraModelProvider> modelProviders = new ArrayList<>();
+	private final List<ModelAppender> modelAppenders = new ArrayList<>();
+	private final List<Function<ResourceManager, ModelResourceProvider>> resourceProviderSuppliers = new ArrayList<>();
+	private final List<Function<ResourceManager, ModelVariantProvider>> variantProviderSuppliers = new ArrayList<>();
+
+	{
+		// Grabs the resource manager to use it in the main model loading code.
+		// When using the v1 API, data should be loaded in parallel before model loading starts.
+		PreparableModelLoadingPlugin.register(
+				(resourceManager, executor) -> CompletableFuture.completedFuture(resourceManager),
+				this::onInitializeModelLoader);
+	}
+
+	private void onInitializeModelLoader(ResourceManager resourceManager, ModelLoadingPlugin.Context pluginContext) {
+		Consumer<Identifier> extraModelConsumer = pluginContext::addModels;
+		Consumer<ModelIdentifier> extraModelConsumer2 = pluginContext::addModels;
+		// A bit hacky, but avoids the allocation of a new context wrapper every time.
+		ModelProviderContext resourceProviderContext = ((ModelLoaderPluginContextImpl) pluginContext).modelGetter::apply;
+
+		for (ExtraModelProvider provider : modelProviders) {
+			provider.provideExtraModels(resourceManager, extraModelConsumer);
+		}
+
+		for (ModelAppender appender : modelAppenders) {
+			appender.appendAll(resourceManager, extraModelConsumer2);
+		}
+
+		for (Function<ResourceManager, ModelResourceProvider> supplier : resourceProviderSuppliers) {
+			ModelResourceProvider provider = supplier.apply(resourceManager);
+
+			pluginContext.resolveModel().register(resolverContext -> {
+				try {
+					return provider.loadModelResource(resolverContext.id(), resourceProviderContext);
+				} catch (ModelProviderException e) {
+					throw new RuntimeException(e);
+				}
+			});
+		}
+
+		for (Function<ResourceManager, ModelVariantProvider> supplier : variantProviderSuppliers) {
+			ModelVariantProvider provider = supplier.apply(resourceManager);
+			((ModelLoaderPluginContextImpl) pluginContext).legacyVariantProviders().register(modelId -> {
+				try {
+					return provider.loadModelVariant(modelId, resourceProviderContext);
+				} catch (ModelProviderException e) {
+					throw new RuntimeException(e);
+				}
+			});
+		}
+	}
+
+	@Override
+	public void registerModelProvider(ExtraModelProvider provider) {
+		modelProviders.add(provider);
+	}
+
+	@Override
+	public void registerAppender(ModelAppender appender) {
+		modelAppenders.add(appender);
+	}
+
+	@Override
+	public void registerResourceProvider(Function<ResourceManager, ModelResourceProvider> providerSupplier) {
+		resourceProviderSuppliers.add(providerSupplier);
+	}
+
+	@Override
+	public void registerVariantProvider(Function<ResourceManager, ModelVariantProvider> providerSupplier) {
+		variantProviderSuppliers.add(providerSupplier);
+	}
+}
diff --git a/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png b/deprecated/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
similarity index 100%
rename from fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
rename to deprecated/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
diff --git a/fabric-models-v0/src/client/resources/fabric.mod.json b/deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
similarity index 82%
rename from fabric-models-v0/src/client/resources/fabric.mod.json
rename to deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
index 93a4b284b..ced3a503e 100644
--- a/fabric-models-v0/src/client/resources/fabric.mod.json
+++ b/deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
@@ -17,13 +17,11 @@
   ],
   "depends": {
     "fabricloader": ">=0.4.0",
-    "fabric-api-base": "*"
+    "fabric-api-base": "*",
+    "fabric-model-loading-api-v1": "*"
   },
   "description": "Hooks for models and model loading.",
-  "mixins": [
-    "fabric-models-v0.mixins.json"
-  ],
   "custom": {
-    "fabric-api:module-lifecycle": "stable"
+    "fabric-api:module-lifecycle": "deprecated"
   }
 }
diff --git a/fabric-models-v0/build.gradle b/fabric-model-loading-api-v1/build.gradle
similarity index 69%
rename from fabric-models-v0/build.gradle
rename to fabric-model-loading-api-v1/build.gradle
index 58fefe77b..7831d25ef 100644
--- a/fabric-models-v0/build.gradle
+++ b/fabric-model-loading-api-v1/build.gradle
@@ -1,9 +1,10 @@
-archivesBaseName = "fabric-models-v0"
+archivesBaseName = "fabric-model-loading-api-v1"
 version = getSubprojectVersion(project)
 
 moduleDependencies(project, ['fabric-api-base'])
 
 testDependencies(project, [
+	':fabric-renderer-api-v1',
 	':fabric-rendering-v1',
 	':fabric-resource-loader-v0'
 ])
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java
new file mode 100644
index 000000000..dacd2f1a4
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Block state resolvers are responsible for mapping each {@link BlockState} of a block to an {@link UnbakedModel}.
+ * They replace the {@code blockstates/} JSON files. One block can be mapped to only one block state resolver; multiple
+ * resolvers will not receive the same block.
+ *
+ * <p>Block state resolvers can be used to create custom block state formats or dynamically resolve block state models.
+ *
+ * <p>Use {@link ModelResolver} instead of this interface if interacting with the block and block states directly is not
+ * necessary. This includes custom model deserializers and loaders.
+ *
+ * @see ModelResolver
+ * @see ModelModifier.OnLoad
+ */
+@FunctionalInterface
+public interface BlockStateResolver {
+	/**
+	 * Resolves the models for all block states of the block.
+	 *
+	 * <p>For each block state, call {@link Context#setModel} to set its unbaked model.
+	 * This method must be called exactly once for each block state.
+	 *
+	 * <p>Note that if multiple block states share the same unbaked model instance, it will be baked multiple times
+	 * (once per block state that has the model set), which is not efficient. To improve efficiency in this case, the
+	 * model should be delegated to using {@link DelegatingUnbakedModel} to ensure that it is only baked once. The inner
+	 * model can be loaded using {@link ModelResolver} if custom loading logic is necessary.
+	 */
+	void resolveBlockStates(Context context);
+
+	/**
+	 * The context for block state resolution.
+	 */
+	@ApiStatus.NonExtendable
+	interface Context {
+		/**
+		 * The block for which block state models are being resolved.
+		 */
+		Block block();
+
+		/**
+		 * Sets the model for a block state.
+		 *
+		 * @param state the block state for which this model should be used
+		 * @param model the unbaked model for this block state
+		 */
+		void setModel(BlockState state, UnbakedModel model);
+
+		/**
+		 * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
+		 *
+		 * @param id the model identifier
+		 * @return the unbaked model, or a missing model if it is not present
+		 */
+		UnbakedModel getOrLoadModel(Identifier id);
+
+		/**
+		 * The current model loader instance, which changes between resource reloads.
+		 *
+		 * <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
+		 * use {@link #getOrLoadModel} from the context instead.
+		 */
+		ModelLoader loader();
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java
new file mode 100644
index 000000000..ee37263fd
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBakeSettings;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * An unbaked model that returns another {@link BakedModel} at {@linkplain #bake bake time}.
+ * This allows multiple {@link UnbakedModel}s to share the same {@link BakedModel} instance
+ * and prevents baking the same model multiple times.
+ */
+public final class DelegatingUnbakedModel implements UnbakedModel {
+	private final Identifier delegate;
+	private final List<Identifier> dependencies;
+
+	/**
+	 * Constructs a new delegating model.
+	 *
+	 * @param delegate The identifier (can be a {@link ModelIdentifier}) of the underlying baked model.
+	 */
+	public DelegatingUnbakedModel(Identifier delegate) {
+		this.delegate = delegate;
+		this.dependencies = List.of(delegate);
+	}
+
+	@Override
+	public Collection<Identifier> getModelDependencies() {
+		return dependencies;
+	}
+
+	@Override
+	public void setParents(Function<Identifier, UnbakedModel> modelLoader) {
+	}
+
+	@Nullable
+	@Override
+	public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
+		return baker.bake(delegate, rotationContainer);
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java
new file mode 100644
index 000000000..dbc5e0c4c
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Fabric-provided helper methods for {@link BakedModelManager}.
+ *
+ * <p>Note: This interface is automatically implemented on the {@link BakedModelManager} via Mixin and interface injection.
+ */
+public interface FabricBakedModelManager {
+	/**
+	 * An alternative to {@link BakedModelManager#getModel(ModelIdentifier)} that accepts an
+	 * {@link Identifier} instead. Models loaded using {@link ModelLoadingPlugin.Context#addModels}
+	 * do not have a corresponding {@link ModelIdentifier}, so the vanilla method cannot be used to
+	 * retrieve them. The {@link Identifier} that was used to load them can be used in this method
+	 * to retrieve them.
+	 *
+	 * <p><b>This method, as well as its vanilla counterpart, should only be used after the
+	 * {@link BakedModelManager} has completed reloading.</b> Otherwise, the result will be
+	 * outdated or null.
+	 *
+	 * @param id the id of the model
+	 * @return the model
+	 */
+	@Nullable
+	default BakedModel getModel(Identifier id) {
+		throw new UnsupportedOperationException("Implemented via mixin.");
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
new file mode 100644
index 000000000..002deb2fa
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import java.util.Collection;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.block.Block;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+/**
+ * A model loading plugin is used to extend the model loading process through the passed {@link Context} object.
+ *
+ * <p>{@link PreparableModelLoadingPlugin} can be used if some resources need to be loaded from the
+ * {@link ResourceManager}.
+ */
+@FunctionalInterface
+public interface ModelLoadingPlugin {
+	/**
+	 * Registers a model loading plugin.
+	 */
+	static void register(ModelLoadingPlugin plugin) {
+		ModelLoadingPluginManager.registerPlugin(plugin);
+	}
+
+	/**
+	 * Called towards the beginning of the model loading process, every time resource are (re)loaded.
+	 * Use the context object to extend model loading as desired.
+	 */
+	void onInitializeModelLoader(Context pluginContext);
+
+	@ApiStatus.NonExtendable
+	interface Context {
+		/**
+		 * Adds one or more models (can be {@link ModelIdentifier}s) to the list of models that will be loaded and
+		 * baked.
+		 */
+		void addModels(Identifier... ids);
+
+		/**
+		 * Adds multiple models (can be {@link ModelIdentifier}s) to the list of models that will be loaded and baked.
+		 */
+		void addModels(Collection<? extends Identifier> ids);
+
+		/**
+		 * Registers a block state resolver for a block.
+		 *
+		 * <p>The block must be registered and a block state resolver must not have been previously registered for the
+		 * block.
+		 */
+		void registerBlockStateResolver(Block block, BlockStateResolver resolver);
+
+		/**
+		 * Event access to register model resolvers.
+		 */
+		Event<ModelResolver> resolveModel();
+
+		/**
+		 * Event access to monitor unbaked model loads and replace the loaded model.
+		 */
+		Event<ModelModifier.OnLoad> modifyModelOnLoad();
+
+		/**
+		 * Event access to replace the unbaked model used for baking without replacing the cached model.
+		 *
+		 * <p>This is useful for mods which wish to wrap a model without affecting other models that use it as a parent
+		 * (e.g. wrap a block's model into a non-{@link JsonUnbakedModel} class but still allow the item model to be
+		 * loaded and baked without exceptions).
+		 */
+		Event<ModelModifier.BeforeBake> modifyModelBeforeBake();
+
+		/**
+		 * Event access to monitor baked model loads and replace the loaded model.
+		 */
+		Event<ModelModifier.AfterBake> modifyModelAfterBake();
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
new file mode 100644
index 000000000..8a6fb0894
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import java.util.function.Function;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBakeSettings;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.event.Event;
+
+/**
+ * Contains interfaces for the events that can be used to modify models at different points in the loading and baking
+ * process.
+ *
+ * <p>Example use cases:
+ * <ul>
+ *     <li>Overriding a model for a particular block state - check if the given identifier is a {@link ModelIdentifier},
+ *     and then check if it has the appropriate variant for that block state. If so, return your desired model,
+ *     otherwise return the given model.</li>
+ *     <li>Wrapping a model to override certain behaviors - simply return a new model instance and delegate calls
+ *     to the original model as needed.</li>
+ * </ul>
+ *
+ * <p>Phases are used to ensure that modifications occur in a reasonable order, e.g. wrapping occurs after overrides,
+ * and separate phases are provided for mods that wrap their own models and mods that need to wrap models of other mods
+ * or wrap models arbitrarily.
+ *
+ * <p>These callbacks are invoked for <b>every single model that is loaded or baked</b>, so implementations should be
+ * as efficient as possible.
+ */
+public final class ModelModifier {
+	/**
+	 * Recommended phase to use when overriding models, e.g. replacing a model with another model.
+	 */
+	public static final Identifier OVERRIDE_PHASE = new Identifier("fabric", "override");
+	/**
+	 * Recommended phase to use for transformations that need to happen before wrapping, but after model overrides.
+	 */
+	public static final Identifier DEFAULT_PHASE = Event.DEFAULT_PHASE;
+	/**
+	 * Recommended phase to use when wrapping models.
+	 */
+	public static final Identifier WRAP_PHASE = new Identifier("fabric", "wrap");
+	/**
+	 * Recommended phase to use when wrapping models with transformations that want to happen last,
+	 * e.g. for connected textures or other similar visual effects that should be the final processing step.
+	 */
+	public static final Identifier WRAP_LAST_PHASE = new Identifier("fabric", "wrap_last");
+
+	@FunctionalInterface
+	public interface OnLoad {
+		/**
+		 * This handler is invoked to allow modification of an unbaked model right after it is first loaded and before
+		 * it is cached.
+		 *
+		 * @param model the current unbaked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyModelOnLoad
+		 */
+		UnbakedModel modifyModelOnLoad(UnbakedModel model, Context context);
+
+		/**
+		 * The context for an on load model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of this model (may be a {@link ModelIdentifier}).
+			 *
+			 * <p>For item models, only the {@link ModelIdentifier} with the {@code inventory} variant is passed, and
+			 * not the corresponding plain identifier.
+			 */
+			Identifier id();
+
+			/**
+			 * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already
+			 * loaded.
+			 *
+			 * @param id the model identifier
+			 * @return the unbaked model, or a missing model if it is not present
+			 */
+			UnbakedModel getOrLoadModel(Identifier id);
+
+			/**
+			 * The current model loader instance, which changes between resource reloads.
+			 *
+			 * <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model
+			 * resolution; use {@link #getOrLoadModel} from the context instead.
+			 */
+			ModelLoader loader();
+		}
+	}
+
+	@FunctionalInterface
+	public interface BeforeBake {
+		/**
+		 * This handler is invoked to allow modification of the unbaked model instance right before it is baked.
+		 *
+		 * @param model the current unbaked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyModelBeforeBake
+		 */
+		UnbakedModel modifyModelBeforeBake(UnbakedModel model, Context context);
+
+		/**
+		 * The context for a before bake model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of this model (may be a {@link ModelIdentifier}).
+			 */
+			Identifier id();
+
+			/**
+			 * The function that can be used to retrieve sprites.
+			 */
+			Function<SpriteIdentifier, Sprite> textureGetter();
+
+			/**
+			 * The settings this model is being baked with.
+			 */
+			ModelBakeSettings settings();
+
+			/**
+			 * The baker being used to bake this model.
+			 * It can be used to {@linkplain Baker#getOrLoadModel load unbaked models} and
+			 * {@linkplain Baker#bake load baked models}.
+			 */
+			Baker baker();
+
+			/**
+			 * The current model loader instance, which changes between resource reloads.
+			 */
+			ModelLoader loader();
+		}
+	}
+
+	@FunctionalInterface
+	public interface AfterBake {
+		/**
+		 * This handler is invoked to allow modification of the baked model instance right after it is baked and before
+		 * it is cached.
+		 *
+		 * <p>For further information, see the docs of {@link ModelLoadingPlugin.Context#modifyModelAfterBake()}.
+		 *
+		 * @param model the current baked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyModelAfterBake
+		 */
+		BakedModel modifyModelAfterBake(BakedModel model, Context context);
+
+		/**
+		 * The context for an after bake model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of this model (may be a {@link ModelIdentifier}).
+			 */
+			Identifier id();
+
+			/**
+			 * The unbaked model that is being baked.
+			 */
+			UnbakedModel sourceModel();
+
+			/**
+			 * The function that can be used to retrieve sprites.
+			 */
+			Function<SpriteIdentifier, Sprite> textureGetter();
+
+			/**
+			 * The settings this model is being baked with.
+			 */
+			ModelBakeSettings settings();
+
+			/**
+			 * The baker being used to bake this model.
+			 * It can be used to {@linkplain Baker#getOrLoadModel load unbaked models} and
+			 * {@linkplain Baker#bake load baked models}.
+			 */
+			Baker baker();
+
+			/**
+			 * The current model loader instance, which changes between resource reloads.
+			 */
+			ModelLoader loader();
+		}
+	}
+
+	private ModelModifier() { }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java
new file mode 100644
index 000000000..8236af031
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Model resolvers are able to provide a custom model for specific {@link Identifier}s.
+ * In vanilla, these {@link Identifier}s are converted to file paths and used to load
+ * a model from JSON. Since model resolvers override this process, they can be used to
+ * create custom model formats.
+ *
+ * <p>Only one resolver may provide a custom model for a certain {@link Identifier}.
+ * Thus, resolvers that load models using a custom format could conflict. To avoid
+ * conflicts, such resolvers may want to only load files with a mod-suffixed name
+ * or only load files that have been explicitly defined elsewhere.
+ *
+ * <p>If it is necessary to load and bake an arbitrary model that is not referenced
+ * normally, a model resolver can be used in conjunction with
+ * {@link ModelLoadingPlugin.Context#addModels} to directly load and bake custom model
+ * instances.
+ *
+ * <p>Model resolvers are invoked for <b>every single model that will be loaded</b>,
+ * so implementations should be as efficient as possible.
+ *
+ * @see ModelLoadingPlugin.Context#addModels
+ */
+@FunctionalInterface
+public interface ModelResolver {
+	/**
+	 * @return the resolved {@link UnbakedModel}, or {@code null} if this resolver does not handle the current {@link Identifier}
+	 */
+	@Nullable
+	UnbakedModel resolveModel(Context context);
+
+	/**
+	 * The context for model resolution.
+	 */
+	@ApiStatus.NonExtendable
+	interface Context {
+		/**
+		 * The identifier of the model to be loaded.
+		 */
+		Identifier id();
+
+		/**
+		 * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
+		 *
+		 * @param id the model identifier
+		 * @return the unbaked model, or a missing model if it is not present
+		 */
+		UnbakedModel getOrLoadModel(Identifier id);
+
+		/**
+		 * The current model loader instance, which changes between resource reloads.
+		 *
+		 * <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
+		 * use {@link #getOrLoadModel} from the context instead.
+		 */
+		ModelLoader loader();
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java
new file mode 100644
index 000000000..b900cffc5
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.model.loading.v1;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
+import net.minecraft.resource.ResourceManager;
+
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+/**
+ * A model loading plugin is used to extend the model loading process through the passed
+ * {@link ModelLoadingPlugin.Context} object.
+ *
+ * <p>This version of {@link ModelLoadingPlugin} allows loading ("preparing") some data off-thread in parallel before
+ * the model loading process starts. Usually, this means loading some resources from the provided
+ * {@link ResourceManager}.
+ */
+@FunctionalInterface
+public interface PreparableModelLoadingPlugin<T> {
+	/**
+	 * Registers a preparable model loading plugin.
+	 */
+	static <T> void register(DataLoader<T> loader, PreparableModelLoadingPlugin<T> plugin) {
+		ModelLoadingPluginManager.registerPlugin(loader, plugin);
+	}
+
+	/**
+	 * Called towards the beginning of the model loading process, every time resource are (re)loaded.
+	 * Use the context object to extend model loading as desired.
+	 *
+	 * @param data The data loaded by the {@link DataLoader}.
+	 * @param pluginContext The context that can be used to extend model loading.
+	 */
+	void onInitializeModelLoader(T data, ModelLoadingPlugin.Context pluginContext);
+
+	@FunctionalInterface
+	interface DataLoader<T> {
+		/**
+		 * Returns a {@link CompletableFuture} that will load the data.
+		 * Do not block the thread when this function is called, rather use
+		 * {@link CompletableFuture#supplyAsync(Supplier, Executor)} to compute the data.
+		 * The completable future should be scheduled to run using the passed executor.
+		 *
+		 * @param resourceManager The resource manager that can be used to retrieve resources.
+		 * @param executor The executor that <b>must</b> be used to schedule any completable future.
+		 */
+		CompletableFuture<T> load(ResourceManager resourceManager, Executor executor);
+	}
+}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
similarity index 71%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
index e93a75b23..3b70bc224 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package net.fabricmc.fabric.impl.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
 
-import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.block.Block;
 import net.minecraft.util.Identifier;
 
-public interface ModelLoaderHooks {
-	void fabric_addModel(Identifier id);
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
 
-	UnbakedModel fabric_loadModel(Identifier id);
+record BlockStateResolverHolder(BlockStateResolver resolver, Block block, Identifier blockId) {
 }
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
similarity index 61%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
index b4748ab6f..ecc5ddc0b 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
@@ -14,11 +14,17 @@
  * limitations under the License.
  */
 
-package net.fabricmc.fabric.impl.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
 
-import net.minecraft.client.render.model.BakedModel;
-import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
 
-public interface BakedModelManagerHooks {
-	BakedModel fabric_getModel(Identifier id);
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+
+/**
+ * Legacy v0 bridge - remove if the legacy v0 module is removed.
+ */
+public interface LegacyModelVariantProvider {
+	@Nullable
+	UnbakedModel loadModelVariant(ModelIdentifier modelId);
 }
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
similarity index 52%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
index 8117abbfe..67e1dfd26 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
@@ -14,26 +14,24 @@
  * limitations under the License.
  */
 
-package net.fabricmc.fabric.mixin.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
 
-import java.util.Map;
-
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Shadow;
-
-import net.minecraft.client.render.model.BakedModel;
-import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
 import net.minecraft.util.Identifier;
 
-import net.fabricmc.fabric.impl.client.model.BakedModelManagerHooks;
+public interface ModelLoaderHooks {
+	ModelLoadingEventDispatcher fabric_getDispatcher();
 
-@Mixin(BakedModelManager.class)
-public class BakedModelManagerMixin implements BakedModelManagerHooks {
-	@Shadow
-	private Map<Identifier, BakedModel> models;
+	UnbakedModel fabric_getMissingModel();
 
-	@Override
-	public BakedModel fabric_getModel(Identifier id) {
-		return models.get(id);
-	}
+	UnbakedModel fabric_getOrLoadModel(Identifier id);
+
+	void fabric_putModel(Identifier id, UnbakedModel model);
+
+	void fabric_putModelDirectly(Identifier id, UnbakedModel model);
+
+	void fabric_queueModelDependencies(UnbakedModel model);
+
+	JsonUnbakedModel fabric_loadModelFromJson(Identifier id);
 }
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java
new file mode 100644
index 000000000..ba4ceec64
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.model.loading;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.block.Block;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.registry.Registries;
+import net.minecraft.registry.RegistryKey;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelResolver;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+public class ModelLoaderPluginContextImpl implements ModelLoadingPlugin.Context {
+	private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoaderPluginContextImpl.class);
+
+	final Set<Identifier> extraModels = new LinkedHashSet<>();
+
+	private final Map<BlockKey, BlockStateResolverHolder> blockStateResolvers = new HashMap<>();
+	private final BlockKey lookupKey = new BlockKey();
+
+	private final Event<ModelResolver> modelResolvers = EventFactory.createArrayBacked(ModelResolver.class, resolvers -> context -> {
+		for (ModelResolver resolver : resolvers) {
+			try {
+				UnbakedModel model = resolver.resolveModel(context);
+
+				if (model != null) {
+					return model;
+				}
+			} catch (Exception exception) {
+				LOGGER.error("Failed to resolve model", exception);
+			}
+		}
+
+		return null;
+	});
+
+	private static final Identifier[] MODEL_MODIFIER_PHASES = new Identifier[] { ModelModifier.OVERRIDE_PHASE, ModelModifier.DEFAULT_PHASE, ModelModifier.WRAP_PHASE, ModelModifier.WRAP_LAST_PHASE };
+
+	private final Event<ModelModifier.OnLoad> onLoadModifiers = EventFactory.createWithPhases(ModelModifier.OnLoad.class, modifiers -> (model, context) -> {
+		for (ModelModifier.OnLoad modifier : modifiers) {
+			try {
+				model = modifier.modifyModelOnLoad(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify unbaked model on load", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.BeforeBake> beforeBakeModifiers = EventFactory.createWithPhases(ModelModifier.BeforeBake.class, modifiers -> (model, context) -> {
+		for (ModelModifier.BeforeBake modifier : modifiers) {
+			try {
+				model = modifier.modifyModelBeforeBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify unbaked model before bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.AfterBake> afterBakeModifiers = EventFactory.createWithPhases(ModelModifier.AfterBake.class, modifiers -> (model, context) -> {
+		for (ModelModifier.AfterBake modifier : modifiers) {
+			try {
+				model = modifier.modifyModelAfterBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify baked model after bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
+
+	/**
+	 * This field is used by the v0 wrapper to avoid constantly wrapping the context in hot code.
+	 */
+	public final Function<Identifier, UnbakedModel> modelGetter;
+
+	public ModelLoaderPluginContextImpl(Function<Identifier, UnbakedModel> modelGetter) {
+		this.modelGetter = modelGetter;
+	}
+
+	@Override
+	public void addModels(Identifier... ids) {
+		for (Identifier id : ids) {
+			extraModels.add(id);
+		}
+	}
+
+	@Override
+	public void addModels(Collection<? extends Identifier> ids) {
+		extraModels.addAll(ids);
+	}
+
+	@Override
+	public void registerBlockStateResolver(Block block, BlockStateResolver resolver) {
+		Objects.requireNonNull(block, "block cannot be null");
+		Objects.requireNonNull(resolver, "resolver cannot be null");
+
+		Optional<RegistryKey<Block>> optionalKey = Registries.BLOCK.getKey(block);
+
+		if (optionalKey.isEmpty()) {
+			throw new IllegalArgumentException("Received unregistered block");
+		}
+
+		Identifier blockId = optionalKey.get().getValue();
+		BlockKey key = new BlockKey(blockId.getNamespace(), blockId.getPath());
+		BlockStateResolverHolder holder = new BlockStateResolverHolder(resolver, block, blockId);
+
+		if (blockStateResolvers.put(key, holder) != null) {
+			throw new IllegalArgumentException("Duplicate block state resolver for block " + blockId);
+		}
+	}
+
+	@Nullable
+	BlockStateResolverHolder getBlockStateResolver(ModelIdentifier modelId) {
+		BlockKey key = lookupKey;
+		key.namespace = modelId.getNamespace();
+		key.path = modelId.getPath();
+
+		return blockStateResolvers.get(key);
+	}
+
+	@Override
+	public Event<ModelResolver> resolveModel() {
+		return modelResolvers;
+	}
+
+	@Override
+	public Event<ModelModifier.OnLoad> modifyModelOnLoad() {
+		return onLoadModifiers;
+	}
+
+	@Override
+	public Event<ModelModifier.BeforeBake> modifyModelBeforeBake() {
+		return beforeBakeModifiers;
+	}
+
+	@Override
+	public Event<ModelModifier.AfterBake> modifyModelAfterBake() {
+		return afterBakeModifiers;
+	}
+
+	private static class BlockKey {
+		private String namespace;
+		private String path;
+
+		private BlockKey() {
+		}
+
+		private BlockKey(String namespace, String path) {
+			this.namespace = namespace;
+			this.path = path;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) return true;
+			if (o == null || getClass() != o.getClass()) return false;
+			BlockKey blockKey = (BlockKey) o;
+			return namespace.equals(blockKey.namespace) && path.equals(blockKey.path);
+		}
+
+		@Override
+		public int hashCode() {
+			return 31 * namespace.hashCode() + path.hashCode();
+		}
+	}
+
+	// Legacy v0 bridge - remove if the legacy v0 module is removed.
+
+	private final Event<LegacyModelVariantProvider> legacyVariantProviders = EventFactory.createArrayBacked(LegacyModelVariantProvider.class, providers -> modelId -> {
+		for (LegacyModelVariantProvider provider : providers) {
+			try {
+				UnbakedModel model = provider.loadModelVariant(modelId);
+
+				if (model != null) {
+					return model;
+				}
+			} catch (Exception exception) {
+				LOGGER.error("Failed to run legacy model variant provider", exception);
+			}
+		}
+
+		return null;
+	});
+
+	public Event<LegacyModelVariantProvider> legacyVariantProviders() {
+		return legacyVariantProviders;
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
new file mode 100644
index 000000000..fba9dc19a
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.model.loading;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceSet;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.block.BlockModels;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBakeSettings;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelResolver;
+
+public class ModelLoadingEventDispatcher {
+	private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoadingEventDispatcher.class);
+
+	private final ModelLoader loader;
+	private final ModelLoaderPluginContextImpl pluginContext;
+
+	private final ObjectArrayList<ModelResolverContext> modelResolverContextStack = new ObjectArrayList<>();
+
+	private final ObjectArrayList<BlockStateResolverContext> blockStateResolverContextStack = new ObjectArrayList<>();
+	private final ReferenceSet<Block> resolvingBlocks = new ReferenceOpenHashSet<>();
+
+	private final ObjectArrayList<OnLoadModifierContext> onLoadModifierContextStack = new ObjectArrayList<>();
+	private final ObjectArrayList<BeforeBakeModifierContext> beforeBakeModifierContextStack = new ObjectArrayList<>();
+	private final ObjectArrayList<AfterBakeModifierContext> afterBakeModifierContextStack = new ObjectArrayList<>();
+
+	public ModelLoadingEventDispatcher(ModelLoader loader, List<ModelLoadingPlugin> plugins) {
+		this.loader = loader;
+		this.pluginContext = new ModelLoaderPluginContextImpl(((ModelLoaderHooks) loader)::fabric_getOrLoadModel);
+
+		for (ModelLoadingPlugin plugin : plugins) {
+			try {
+				plugin.onInitializeModelLoader(pluginContext);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to initialize model loading plugin", exception);
+			}
+		}
+	}
+
+	public void addExtraModels(Consumer<Identifier> extraModelConsumer) {
+		for (Identifier id : pluginContext.extraModels) {
+			extraModelConsumer.accept(id);
+		}
+	}
+
+	/**
+	 * @return {@code true} to cancel the vanilla method
+	 */
+	public boolean loadModel(Identifier id) {
+		if (id instanceof ModelIdentifier modelId) {
+			if ("inventory".equals(modelId.getVariant())) {
+				// We ALWAYS override the vanilla inventory model code path entirely, even for vanilla item models.
+				// See loadItemModel for an explanation.
+				loadItemModel(modelId);
+				return true;
+			} else {
+				// Prioritize block state resolver over legacy variant provider
+				BlockStateResolverHolder resolver = pluginContext.getBlockStateResolver(modelId);
+
+				if (resolver != null) {
+					loadBlockStateModels(resolver.resolver(), resolver.block(), resolver.blockId());
+					return true;
+				}
+
+				UnbakedModel legacyModel = legacyLoadModelVariant(modelId);
+
+				if (legacyModel != null) {
+					((ModelLoaderHooks) loader).fabric_putModel(id, legacyModel);
+					return true;
+				}
+
+				return false;
+			}
+		} else {
+			UnbakedModel model = resolveModel(id);
+
+			if (model != null) {
+				((ModelLoaderHooks) loader).fabric_putModel(id, model);
+				return true;
+			}
+
+			return false;
+		}
+	}
+
+	@Nullable
+	private UnbakedModel legacyLoadModelVariant(ModelIdentifier modelId) {
+		return pluginContext.legacyVariantProviders().invoker().loadModelVariant(modelId);
+	}
+
+	/**
+	 * This function handles both modded item models and vanilla item models.
+	 * The vanilla code path for item models is never used.
+	 * See the long comment in the function for an explanation.
+	 */
+	private void loadItemModel(ModelIdentifier modelId) {
+		ModelLoaderHooks loaderHooks = (ModelLoaderHooks) loader;
+
+		Identifier id = modelId.withPrefixedPath("item/");
+
+		// Legacy variant provider
+		UnbakedModel model = legacyLoadModelVariant(modelId);
+
+		// Model resolver
+		if (model == null) {
+			model = resolveModel(id);
+		}
+
+		// Load from the vanilla code path otherwise.
+		if (model == null) {
+			model = loaderHooks.fabric_loadModelFromJson(id);
+		}
+
+		// This is a bit tricky:
+		// We have a single UnbakedModel now, but there are two identifiers:
+		// the ModelIdentifier (...#inventory) and the Identifier (...:item/...).
+		// So we call the on load modifier now and then directly add the model to the ModelLoader,
+		// reimplementing the behavior of ModelLoader#put.
+		// Calling ModelLoader#put is not an option as the model for the Identifier would not be replaced by an on load modifier.
+		// This is why we override the vanilla code path entirely.
+		model = modifyModelOnLoad(modelId, model);
+
+		loaderHooks.fabric_putModelDirectly(modelId, model);
+		loaderHooks.fabric_putModelDirectly(id, model);
+		loaderHooks.fabric_queueModelDependencies(model);
+	}
+
+	private void loadBlockStateModels(BlockStateResolver resolver, Block block, Identifier blockId) {
+		if (!resolvingBlocks.add(block)) {
+			throw new IllegalStateException("Circular reference while resolving models for block " + block);
+		}
+
+		try {
+			resolveBlockStates(resolver, block, blockId);
+		} finally {
+			resolvingBlocks.remove(block);
+		}
+	}
+
+	private void resolveBlockStates(BlockStateResolver resolver, Block block, Identifier blockId) {
+		// Get and prepare context
+		if (blockStateResolverContextStack.isEmpty()) {
+			blockStateResolverContextStack.add(new BlockStateResolverContext());
+		}
+
+		BlockStateResolverContext context = blockStateResolverContextStack.pop();
+		context.prepare(block);
+
+		Reference2ReferenceMap<BlockState, UnbakedModel> resolvedModels = context.models;
+		ImmutableList<BlockState> allStates = block.getStateManager().getStates();
+		boolean thrown = false;
+
+		// Call resolver
+		try {
+			resolver.resolveBlockStates(context);
+		} catch (Exception e) {
+			LOGGER.error("Failed to resolve block state models for block {}. Using missing model for all states.", block, e);
+			thrown = true;
+		}
+
+		// Copy models over to the loader
+		if (thrown) {
+			UnbakedModel missingModel = ((ModelLoaderHooks) loader).fabric_getMissingModel();
+
+			for (BlockState state : allStates) {
+				ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+				((ModelLoaderHooks) loader).fabric_putModelDirectly(modelId, missingModel);
+			}
+		} else if (resolvedModels.size() == allStates.size()) {
+			// If there are as many resolved models as total states, all states have
+			// been resolved and models do not need to be null-checked.
+			resolvedModels.forEach((state, model) -> {
+				ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+				((ModelLoaderHooks) loader).fabric_putModel(modelId, model);
+			});
+		} else {
+			UnbakedModel missingModel = ((ModelLoaderHooks) loader).fabric_getMissingModel();
+
+			for (BlockState state : allStates) {
+				ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+				@Nullable
+				UnbakedModel model = resolvedModels.get(state);
+
+				if (model == null) {
+					LOGGER.error("Block state resolver did not provide a model for state {} in block {}. Using missing model.", state, block);
+					((ModelLoaderHooks) loader).fabric_putModelDirectly(modelId, missingModel);
+				} else {
+					((ModelLoaderHooks) loader).fabric_putModel(modelId, model);
+				}
+			}
+		}
+
+		resolvedModels.clear();
+
+		// Store context for reuse
+		blockStateResolverContextStack.add(context);
+	}
+
+	@Nullable
+	private UnbakedModel resolveModel(Identifier id) {
+		if (modelResolverContextStack.isEmpty()) {
+			modelResolverContextStack.add(new ModelResolverContext());
+		}
+
+		ModelResolverContext context = modelResolverContextStack.pop();
+		context.prepare(id);
+
+		UnbakedModel model = pluginContext.resolveModel().invoker().resolveModel(context);
+
+		modelResolverContextStack.push(context);
+		return model;
+	}
+
+	public UnbakedModel modifyModelOnLoad(Identifier id, UnbakedModel model) {
+		if (onLoadModifierContextStack.isEmpty()) {
+			onLoadModifierContextStack.add(new OnLoadModifierContext());
+		}
+
+		OnLoadModifierContext context = onLoadModifierContextStack.pop();
+		context.prepare(id);
+
+		model = pluginContext.modifyModelOnLoad().invoker().modifyModelOnLoad(model, context);
+
+		onLoadModifierContextStack.push(context);
+		return model;
+	}
+
+	public UnbakedModel modifyModelBeforeBake(UnbakedModel model, Identifier id, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
+		if (beforeBakeModifierContextStack.isEmpty()) {
+			beforeBakeModifierContextStack.add(new BeforeBakeModifierContext());
+		}
+
+		BeforeBakeModifierContext context = beforeBakeModifierContextStack.pop();
+		context.prepare(id, textureGetter, settings, baker);
+
+		model = pluginContext.modifyModelBeforeBake().invoker().modifyModelBeforeBake(model, context);
+
+		beforeBakeModifierContextStack.push(context);
+		return model;
+	}
+
+	public BakedModel modifyModelAfterBake(BakedModel model, Identifier id, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
+		if (afterBakeModifierContextStack.isEmpty()) {
+			afterBakeModifierContextStack.add(new AfterBakeModifierContext());
+		}
+
+		AfterBakeModifierContext context = afterBakeModifierContextStack.pop();
+		context.prepare(id, sourceModel, textureGetter, settings, baker);
+
+		model = pluginContext.modifyModelAfterBake().invoker().modifyModelAfterBake(model, context);
+
+		afterBakeModifierContextStack.push(context);
+		return model;
+	}
+
+	private class ModelResolverContext implements ModelResolver.Context {
+		private Identifier id;
+
+		private void prepare(Identifier id) {
+			this.id = id;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public UnbakedModel getOrLoadModel(Identifier id) {
+			return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+		}
+
+		@Override
+		public ModelLoader loader() {
+			return loader;
+		}
+	}
+
+	private class BlockStateResolverContext implements BlockStateResolver.Context {
+		private Block block;
+		private final Reference2ReferenceMap<BlockState, UnbakedModel> models = new Reference2ReferenceOpenHashMap<>();
+
+		private void prepare(Block block) {
+			this.block = block;
+			models.clear();
+		}
+
+		@Override
+		public Block block() {
+			return block;
+		}
+
+		@Override
+		public void setModel(BlockState state, UnbakedModel model) {
+			Objects.requireNonNull(model, "state cannot be null");
+			Objects.requireNonNull(model, "model cannot be null");
+
+			if (!state.isOf(block)) {
+				throw new IllegalArgumentException("Attempted to set model for state " + state + " on block " + block);
+			}
+
+			if (models.putIfAbsent(state, model) != null) {
+				throw new IllegalStateException("Duplicate model for state " + state + " on block " + block);
+			}
+		}
+
+		@Override
+		public UnbakedModel getOrLoadModel(Identifier id) {
+			return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+		}
+
+		@Override
+		public ModelLoader loader() {
+			return loader;
+		}
+	}
+
+	private class OnLoadModifierContext implements ModelModifier.OnLoad.Context {
+		private Identifier id;
+
+		private void prepare(Identifier id) {
+			this.id = id;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public UnbakedModel getOrLoadModel(Identifier id) {
+			return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+		}
+
+		@Override
+		public ModelLoader loader() {
+			return loader;
+		}
+	}
+
+	private class BeforeBakeModifierContext implements ModelModifier.BeforeBake.Context {
+		private Identifier id;
+		private Function<SpriteIdentifier, Sprite> textureGetter;
+		private ModelBakeSettings settings;
+		private Baker baker;
+
+		private void prepare(Identifier id, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
+			this.id = id;
+			this.textureGetter = textureGetter;
+			this.settings = settings;
+			this.baker = baker;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public Function<SpriteIdentifier, Sprite> textureGetter() {
+			return textureGetter;
+		}
+
+		@Override
+		public ModelBakeSettings settings() {
+			return settings;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+
+		@Override
+		public ModelLoader loader() {
+			return loader;
+		}
+	}
+
+	private class AfterBakeModifierContext implements ModelModifier.AfterBake.Context {
+		private Identifier id;
+		private UnbakedModel sourceModel;
+		private Function<SpriteIdentifier, Sprite> textureGetter;
+		private ModelBakeSettings settings;
+		private Baker baker;
+
+		private void prepare(Identifier id, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
+			this.id = id;
+			this.sourceModel = sourceModel;
+			this.textureGetter = textureGetter;
+			this.settings = settings;
+			this.baker = baker;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public UnbakedModel sourceModel() {
+			return sourceModel;
+		}
+
+		@Override
+		public Function<SpriteIdentifier, Sprite> textureGetter() {
+			return textureGetter;
+		}
+
+		@Override
+		public ModelBakeSettings settings() {
+			return settings;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+
+		@Override
+		public ModelLoader loader() {
+			return loader;
+		}
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java
new file mode 100644
index 000000000..8ccbee026
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.model.loading;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Util;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
+
+public final class ModelLoadingPluginManager {
+	private static final List<ModelLoadingPlugin> PLUGINS = new ArrayList<>();
+	private static final List<PreparablePluginHolder<?>> PREPARABLE_PLUGINS = new ArrayList<>();
+
+	public static final ThreadLocal<List<ModelLoadingPlugin>> CURRENT_PLUGINS = new ThreadLocal<>();
+
+	public static void registerPlugin(ModelLoadingPlugin plugin) {
+		Objects.requireNonNull(plugin, "plugin must not be null");
+
+		PLUGINS.add(plugin);
+	}
+
+	public static <T> void registerPlugin(PreparableModelLoadingPlugin.DataLoader<T> loader, PreparableModelLoadingPlugin<T> plugin) {
+		Objects.requireNonNull(loader, "data loader must not be null");
+		Objects.requireNonNull(plugin, "plugin must not be null");
+
+		PREPARABLE_PLUGINS.add(new PreparablePluginHolder<>(loader, plugin));
+	}
+
+	/**
+	 * The current exception behavior as of 1.20 is as follows.
+	 * If getting a {@link CompletableFuture}s throws then the whole client will crash.
+	 * If a {@link CompletableFuture} completes exceptionally then the resource reload will fail.
+	 */
+	public static CompletableFuture<List<ModelLoadingPlugin>> preparePlugins(ResourceManager resourceManager, Executor executor) {
+		List<CompletableFuture<ModelLoadingPlugin>> futures = new ArrayList<>();
+
+		for (ModelLoadingPlugin plugin : PLUGINS) {
+			futures.add(CompletableFuture.completedFuture(plugin));
+		}
+
+		for (PreparablePluginHolder<?> holder : PREPARABLE_PLUGINS) {
+			futures.add(preparePlugin(holder, resourceManager, executor));
+		}
+
+		return Util.combine(futures);
+	}
+
+	private static <T> CompletableFuture<ModelLoadingPlugin> preparePlugin(PreparablePluginHolder<T> holder, ResourceManager resourceManager, Executor executor) {
+		CompletableFuture<T> dataFuture = holder.loader.load(resourceManager, executor);
+		return dataFuture.thenApply(data -> pluginContext -> holder.plugin.onInitializeModelLoader(data, pluginContext));
+	}
+
+	private ModelLoadingPluginManager() { }
+
+	private record PreparablePluginHolder<T>(PreparableModelLoadingPlugin.DataLoader<T> loader, PreparableModelLoadingPlugin<T> plugin) { }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java
new file mode 100644
index 000000000..d627eb951
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.model.loading;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+import java.util.function.BiFunction;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.resource.ResourceReloader;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.profiler.Profiler;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.FabricBakedModelManager;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+@Mixin(BakedModelManager.class)
+public class BakedModelManagerMixin implements FabricBakedModelManager {
+	@Shadow
+	private Map<Identifier, BakedModel> models;
+
+	@Override
+	public BakedModel getModel(Identifier id) {
+		return models.get(id);
+	}
+
+	@Redirect(
+			method = "reload",
+			at = @At(
+					value = "INVOKE",
+					target = "java/util/concurrent/CompletableFuture.thenCombineAsync(Ljava/util/concurrent/CompletionStage;Ljava/util/function/BiFunction;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;",
+					remap = false
+			),
+			allow = 1)
+	private CompletableFuture<ModelLoader> loadModelPluginData(
+			CompletableFuture<Map<Identifier, JsonUnbakedModel>> self,
+			CompletionStage<Map<Identifier, List<ModelLoader.SourceTrackedData>>> otherFuture,
+			BiFunction<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<ModelLoader.SourceTrackedData>>, ModelLoader> modelLoaderConstructor,
+			Executor executor,
+			// reload args
+			ResourceReloader.Synchronizer synchronizer,
+			ResourceManager manager,
+			Profiler prepareProfiler,
+			Profiler applyProfiler,
+			Executor prepareExecutor,
+			Executor applyExecutor) {
+		CompletableFuture<List<ModelLoadingPlugin>> pluginsFuture = ModelLoadingPluginManager.preparePlugins(manager, prepareExecutor);
+		CompletableFuture<Pair<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<ModelLoader.SourceTrackedData>>>> pairFuture = self.thenCombine(otherFuture, Pair::new);
+		return pairFuture.thenCombineAsync(pluginsFuture, (pair, plugins) -> {
+			ModelLoadingPluginManager.CURRENT_PLUGINS.set(plugins);
+			return modelLoaderConstructor.apply(pair.getLeft(), pair.getRight());
+		}, executor);
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderBakerImplMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderBakerImplMixin.java
new file mode 100644
index 000000000..ef55ae900
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderBakerImplMixin.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.model.loading;
+
+import java.util.function.Function;
+
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBakeSettings;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderHooks;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
+
+@Mixin(targets = "net/minecraft/client/render/model/ModelLoader$BakerImpl")
+public class ModelLoaderBakerImplMixin {
+	@Shadow
+	@Final
+	private ModelLoader field_40571;
+	@Shadow
+	@Final
+	private Function<SpriteIdentifier, Sprite> textureGetter;
+
+	@ModifyVariable(method = "bake", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/render/model/ModelLoader$BakerImpl;getOrLoadModel(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/UnbakedModel;"))
+	private UnbakedModel invokeModifyBeforeBake(UnbakedModel model, Identifier id, ModelBakeSettings settings) {
+		ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
+		return dispatcher.modifyModelBeforeBake(model, id, textureGetter, settings, (Baker) this);
+	}
+
+	@Redirect(method = "bake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/UnbakedModel;bake(Lnet/minecraft/client/render/model/Baker;Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/BakedModel;"))
+	private BakedModel invokeModifyAfterBake(UnbakedModel unbakedModel, Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Identifier id) {
+		BakedModel model = unbakedModel.bake(baker, textureGetter, settings, id);
+		ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
+		return dispatcher.modifyModelAfterBake(model, id, unbakedModel, textureGetter, settings, baker);
+	}
+
+	@Redirect(method = "bake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/json/JsonUnbakedModel;bake(Lnet/minecraft/client/render/model/Baker;Lnet/minecraft/client/render/model/json/JsonUnbakedModel;Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Lnet/minecraft/util/Identifier;Z)Lnet/minecraft/client/render/model/BakedModel;"))
+	private BakedModel invokeModifyAfterBake(JsonUnbakedModel unbakedModel, Baker baker, JsonUnbakedModel parent, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Identifier id, boolean hasDepth) {
+		BakedModel model = unbakedModel.bake(baker, parent, textureGetter, settings, id, hasDepth);
+		ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
+		return dispatcher.modifyModelAfterBake(model, id, unbakedModel, textureGetter, settings, baker);
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderMixin.java
new file mode 100644
index 000000000..33cf18a34
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelLoaderMixin.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.model.loading;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.client.color.block.BlockColors;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.profiler.Profiler;
+
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderHooks;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+@Mixin(ModelLoader.class)
+public abstract class ModelLoaderMixin implements ModelLoaderHooks {
+	// The missing model is always loaded and added first.
+	@Final
+	@Shadow
+	public static ModelIdentifier MISSING_ID;
+	@Final
+	@Shadow
+	private Set<Identifier> modelsToLoad;
+	@Final
+	@Shadow
+	private Map<Identifier, UnbakedModel> unbakedModels;
+	@Shadow
+	@Final
+	private Map<Identifier, UnbakedModel> modelsToBake;
+
+	@Unique
+	private ModelLoadingEventDispatcher fabric_eventDispatcher;
+	// Explicitly not @Unique to allow mods that heavily rework model loading to reimplement the guard.
+	// Note that this is an implementation detail; it can change at any time.
+	private int fabric_guardGetOrLoadModel = 0;
+	private boolean fabric_enableGetOrLoadModelGuard = true;
+
+	@Shadow
+	private void addModel(ModelIdentifier id) {
+	}
+
+	@Shadow
+	public abstract UnbakedModel getOrLoadModel(Identifier id);
+
+	@Shadow
+	private void loadModel(Identifier id) {
+	}
+
+	@Shadow
+	private void putModel(Identifier id, UnbakedModel unbakedModel) {
+	}
+
+	@Shadow
+	public abstract JsonUnbakedModel loadModelFromJson(Identifier id);
+
+	@Inject(method = "<init>", at = @At(value = "INVOKE", target = "net/minecraft/util/profiler/Profiler.swap(Ljava/lang/String;)V", ordinal = 0))
+	private void afterMissingModelInit(BlockColors blockColors, Profiler profiler, Map<Identifier, JsonUnbakedModel> jsonUnbakedModels, Map<Identifier, List<ModelLoader.SourceTrackedData>> blockStates, CallbackInfo info) {
+		// Sanity check
+		if (!unbakedModels.containsKey(MISSING_ID)) {
+			throw new AssertionError("Missing model not initialized. This is likely a Fabric API porting bug.");
+		}
+
+		profiler.swap("fabric_plugins_init");
+
+		fabric_eventDispatcher = new ModelLoadingEventDispatcher((ModelLoader) (Object) this, ModelLoadingPluginManager.CURRENT_PLUGINS.get());
+		ModelLoadingPluginManager.CURRENT_PLUGINS.remove();
+		fabric_eventDispatcher.addExtraModels(this::addModel);
+	}
+
+	@Unique
+	private void addModel(Identifier id) {
+		if (id instanceof ModelIdentifier) {
+			addModel((ModelIdentifier) id);
+		} else {
+			// The vanilla addModel method is arbitrarily limited to ModelIdentifiers,
+			// but it's useful to tell the game to just load and bake a direct model path as well.
+			// Replicate the vanilla logic of addModel here.
+			UnbakedModel unbakedModel = getOrLoadModel(id);
+			this.unbakedModels.put(id, unbakedModel);
+			this.modelsToBake.put(id, unbakedModel);
+		}
+	}
+
+	@Inject(method = "getOrLoadModel", at = @At("HEAD"))
+	private void fabric_preventNestedGetOrLoadModel(Identifier id, CallbackInfoReturnable<UnbakedModel> cir) {
+		if (fabric_enableGetOrLoadModelGuard && fabric_guardGetOrLoadModel > 0) {
+			throw new IllegalStateException("ModelLoader#getOrLoadModel called from a ModelResolver or ModelModifier.OnBake instance. This is not allowed to prevent errors during model loading. Use getOrLoadModel from the context instead.");
+		}
+	}
+
+	@Inject(method = "loadModel", at = @At("HEAD"), cancellable = true)
+	private void onLoadModel(Identifier id, CallbackInfo ci) {
+		// Prevent calls to getOrLoadModel from loadModel as it will cause problems.
+		// Mods should call getOrLoadModel on the ModelResolver.Context instead.
+		fabric_guardGetOrLoadModel++;
+
+		try {
+			if (fabric_eventDispatcher.loadModel(id)) {
+				ci.cancel();
+			}
+		} finally {
+			fabric_guardGetOrLoadModel--;
+		}
+	}
+
+	@ModifyVariable(method = "putModel", at = @At("HEAD"), argsOnly = true)
+	private UnbakedModel onPutModel(UnbakedModel model, Identifier id) {
+		fabric_guardGetOrLoadModel++;
+
+		try {
+			return fabric_eventDispatcher.modifyModelOnLoad(id, model);
+		} finally {
+			fabric_guardGetOrLoadModel--;
+		}
+	}
+
+	@Override
+	public ModelLoadingEventDispatcher fabric_getDispatcher() {
+		return fabric_eventDispatcher;
+	}
+
+	@Override
+	public UnbakedModel fabric_getMissingModel() {
+		return unbakedModels.get(MISSING_ID);
+	}
+
+	/**
+	 * Unlike getOrLoadModel, this method supports nested model loading.
+	 *
+	 * <p>Vanilla does not due to the iteration over modelsToLoad which causes models to be resolved multiple times,
+	 * possibly leading to crashes.
+	 */
+	@Override
+	public UnbakedModel fabric_getOrLoadModel(Identifier id) {
+		if (this.unbakedModels.containsKey(id)) {
+			return this.unbakedModels.get(id);
+		}
+
+		if (!modelsToLoad.add(id)) {
+			throw new IllegalStateException("Circular reference while loading " + id);
+		}
+
+		try {
+			loadModel(id);
+		} finally {
+			modelsToLoad.remove(id);
+		}
+
+		return unbakedModels.get(id);
+	}
+
+	@Override
+	public void fabric_putModel(Identifier id, UnbakedModel model) {
+		putModel(id, model);
+	}
+
+	@Override
+	public void fabric_putModelDirectly(Identifier id, UnbakedModel model) {
+		unbakedModels.put(id, model);
+	}
+
+	@Override
+	public void fabric_queueModelDependencies(UnbakedModel model) {
+		modelsToLoad.addAll(model.getModelDependencies());
+	}
+
+	@Override
+	public JsonUnbakedModel fabric_loadModelFromJson(Identifier id) {
+		return loadModelFromJson(id);
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/resources/assets/fabric-model-loading-api-v1/icon.png b/fabric-model-loading-api-v1/src/client/resources/assets/fabric-model-loading-api-v1/icon.png
new file mode 100644
index 000000000..2931efbf6
Binary files /dev/null and b/fabric-model-loading-api-v1/src/client/resources/assets/fabric-model-loading-api-v1/icon.png differ
diff --git a/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json b/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json
new file mode 100644
index 000000000..133a6b442
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json
@@ -0,0 +1,13 @@
+{
+  "required": true,
+  "package": "net.fabricmc.fabric.mixin.client.model.loading",
+  "compatibilityLevel": "JAVA_17",
+  "client": [
+    "BakedModelManagerMixin",
+    "ModelLoaderMixin",
+    "ModelLoaderBakerImplMixin"
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/fabric-model-loading-api-v1/src/client/resources/fabric.mod.json b/fabric-model-loading-api-v1/src/client/resources/fabric.mod.json
new file mode 100644
index 000000000..abc4ffa0e
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/resources/fabric.mod.json
@@ -0,0 +1,35 @@
+{
+  "schemaVersion": 1,
+  "id": "fabric-model-loading-api-v1",
+  "name": "Fabric Model Loading API (v1)",
+  "version": "${version}",
+  "environment": "client",
+  "license": "Apache-2.0",
+  "icon": "assets/fabric-model-loading-api-v1/icon.png",
+  "contact": {
+    "homepage": "https://fabricmc.net",
+    "irc": "irc://irc.esper.net:6667/fabric",
+    "issues": "https://github.com/FabricMC/fabric/issues",
+    "sources": "https://github.com/FabricMC/fabric"
+  },
+  "authors": [
+    "FabricMC"
+  ],
+  "depends": {
+    "fabricloader": ">=0.14.21",
+    "fabric-api-base": "*"
+  },
+  "description": "Provides hooks for model loading.",
+  "mixins": [
+    {
+      "environment": "client",
+      "config": "fabric-model-loading-api-v1.mixins.json"
+    }
+  ],
+  "custom": {
+    "fabric-api:module-lifecycle": "stable",
+    "loom:injected_interfaces": {
+      "net/minecraft/class_1092": [ "net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager" ]
+    }
+  }
+}
diff --git a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelFeatureRenderer.java b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/BakedModelFeatureRenderer.java
similarity index 86%
rename from fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelFeatureRenderer.java
rename to fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/BakedModelFeatureRenderer.java
index 9577ce3cf..056ac08fe 100644
--- a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelFeatureRenderer.java
+++ b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/BakedModelFeatureRenderer.java
@@ -14,13 +14,15 @@
  * limitations under the License.
  */
 
-package net.fabricmc.fabric.test.model;
+package net.fabricmc.fabric.test.model.loading;
 
 import java.util.function.Supplier;
 
 import org.joml.AxisAngle4f;
 import org.joml.Quaternionf;
 
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.OverlayTexture;
 import net.minecraft.client.render.TexturedRenderLayers;
 import net.minecraft.client.render.VertexConsumer;
 import net.minecraft.client.render.VertexConsumerProvider;
@@ -32,7 +34,7 @@ import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.entity.LivingEntity;
 
 public class BakedModelFeatureRenderer<T extends LivingEntity, M extends EntityModel<T>> extends FeatureRenderer<T, M> {
-	private Supplier<BakedModel> modelSupplier;
+	private final Supplier<BakedModel> modelSupplier;
 
 	public BakedModelFeatureRenderer(FeatureRendererContext<T, M> context, Supplier<BakedModel> modelSupplier) {
 		super(context);
@@ -50,7 +52,7 @@ public class BakedModelFeatureRenderer<T extends LivingEntity, M extends EntityM
 		matrices.scale(-0.75F, -0.75F, 0.75F);
 		float aboveHead = (float) (Math.sin(animationProgress * 0.08F)) * 0.5F + 0.5F;
 		matrices.translate(-0.5F, 0.75F + aboveHead, -0.5F);
-		BakedModelRenderer.renderBakedModel(model, vertices, matrices.peek(), light);
+		MinecraftClient.getInstance().getBlockRenderManager().getModelRenderer().render(matrices.peek(), vertices, null, model, 1, 1, 1, light, OverlayTexture.DEFAULT_UV);
 		matrices.pop();
 	}
 }
diff --git a/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/ModelTestModClient.java b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/ModelTestModClient.java
new file mode 100644
index 000000000..2f0ba65ab
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/ModelTestModClient.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.model.loading;
+
+import java.util.function.Supplier;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.block.CropBlock;
+import net.minecraft.block.HorizontalConnectingBlock;
+import net.minecraft.client.render.block.BlockModels;
+import net.minecraft.client.render.entity.PlayerEntityRenderer;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceType;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.math.random.Random;
+import net.minecraft.world.BlockRenderView;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.DelegatingUnbakedModel;
+import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback;
+import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
+import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
+import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
+
+public class ModelTestModClient implements ClientModInitializer {
+	public static final String ID = "fabric-model-loading-api-v1-testmod";
+
+	public static final Identifier MODEL_ID = new Identifier(ID, "half_red_sand");
+
+	static class DownQuadRemovingModel extends ForwardingBakedModel {
+		DownQuadRemovingModel(BakedModel model) {
+			wrapped = model;
+		}
+
+		@Override
+		public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) {
+			context.pushTransform(q -> q.cullFace() != Direction.DOWN);
+			super.emitBlockQuads(blockView, state, pos, randomSupplier, context);
+			context.popTransform();
+		}
+	}
+
+	@Override
+	public void onInitializeClient() {
+		ModelLoadingPlugin.register(pluginContext -> {
+			pluginContext.addModels(MODEL_ID);
+			// remove bottom face of gold blocks
+			pluginContext.modifyModelAfterBake().register(ModelModifier.WRAP_PHASE, (model, context) -> {
+				if (context.id().getPath().equals("block/gold_block")) {
+					return new DownQuadRemovingModel(model);
+				} else {
+					return model;
+				}
+			});
+			// make fences with west: true and everything else false appear to be a missing model visually
+			ModelIdentifier fenceId = BlockModels.getModelId(Blocks.OAK_FENCE.getDefaultState().with(HorizontalConnectingBlock.WEST, true));
+			pluginContext.modifyModelOnLoad().register(ModelModifier.OVERRIDE_PHASE, (model, context) -> {
+				if (fenceId.equals(context.id())) {
+					return context.getOrLoadModel(ModelLoader.MISSING_ID);
+				}
+
+				return model;
+			});
+			// make brown glazed terracotta appear to be a missing model visually, but without affecting the item, by using pre-bake
+			// using load here would make the item also appear missing
+			pluginContext.modifyModelBeforeBake().register(ModelModifier.OVERRIDE_PHASE, (model, context) -> {
+				if (context.id().getPath().equals("block/brown_glazed_terracotta")) {
+					return context.loader().getOrLoadModel(ModelLoader.MISSING_ID);
+				}
+
+				return model;
+			});
+
+			// Make wheat stages 1->6 use the same model as stage 0. This can be done with resource packs, this is just a test.
+			pluginContext.registerBlockStateResolver(Blocks.WHEAT, context -> {
+				BlockState state = context.block().getDefaultState();
+
+				// All the block state models are top-level...
+				// Use a delegating unbaked model to make sure the identical models only get baked a single time.
+				Identifier wheatStage0Id = new Identifier("block/wheat_stage0");
+
+				UnbakedModel stage0Model = new DelegatingUnbakedModel(wheatStage0Id);
+
+				for (int age = 0; age <= 6; age++) {
+					context.setModel(state.with(CropBlock.AGE, age), stage0Model);
+				}
+
+				context.setModel(state.with(CropBlock.AGE, 7), context.getOrLoadModel(new Identifier("block/wheat_stage7")));
+			});
+		});
+
+		ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(SpecificModelReloadListener.INSTANCE);
+
+		LivingEntityFeatureRendererRegistrationCallback.EVENT.register((entityType, entityRenderer, registrationHelper, context) -> {
+			if (entityRenderer instanceof PlayerEntityRenderer playerRenderer) {
+				registrationHelper.register(new BakedModelFeatureRenderer<>(playerRenderer, SpecificModelReloadListener.INSTANCE::getSpecificModel));
+			}
+		});
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/NestedModelLoadingTest.java b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/NestedModelLoadingTest.java
new file mode 100644
index 000000000..7183940e4
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/NestedModelLoadingTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.model.loading;
+
+import com.mojang.logging.LogUtils;
+import org.slf4j.Logger;
+
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
+/**
+ * Tests that deep model resolution resolve each model a single time, depth-first.
+ */
+public class NestedModelLoadingTest implements ClientModInitializer {
+	private static final Logger LOGGER = LogUtils.getLogger();
+
+	private static Identifier id(String path) {
+		return new Identifier("fabric-model-loading-api-v1-testmod", path);
+	}
+
+	private static final Identifier BASE_MODEL = id("nested_base");
+	private static final Identifier NESTED_MODEL_1 = id("nested_1");
+	private static final Identifier NESTED_MODEL_2 = id("nested_2");
+	private static final Identifier NESTED_MODEL_3 = id("nested_3");
+	private static final Identifier NESTED_MODEL_4 = id("nested_4");
+	private static final Identifier NESTED_MODEL_5 = id("nested_5");
+	private static final Identifier TARGET_MODEL = new Identifier("minecraft", "block/stone");
+
+	@Override
+	public void onInitializeClient() {
+		ModelLoadingPlugin.register(pluginContext -> {
+			pluginContext.addModels(BASE_MODEL);
+
+			pluginContext.resolveModel().register(context -> {
+				Identifier id = context.id();
+				UnbakedModel ret = null;
+
+				if (id.equals(BASE_MODEL)) {
+					LOGGER.info("Nested model 1 started loading");
+					ret = context.getOrLoadModel(NESTED_MODEL_1);
+					LOGGER.info("Nested model 1 finished loading");
+				} else if (id.equals(NESTED_MODEL_1)) {
+					LOGGER.info(" Nested model 2 started loading");
+					ret = context.getOrLoadModel(NESTED_MODEL_2);
+					LOGGER.info(" Nested model 2 finished loading");
+				} else if (id.equals(NESTED_MODEL_2)) {
+					LOGGER.info("  Nested model 3 started loading");
+					ret = context.getOrLoadModel(NESTED_MODEL_3);
+					LOGGER.info("  Nested model 3 finished loading");
+				} else if (id.equals(NESTED_MODEL_3)) {
+					// Will be overridden by the model modifier below anyway.
+					LOGGER.info("   Returning dummy model for nested model 3");
+					ret = context.getOrLoadModel(ModelLoader.MISSING_ID);
+				} else if (id.equals(NESTED_MODEL_4)) {
+					// Will be overridden by the model modifier below anyway.
+					LOGGER.info("    Returning dummy model for nested model 4");
+					ret = context.getOrLoadModel(ModelLoader.MISSING_ID);
+				} else if (id.equals(NESTED_MODEL_5)) {
+					LOGGER.info("     Target model started loading");
+					ret = context.getOrLoadModel(TARGET_MODEL);
+					LOGGER.info("     Target model finished loading");
+				}
+
+				return ret;
+			});
+
+			pluginContext.modifyModelOnLoad().register((model, context) -> {
+				UnbakedModel ret = model;
+
+				if (context.id().equals(NESTED_MODEL_3)) {
+					Identifier id = context.id();
+
+					LOGGER.info("   Nested model 4 started loading");
+					ret = context.getOrLoadModel(NESTED_MODEL_4);
+					LOGGER.info("   Nested model 4 finished loading");
+
+					if (!id.equals(context.id())) {
+						throw new AssertionError("Context object should not have changed.");
+					}
+				} else if (context.id().equals(NESTED_MODEL_4)) {
+					LOGGER.info("    Nested model 5 started loading");
+					ret = context.getOrLoadModel(NESTED_MODEL_5);
+					LOGGER.info("    Nested model 5 finished loading");
+				}
+
+				return ret;
+			});
+		});
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/PreparablePluginTest.java b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/PreparablePluginTest.java
new file mode 100644
index 000000000..eb4ec3331
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/PreparablePluginTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.model.loading;
+
+import java.io.BufferedReader;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+import com.mojang.datafixers.util.Pair;
+import com.mojang.logging.LogUtils;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.resource.Resource;
+import net.minecraft.resource.ResourceFinder;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Util;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
+
+/**
+ * Allows putting model files in {@code /model_replacements} instead of {@code /models} to override models.
+ * This is just a test for off-thread data loading.
+ *
+ * <p>The visible effect in game is that gold blocks use the diamond texture instead...
+ */
+public class PreparablePluginTest implements ClientModInitializer {
+	private static final Logger LOGGER = LogUtils.getLogger();
+	private static final ResourceFinder MODEL_REPLACEMENTS_FINDER = ResourceFinder.json("model_replacements");
+
+	@Override
+	public void onInitializeClient() {
+		PreparableModelLoadingPlugin.register(PreparablePluginTest::loadModelReplacements, (replacementModels, pluginContext) -> {
+			pluginContext.modifyModelOnLoad().register((model, ctx) -> {
+				@Nullable
+				UnbakedModel replacementModel = replacementModels.get(ctx.id());
+				return replacementModel == null ? model : replacementModel;
+			});
+		});
+	}
+
+	/**
+	 * Adaptation of the {@link BakedModelManager} method.
+	 */
+	private static CompletableFuture<Map<Identifier, JsonUnbakedModel>> loadModelReplacements(ResourceManager resourceManager, Executor executor) {
+		return CompletableFuture.supplyAsync(() -> MODEL_REPLACEMENTS_FINDER.findResources(resourceManager), executor).thenCompose(models2 -> {
+			ArrayList<CompletableFuture<Pair<Identifier, JsonUnbakedModel>>> list = new ArrayList<>(models2.size());
+
+			for (Map.Entry<Identifier, Resource> entry : models2.entrySet()) {
+				list.add(CompletableFuture.supplyAsync(() -> {
+					try (BufferedReader reader = entry.getValue().getReader()) {
+						// Remove model_replacements/ prefix from the identifier
+						Identifier modelId = MODEL_REPLACEMENTS_FINDER.toResourceId(entry.getKey());
+
+						return Pair.of(modelId, JsonUnbakedModel.deserialize(reader));
+					} catch (Exception exception) {
+						LOGGER.error("Failed to load model {}", entry.getKey(), exception);
+						return null;
+					}
+				}, executor));
+			}
+
+			return Util.combineSafe(list).thenApply(models -> models.stream().filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Pair::getFirst, Pair::getSecond)));
+		});
+	}
+}
diff --git a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/SpecificModelReloadListener.java b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/SpecificModelReloadListener.java
similarity index 88%
rename from fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/SpecificModelReloadListener.java
rename to fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/SpecificModelReloadListener.java
index 948b419b3..7a9c9f90f 100644
--- a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/SpecificModelReloadListener.java
+++ b/fabric-model-loading-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/model/loading/SpecificModelReloadListener.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package net.fabricmc.fabric.test.model;
+package net.fabricmc.fabric.test.model.loading;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -27,7 +27,6 @@ import net.minecraft.util.Identifier;
 import net.minecraft.util.Unit;
 import net.minecraft.util.profiler.Profiler;
 
-import net.fabricmc.fabric.api.client.model.BakedModelManagerHelper;
 import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;
 import net.fabricmc.fabric.api.resource.ResourceReloadListenerKeys;
 
@@ -48,7 +47,7 @@ public class SpecificModelReloadListener extends SinglePreparationResourceReload
 
 	@Override
 	protected void apply(Unit loader, ResourceManager manager, Profiler profiler) {
-		specificModel = BakedModelManagerHelper.getModel(MinecraftClient.getInstance().getBakedModelManager(), ModelTestModClient.MODEL_ID);
+		specificModel = MinecraftClient.getInstance().getBakedModelManager().getModel(ModelTestModClient.MODEL_ID);
 	}
 
 	@Override
diff --git a/fabric-models-v0/src/testmodClient/resources/assets/fabric-models-v0-testmod/models/half_red_sand.json b/fabric-model-loading-api-v1/src/testmodClient/resources/assets/fabric-model-loading-api-v1-testmod/models/half_red_sand.json
similarity index 100%
rename from fabric-models-v0/src/testmodClient/resources/assets/fabric-models-v0-testmod/models/half_red_sand.json
rename to fabric-model-loading-api-v1/src/testmodClient/resources/assets/fabric-model-loading-api-v1-testmod/models/half_red_sand.json
diff --git a/fabric-model-loading-api-v1/src/testmodClient/resources/assets/minecraft/model_replacements/block/gold_block.json b/fabric-model-loading-api-v1/src/testmodClient/resources/assets/minecraft/model_replacements/block/gold_block.json
new file mode 100644
index 000000000..f03eb10fd
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/testmodClient/resources/assets/minecraft/model_replacements/block/gold_block.json
@@ -0,0 +1,6 @@
+{
+  "parent": "minecraft:block/cube_all",
+  "textures": {
+    "all": "minecraft:block/diamond_block"
+  }
+}
diff --git a/fabric-model-loading-api-v1/src/testmodClient/resources/fabric.mod.json b/fabric-model-loading-api-v1/src/testmodClient/resources/fabric.mod.json
new file mode 100644
index 000000000..97eea29e4
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/testmodClient/resources/fabric.mod.json
@@ -0,0 +1,19 @@
+{
+  "schemaVersion": 1,
+  "id": "fabric-model-loading-api-v1-testmod",
+  "name": "Fabric Model Loading API (v1) Test Mod",
+  "version": "1.0.0",
+  "environment": "client",
+  "license": "Apache-2.0",
+  "depends": {
+    "fabric-model-loading-api-v1": "*",
+    "fabric-resource-loader-v0": "*"
+  },
+  "entrypoints": {
+    "client": [
+      "net.fabricmc.fabric.test.model.loading.ModelTestModClient",
+      "net.fabricmc.fabric.test.model.loading.NestedModelLoadingTest",
+      "net.fabricmc.fabric.test.model.loading.PreparablePluginTest"
+    ]
+  }
+}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java b/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
deleted file mode 100644
index 4c58d8da9..000000000
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.impl.client.model;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import com.google.common.collect.Lists;
-import org.slf4j.LoggerFactory;
-import org.slf4j.Logger;
-import org.jetbrains.annotations.Nullable;
-
-import net.minecraft.client.render.model.ModelLoader;
-import net.minecraft.client.render.model.UnbakedModel;
-import net.minecraft.client.util.ModelIdentifier;
-import net.minecraft.resource.ResourceManager;
-import net.minecraft.util.Identifier;
-
-import net.fabricmc.fabric.api.client.model.ExtraModelProvider;
-import net.fabricmc.fabric.api.client.model.ModelAppender;
-import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
-import net.fabricmc.fabric.api.client.model.ModelProviderContext;
-import net.fabricmc.fabric.api.client.model.ModelProviderException;
-import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
-import net.fabricmc.fabric.api.client.model.ModelVariantProvider;
-import net.fabricmc.loader.api.FabricLoader;
-
-public class ModelLoadingRegistryImpl implements ModelLoadingRegistry {
-	private static final boolean DEBUG_MODEL_LOADING = FabricLoader.getInstance().isDevelopmentEnvironment()
-			|| Boolean.valueOf(System.getProperty("fabric.debugModelLoading", "false"));
-
-	@FunctionalInterface
-	private interface CustomModelItf<T> {
-		UnbakedModel load(T obj) throws ModelProviderException;
-	}
-
-	public static class LoaderInstance implements ModelProviderContext {
-		private final Logger logger;
-		private final ResourceManager manager;
-		private final List<ModelVariantProvider> modelVariantProviders;
-		private final List<ModelResourceProvider> modelResourceProviders;
-		private final List<ExtraModelProvider> modelAppenders;
-		private ModelLoader loader;
-
-		private LoaderInstance(ModelLoadingRegistryImpl i, ModelLoader loader, ResourceManager manager) {
-			this.logger = ModelLoadingRegistryImpl.LOGGER;
-			this.loader = loader;
-			this.manager = manager;
-			this.modelVariantProviders = i.variantProviderSuppliers.stream().map((s) -> s.apply(manager)).collect(Collectors.toList());
-			this.modelResourceProviders = i.resourceProviderSuppliers.stream().map((s) -> s.apply(manager)).collect(Collectors.toList());
-			this.modelAppenders = i.appenders;
-		}
-
-		@Override
-		public UnbakedModel loadModel(Identifier id) {
-			if (loader == null) {
-				throw new RuntimeException("Called loadModel too late!");
-			}
-
-			return ((ModelLoaderHooks) loader).fabric_loadModel(id);
-		}
-
-		public void onModelPopulation(Consumer<Identifier> addModel) {
-			for (ExtraModelProvider appender : modelAppenders) {
-				appender.provideExtraModels(manager, addModel);
-			}
-		}
-
-		private <T> UnbakedModel loadCustomModel(CustomModelItf<T> function, Collection<T> loaders, String debugName) {
-			if (!DEBUG_MODEL_LOADING) {
-				for (T provider : loaders) {
-					try {
-						UnbakedModel model = function.load(provider);
-
-						if (model != null) {
-							return model;
-						}
-					} catch (ModelProviderException e) {
-						logger.error("Failed to load custom model", e);
-						return null;
-					}
-				}
-
-				return null;
-			}
-
-			UnbakedModel modelLoaded = null;
-			T providerUsed = null;
-			List<T> providersApplied = null;
-
-			for (T provider : loaders) {
-				try {
-					UnbakedModel model = function.load(provider);
-
-					if (model != null) {
-						if (providersApplied != null) {
-							providersApplied.add(provider);
-						} else if (providerUsed != null) {
-							providersApplied = Lists.newArrayList(providerUsed, provider);
-						} else {
-							modelLoaded = model;
-							providerUsed = provider;
-						}
-					}
-				} catch (ModelProviderException e) {
-					logger.error("Failed to load custom model", e);
-					return null;
-				}
-			}
-
-			if (providersApplied != null) {
-				StringBuilder builder = new StringBuilder("Conflict - multiple " + debugName + "s claimed the same unbaked model:");
-
-				for (T loader : providersApplied) {
-					builder.append("\n\t - ").append(loader.getClass().getName());
-				}
-
-				logger.error(builder.toString());
-				return null;
-			} else {
-				return modelLoaded;
-			}
-		}
-
-		@Nullable
-		public UnbakedModel loadModelFromResource(Identifier resourceId) {
-			return loadCustomModel((r) -> r.loadModelResource(resourceId, this), modelResourceProviders, "resource provider");
-		}
-
-		@Nullable
-		public UnbakedModel loadModelFromVariant(Identifier variantId) {
-			if (!(variantId instanceof ModelIdentifier)) {
-				return loadModelFromResource(variantId);
-			} else {
-				ModelIdentifier modelId = (ModelIdentifier) variantId;
-				UnbakedModel model = loadCustomModel((r) -> r.loadModelVariant((ModelIdentifier) variantId, this), modelVariantProviders, "resource provider");
-
-				if (model != null) {
-					return model;
-				}
-
-				// Replicating the special-case from ModelLoader as loadModelFromJson is insufficiently patchable
-				if (Objects.equals(modelId.getVariant(), "inventory")) {
-					Identifier resourceId = new Identifier(modelId.getNamespace(), "item/" + modelId.getPath());
-					model = loadModelFromResource(resourceId);
-
-					if (model != null) {
-						return model;
-					}
-				}
-
-				return null;
-			}
-		}
-
-		public void finish() {
-			loader = null;
-		}
-	}
-
-	private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoadingRegistryImpl.class);
-
-	private final List<Function<ResourceManager, ModelVariantProvider>> variantProviderSuppliers = new ArrayList<>();
-	private final List<Function<ResourceManager, ModelResourceProvider>> resourceProviderSuppliers = new ArrayList<>();
-	private final List<ExtraModelProvider> appenders = new ArrayList<>();
-
-	@Override
-	public void registerModelProvider(ExtraModelProvider appender) {
-		appenders.add(appender);
-	}
-
-	@Override
-	public void registerAppender(ModelAppender appender) {
-		registerModelProvider((manager, consumer) -> appender.appendAll(manager, consumer::accept));
-	}
-
-	@Override
-	public void registerResourceProvider(Function<ResourceManager, ModelResourceProvider> providerSupplier) {
-		resourceProviderSuppliers.add(providerSupplier);
-	}
-
-	@Override
-	public void registerVariantProvider(Function<ResourceManager, ModelVariantProvider> providerSupplier) {
-		variantProviderSuppliers.add(providerSupplier);
-	}
-
-	public static LoaderInstance begin(ModelLoader loader, ResourceManager manager) {
-		return new LoaderInstance((ModelLoadingRegistryImpl) INSTANCE, loader, manager);
-	}
-}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/ModelLoaderMixin.java b/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/ModelLoaderMixin.java
deleted file mode 100644
index b16480a41..000000000
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/ModelLoaderMixin.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.mixin.client.model;
-
-import java.util.Map;
-import java.util.Set;
-
-import org.spongepowered.asm.mixin.Final;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Shadow;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.render.model.ModelLoader;
-import net.minecraft.client.render.model.UnbakedModel;
-import net.minecraft.client.util.ModelIdentifier;
-import net.minecraft.resource.ResourceManager;
-import net.minecraft.util.Identifier;
-
-import net.fabricmc.fabric.impl.client.model.ModelLoaderHooks;
-import net.fabricmc.fabric.impl.client.model.ModelLoadingRegistryImpl;
-
-@Mixin(ModelLoader.class)
-public abstract class ModelLoaderMixin implements ModelLoaderHooks {
-	// this is the first one
-	@Final
-	@Shadow
-	public static ModelIdentifier MISSING_ID;
-	@Final
-	@Shadow
-	private Set<Identifier> modelsToLoad;
-	@Final
-	@Shadow
-	private Map<Identifier, UnbakedModel> unbakedModels;
-	@Shadow
-	@Final
-	private Map<Identifier, UnbakedModel> modelsToBake;
-
-	private ModelLoadingRegistryImpl.LoaderInstance fabric_mlrLoaderInstance;
-
-	@Shadow
-	private void addModel(ModelIdentifier id) {
-	}
-
-	@Shadow
-	private void putModel(Identifier id, UnbakedModel unbakedModel) {
-	}
-
-	@Shadow
-	private void loadModel(Identifier id) {
-	}
-
-	@Shadow
-	public abstract UnbakedModel getOrLoadModel(Identifier id);
-
-	@Inject(at = @At("HEAD"), method = "loadModel", cancellable = true)
-	private void loadModelHook(Identifier id, CallbackInfo ci) {
-		UnbakedModel customModel = fabric_mlrLoaderInstance.loadModelFromVariant(id);
-
-		if (customModel != null) {
-			putModel(id, customModel);
-			ci.cancel();
-		}
-	}
-
-	@Inject(at = @At("HEAD"), method = "addModel")
-	private void addModelHook(ModelIdentifier id, CallbackInfo info) {
-		if (id == MISSING_ID) {
-			//noinspection RedundantCast
-			ModelLoaderHooks hooks = this;
-
-			ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager();
-			fabric_mlrLoaderInstance = ModelLoadingRegistryImpl.begin((ModelLoader) (Object) this, resourceManager);
-			fabric_mlrLoaderInstance.onModelPopulation(hooks::fabric_addModel);
-		}
-	}
-
-	@Inject(at = @At("RETURN"), method = "<init>")
-	private void initFinishedHook(CallbackInfo info) {
-		//noinspection ConstantConditions
-		fabric_mlrLoaderInstance.finish();
-	}
-
-	@Override
-	public void fabric_addModel(Identifier id) {
-		if (id instanceof ModelIdentifier) {
-			addModel((ModelIdentifier) id);
-		} else {
-			// The vanilla addModel method is arbitrarily limited to ModelIdentifiers,
-			// but it's useful to tell the game to just load and bake a direct model path as well.
-			// Replicate the vanilla logic of addModel here.
-			UnbakedModel unbakedModel = getOrLoadModel(id);
-			this.unbakedModels.put(id, unbakedModel);
-			this.modelsToBake.put(id, unbakedModel);
-		}
-	}
-
-	@Override
-	public UnbakedModel fabric_loadModel(Identifier id) {
-		if (!modelsToLoad.add(id)) {
-			throw new IllegalStateException("Circular reference while loading " + id);
-		}
-
-		loadModel(id);
-		modelsToLoad.remove(id);
-		return unbakedModels.get(id);
-	}
-}
diff --git a/fabric-models-v0/src/client/resources/fabric-models-v0.mixins.json b/fabric-models-v0/src/client/resources/fabric-models-v0.mixins.json
deleted file mode 100644
index 4da50ed3d..000000000
--- a/fabric-models-v0/src/client/resources/fabric-models-v0.mixins.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "required": true,
-  "package": "net.fabricmc.fabric.mixin.client.model",
-  "compatibilityLevel": "JAVA_16",
-  "client": [
-    "BakedModelManagerMixin",
-    "ModelLoaderMixin"
-  ],
-  "injectors": {
-    "defaultRequire": 1
-  }
-}
diff --git a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelRenderer.java b/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelRenderer.java
deleted file mode 100644
index bf3328591..000000000
--- a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/BakedModelRenderer.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.test.model;
-
-import org.apache.commons.lang3.ArrayUtils;
-
-import net.minecraft.client.render.OverlayTexture;
-import net.minecraft.client.render.VertexConsumer;
-import net.minecraft.client.render.model.BakedModel;
-import net.minecraft.client.render.model.BakedQuad;
-import net.minecraft.client.util.math.MatrixStack;
-import net.minecraft.util.math.Direction;
-import net.minecraft.util.math.random.Random;
-
-public class BakedModelRenderer {
-	private static final Direction[] CULL_FACES = ArrayUtils.add(Direction.values(), null);
-	private static final Random RANDOM = Random.create();
-
-	public static void renderBakedModel(BakedModel model, VertexConsumer vertices, MatrixStack.Entry entry, int light) {
-		for (Direction cullFace : CULL_FACES) {
-			RANDOM.setSeed(42L);
-
-			for (BakedQuad quad : model.getQuads(null, cullFace, RANDOM)) {
-				vertices.quad(entry, quad, 1.0F, 1.0F, 1.0F, light, OverlayTexture.DEFAULT_UV);
-			}
-		}
-	}
-}
diff --git a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/ModelTestModClient.java b/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/ModelTestModClient.java
deleted file mode 100644
index 2f54acddf..000000000
--- a/fabric-models-v0/src/testmodClient/java/net/fabricmc/fabric/test/model/ModelTestModClient.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.test.model;
-
-import net.minecraft.client.render.entity.PlayerEntityRenderer;
-import net.minecraft.resource.ResourceType;
-import net.minecraft.util.Identifier;
-
-import net.fabricmc.api.ClientModInitializer;
-import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
-import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback;
-import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
-
-public class ModelTestModClient implements ClientModInitializer {
-	public static final String ID = "fabric-models-v0-testmod";
-
-	public static final Identifier MODEL_ID = new Identifier(ID, "half_red_sand");
-
-	@Override
-	public void onInitializeClient() {
-		ModelLoadingRegistry.INSTANCE.registerModelProvider((manager, out) -> {
-			out.accept(MODEL_ID);
-		});
-
-		ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(SpecificModelReloadListener.INSTANCE);
-
-		LivingEntityFeatureRendererRegistrationCallback.EVENT.register((entityType, entityRenderer, registrationHelper, context) -> {
-			if (entityRenderer instanceof PlayerEntityRenderer) {
-				registrationHelper.register(new BakedModelFeatureRenderer<>((PlayerEntityRenderer) entityRenderer, SpecificModelReloadListener.INSTANCE::getSpecificModel));
-			}
-		});
-	}
-}
diff --git a/fabric-models-v0/src/testmodClient/resources/fabric.mod.json b/fabric-models-v0/src/testmodClient/resources/fabric.mod.json
deleted file mode 100644
index 83599a1f7..000000000
--- a/fabric-models-v0/src/testmodClient/resources/fabric.mod.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "schemaVersion": 1,
-  "id": "fabric-models-v0-testmod",
-  "name": "Fabric Models (v0) Test Mod",
-  "version": "1.0.0",
-  "environment": "client",
-  "license": "Apache-2.0",
-  "depends": {
-    "fabric-models-v0": "*",
-    "fabric-resource-loader-v0": "*"
-  },
-  "entrypoints": {
-    "client": [
-      "net.fabricmc.fabric.test.model.ModelTestModClient"
-    ]
-  }
-}
diff --git a/fabric-renderer-api-v1/build.gradle b/fabric-renderer-api-v1/build.gradle
index f8b1366eb..26eccd890 100644
--- a/fabric-renderer-api-v1/build.gradle
+++ b/fabric-renderer-api-v1/build.gradle
@@ -6,7 +6,7 @@ moduleDependencies(project, ['fabric-api-base'])
 testDependencies(project, [
 	':fabric-block-api-v1',
 	':fabric-blockrenderlayer-v1',
-	':fabric-models-v0',
+	':fabric-model-loading-api-v1',
 	':fabric-object-builder-api-v1',
 	':fabric-renderer-indigo',
 	':fabric-rendering-data-attachment-v1',
diff --git a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/FrameModelResourceProvider.java b/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/FrameModelResourceProvider.java
deleted file mode 100644
index 34ff995ba..000000000
--- a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/FrameModelResourceProvider.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.test.renderer.simple.client;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import org.jetbrains.annotations.Nullable;
-
-import net.minecraft.client.render.model.UnbakedModel;
-import net.minecraft.util.Identifier;
-
-import net.fabricmc.fabric.api.client.model.ModelProviderContext;
-import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
-
-/**
- * Provides the unbaked model for use with the frame block.
- */
-final class FrameModelResourceProvider implements ModelResourceProvider {
-	static final Set<Identifier> FRAME_MODELS = new HashSet<>();
-
-	@Nullable
-	@Override
-	public UnbakedModel loadModelResource(Identifier resourceId, ModelProviderContext context) {
-		if (FRAME_MODELS.contains(resourceId)) {
-			return new FrameUnbakedModel();
-		}
-
-		return null;
-	}
-}
diff --git a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/PillarModelVariantProvider.java b/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/PillarModelVariantProvider.java
deleted file mode 100644
index 8fbd97055..000000000
--- a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/PillarModelVariantProvider.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.test.renderer.simple.client;
-
-import org.jetbrains.annotations.Nullable;
-
-import net.minecraft.client.render.model.UnbakedModel;
-import net.minecraft.client.util.ModelIdentifier;
-
-import net.fabricmc.fabric.api.client.model.ModelProviderContext;
-import net.fabricmc.fabric.api.client.model.ModelVariantProvider;
-import net.fabricmc.fabric.test.renderer.simple.RendererTest;
-
-public class PillarModelVariantProvider implements ModelVariantProvider {
-	@Override
-	@Nullable
-	public UnbakedModel loadModelVariant(ModelIdentifier modelId, ModelProviderContext context) {
-		if (RendererTest.PILLAR_ID.equals(modelId)) {
-			return new PillarUnbakedModel();
-		} else {
-			return null;
-		}
-	}
-}
diff --git a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/RendererClientTest.java b/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/RendererClientTest.java
index 3fb47ec6f..1e45c93a9 100644
--- a/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/RendererClientTest.java
+++ b/fabric-renderer-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/renderer/simple/client/RendererClientTest.java
@@ -18,30 +18,47 @@ package net.fabricmc.fabric.test.renderer.simple.client;
 
 import static net.fabricmc.fabric.test.renderer.simple.RendererTest.id;
 
+import java.util.HashSet;
+import java.util.Set;
+
 import net.minecraft.client.render.RenderLayer;
 import net.minecraft.registry.Registries;
+import net.minecraft.util.Identifier;
 
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
-import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
 import net.fabricmc.fabric.test.renderer.simple.FrameBlock;
 import net.fabricmc.fabric.test.renderer.simple.RendererTest;
 
 public final class RendererClientTest implements ClientModInitializer {
+	private static final Set<Identifier> FRAME_MODELS = new HashSet<>();
+
 	@Override
 	public void onInitializeClient() {
-		ModelLoadingRegistry.INSTANCE.registerResourceProvider(manager -> new FrameModelResourceProvider());
-		ModelLoadingRegistry.INSTANCE.registerVariantProvider(manager -> new PillarModelVariantProvider());
-
 		for (FrameBlock frameBlock : RendererTest.FRAMES) {
 			// We don't specify a material for the frame mesh,
 			// so it will use the default material, i.e. the one from BlockRenderLayerMap.
 			BlockRenderLayerMap.INSTANCE.putBlock(frameBlock, RenderLayer.getCutoutMipped());
 
 			String itemPath = Registries.ITEM.getId(frameBlock.asItem()).getPath();
-			FrameModelResourceProvider.FRAME_MODELS.add(id("item/" + itemPath));
+			FRAME_MODELS.add(id("item/" + itemPath));
 		}
 
-		FrameModelResourceProvider.FRAME_MODELS.add(id("block/frame"));
+		FRAME_MODELS.add(id("block/frame"));
+
+		ModelLoadingPlugin.register(pluginContext -> {
+			pluginContext.resolveModel().register(context -> {
+				if (FRAME_MODELS.contains(context.id())) {
+					return new FrameUnbakedModel();
+				}
+
+				return null;
+			});
+
+			pluginContext.registerBlockStateResolver(RendererTest.PILLAR, context -> {
+				context.setModel(context.block().getDefaultState(), new PillarUnbakedModel());
+			});
+		});
 	}
 }
diff --git a/gradle.properties b/gradle.properties
index 62ed69f10..128b85f43 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -38,6 +38,7 @@ fabric-loot-api-v2-version=1.1.38
 fabric-loot-tables-v1-version=1.1.42
 fabric-message-api-v1-version=5.1.6
 fabric-mining-level-api-v1-version=2.1.48
+fabric-model-loading-api-v1-version=1.0.0
 fabric-models-v0-version=0.3.35
 fabric-networking-api-v1-version=1.3.8
 fabric-networking-v0-version=0.3.48
diff --git a/settings.gradle b/settings.gradle
index daeff6014..7b255e985 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -33,7 +33,7 @@ include 'fabric-lifecycle-events-v1'
 include 'fabric-loot-api-v2'
 include 'fabric-message-api-v1'
 include 'fabric-mining-level-api-v1'
-include 'fabric-models-v0'
+include 'fabric-model-loading-api-v1'
 include 'fabric-networking-api-v1'
 include 'fabric-object-builder-api-v1'
 include 'fabric-particles-v1'
@@ -61,6 +61,7 @@ include 'deprecated:fabric-command-api-v1'
 include 'deprecated:fabric-containers-v0'
 include 'deprecated:fabric-events-lifecycle-v0'
 include 'deprecated:fabric-keybindings-v0'
+include 'deprecated:fabric-models-v0'
 include 'deprecated:fabric-networking-v0'
 include 'deprecated:fabric-renderer-registries-v1'
 include 'deprecated:fabric-rendering-v0'