diff --git a/README.md b/README.md
index 494665c..5b01ab5 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ print(response.text)
 
 ```python
 # You can set a default model for the client instead of passing it with each call
-client = Spice(model="claude-3-opus-20240229")
+client = Spice(default_text_model="claude-3-opus-20240229")
 
 messages: List[SpiceMessage] = [
     {"role": "system", "content": "You are a helpful assistant."},
@@ -46,11 +46,14 @@ print(f"Input/Output tokens: {response.input_tokens}/{response.output_tokens}")
 ### Mixing Providers
 
 ```python
+# Commonly used models and providers have premade constants
+from spice.models import GPT_4_0125_PREVIEW
+
 # Alias models for easy configuration, even mixing providers
 model_aliases = {
-    "task1_model": {"model": "gpt-4-0125-preview"},
-    "task2_model": {"model": "claude-3-opus-20240229"},
-    "task3_model": {"model": "claude-3-haiku-20240307"},
+    "task1_model": GPT_4_0125_PREVIEW,
+    "task2_model": "claude-3-opus-20240229",
+    "task3_model": "claude-3-haiku-20240307",
 }
 
 client = Spice(model_aliases=model_aliases)
@@ -70,3 +73,30 @@ for i, response in enumerate(responses, 1):
     print(response.text)
     print(f"Characters per second: {response.characters_per_second:.2f}")
 ```
+
+### Using unknown models
+
+```python
+client = Spice()
+
+messages: List[SpiceMessage] = [
+    {"role": "system", "content": "You are a helpful assistant."},
+    {"role": "user", "content": "list 5 random words"},
+]
+
+# To use Azure, specify the provider and the deployment model name
+response = await client.get_response(messages=messages, model="first-gpt35", provider="azure")
+print(response.text)
+
+# Alternatively, to make a model and it's provider known to Spice, create a custom Model object
+from spice.models import TextModel
+from spice.providers import AZURE
+
+AZURE_GPT = TextModel("first-gpt35", AZURE, context_length=16385)
+response = await client.get_response(messages=messages, model=AZURE_GPT)
+print(response.text)
+
+# Creating the model automatically registers it in Spice's model list, so listing the provider is no longer needed
+response = await client.get_response(messages=messages, model="first-gpt35")
+print(response.text)
+```
diff --git a/pyproject.toml b/pyproject.toml
index f739b55..fa5049e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ packages=["spice"]
 
 [project]
 name = "spiceai"
-version = "0.1.7"
+version = "0.1.8"
 license = {text = "Apache-2.0"}
 description = "A Python library for building AI-powered applications."
 readme = "README.md"
diff --git a/scripts/run.py b/scripts/run.py
index 0f3aa14..9cd1cd7 100644
--- a/scripts/run.py
+++ b/scripts/run.py
@@ -75,16 +75,27 @@ async def multiple_providers_example():
 
 
 async def azure_example():
-    # Use azure deployment name for model
-    client = Spice(default_text_model="first-gpt35", provider="azure")
+    client = Spice()
 
     messages: List[SpiceMessage] = [
         {"role": "system", "content": "You are a helpful assistant."},
         {"role": "user", "content": "list 5 random words"},
     ]
 
-    response = await client.get_response(messages=messages)
+    # To use Azure, specify the provider and the deployment model name
+    response = await client.get_response(messages=messages, model="first-gpt35", provider="azure")
+    print(response.text)
+
+    # Alternatively, to make a model and it's provider known to Spice, create a custom Model object
+    from spice.models import TextModel
+    from spice.providers import AZURE
+
+    AZURE_GPT = TextModel("first-gpt35", AZURE, context_length=16385)
+    response = await client.get_response(messages=messages, model=AZURE_GPT)
+    print(response.text)
 
+    # Creating the model automatically registers it in Spice's model list, so listing the provider is no longer needed
+    response = await client.get_response(messages=messages, model="first-gpt35")
     print(response.text)
 
 
diff --git a/spice/__init__.py b/spice/__init__.py
index 4839dd9..8d8ba28 100644
--- a/spice/__init__.py
+++ b/spice/__init__.py
@@ -1,5 +1,5 @@
 from .errors import SpiceError, AuthenticationError, APIConnectionError  # noqa
 from .spice import Spice, SpiceResponse, StreamingSpiceResponse  # noqa
 from .spice_message import SpiceMessage  # noqa
-from .models import Model, models  # noqa
+from .models import Model, TextModel, VisionModel, EmbeddingModel, TranscriptionModel, models  # noqa
 from .providers import Provider, providers  # noqa
diff --git a/spice/models.py b/spice/models.py
index c1658cd..081850a 100644
--- a/spice/models.py
+++ b/spice/models.py
@@ -1,9 +1,14 @@
+from __future__ import annotations
+
 from dataclasses import dataclass
 from typing import List
 
 from spice.errors import InvalidModelError
 from spice.providers import ANTHROPIC, OPEN_AI, Provider
 
+# Used to fetch a model by name
+models: List[Model] = []
+
 
 # TODO: Add costs to models
 @dataclass
@@ -11,6 +16,9 @@ class Model:
     name: str
     provider: Provider | None
 
+    def __post_init__(self):
+        models.append(self)
+
 
 @dataclass
 class UnknownModel(Model):
@@ -51,22 +59,10 @@ TEXT_EMBEDDING_3_LARGE = EmbeddingModel("text-embedding-3-large", OPEN_AI)
 TEXT_EMBEDDING_3_SMALL = EmbeddingModel("text-embedding-3-small", OPEN_AI)
 TEXT_EMBEDDING_ADA_002 = EmbeddingModel("text-embedding-ada-002", OPEN_AI)
 
-# Used to fetch a model by name
-models: List[Model] = [
-    GPT_4_0125_PREVIEW,
-    GPT_4_VISION_PREVIEW,
-    GPT_35_TURBO_0125,
-    CLAUDE_3_OPUS_20240229,
-    CLAUDE_3_HAIKU_20240307,
-    WHISPER_1,
-    TEXT_EMBEDDING_3_LARGE,
-    TEXT_EMBEDDING_3_SMALL,
-    TEXT_EMBEDDING_ADA_002,
-]
-
 
 def get_model_from_name(model_name: str) -> Model:
-    for model in models:
+    # Search backwards; this way user defined models take priority
+    for model in reversed(models):
         if model.name == model_name:
             return model
 
diff --git a/spice/providers.py b/spice/providers.py
index c8cf620..2b695c8 100644
--- a/spice/providers.py
+++ b/spice/providers.py
@@ -1,13 +1,16 @@
+from __future__ import annotations
+
 import os
 from dataclasses import dataclass
 from typing import Callable, List
 
 from dotenv import load_dotenv
 
-from spice.errors import InvalidProviderError, NoAPIKeyError
+from spice.errors import InvalidProviderError, NoAPIKeyError, SpiceError
 from spice.wrapped_clients import WrappedAnthropicClient, WrappedAzureClient, WrappedClient, WrappedOpenAIClient
 
-# TODO: Is there a reason to have providers? We could just have a client that lazily checks for keys
+# Used to fetch a provider by name
+providers: List[Provider] = []
 
 
 @dataclass
@@ -15,6 +18,9 @@ class Provider:
     name: str
     get_client: Callable[[], WrappedClient]
 
+    def __post_init__(self):
+        providers.append(self)
+
 
 def get_openai_client(cache=[]):
     if cache:
@@ -43,7 +49,7 @@ def get_azure_client(cache=[]):
     if key is None:
         raise NoAPIKeyError("AZURE_OPENAI_KEY not set")
     if endpoint is None:
-        raise NoAPIKeyError("AZURE_OPENAI_ENDPOINT not set")
+        raise SpiceError("AZURE_OPENAI_ENDPOINT not set")
 
     client = WrappedAzureClient(key, endpoint)
     cache.append(client)
@@ -69,9 +75,6 @@ OPEN_AI = Provider("open_ai", get_openai_client)
 AZURE = Provider("azure", get_azure_client)
 ANTHROPIC = Provider("anthropic", get_anthropic_client)
 
-# Used to fetch a provider by name
-providers: List[Provider] = [OPEN_AI, AZURE, ANTHROPIC]
-
 
 def get_provider_from_name(provider_name: str) -> Provider:
     for provider in providers:
diff --git a/spice/spice.py b/spice/spice.py
index d9edb21..d8f3227 100644
--- a/spice/spice.py
+++ b/spice/spice.py
@@ -32,19 +32,36 @@ class SpiceResponse:
     """
 
     call_args: SpiceCallArgs
+    """The call arguments given to the model that created this response."""
+
     text: str
+    """The total text sent by the model."""
+
     total_time: float
+    """
+    How long it took for the response to be completed.
+    May be inaccurate for streamed responses if not iterated over and completed immediately.
+    """
+
     input_tokens: int
+    """The number of input tokens given in this response."""
+
     output_tokens: int
+    """The number of output tokens given by the model in this response."""
+
     completed: bool
+    """Whether or not this response was fully completed. This will only ever be false for streaming responses."""
+
     # TODO: Add cost
 
     @property
     def total_tokens(self) -> int:
+        """The total tokens, input and output, in this response."""
         return self.input_tokens + self.output_tokens
 
     @property
     def characters_per_second(self) -> float:
+        """The characters per second that the model output. May be inaccurate for streamed responses if not iterated over and completed immediately."""
         return len(self.text) / self.total_time
 
 
@@ -125,13 +142,30 @@ class StreamingSpiceResponse:
 
 
 class Spice:
+    """
+    The Spice client. The majority of the time, only one Spice client should be initialized and used.
+    Automatically handles multiple providers and their respective errors.
+    """
+
     def __init__(
         self,
         default_text_model: Optional[TextModel | str] = None,
         default_embeddings_model: Optional[EmbeddingModel | str] = None,
-        provider: Optional[Provider | str] = None,
         model_aliases: Optional[Dict[str, Model | str]] = None,
     ):
+        """
+        Creates a new Spice client.
+
+        Args:
+            default_text_model: The default model that will be used for chat completions if no other model is given.
+            Will raise an InvalidModelError if the model is not a text model.
+
+            default_embeddings_model: The default model that will be used for embeddings if no other model is given.
+            Will raise an InvalidModelError if the model is not an embeddings model.
+
+            model_aliases: A custom model name map.
+        """
+
         if isinstance(default_text_model, str):
             text_model = get_model_from_name(default_text_model)
         else:
@@ -148,16 +182,24 @@ class Spice:
             raise InvalidModelError("Default embeddings model must be an embeddings model")
         self._default_embeddings_model = embeddings_model
 
-        if isinstance(provider, str):
-            provider = get_provider_from_name(provider)
-        self._provider = provider
-
         # TODO: Should we validate model aliases?
         self._model_aliases = model_aliases
 
-    def _get_client(self, model: Model) -> WrappedClient:
-        if self._provider is not None:
-            return self._provider.get_client()
+    def load_provider(self, provider: Provider | str):
+        """
+        Loads the specified provider and raises a NoAPIKeyError if no valid api key for the provider is found.
+        Providers not preloaded will be loaded on first use, and the NoAPIKeyError will be raised when they are used.
+        """
+
+        if isinstance(provider, str):
+            provider = get_provider_from_name(provider)
+        provider.get_client()
+
+    def _get_client(self, model: Model, provider: Optional[Provider | str]) -> WrappedClient:
+        if provider is not None:
+            if isinstance(provider, str):
+                provider = get_provider_from_name(provider)
+            return provider.get_client()
         else:
             if model.provider is None:
                 raise InvalidModelError("Provider is required when unknown models are used")
@@ -207,12 +249,33 @@ class Spice:
         self,
         messages: List[SpiceMessage],
         model: Optional[TextModel | str] = None,
+        provider: Optional[Provider | str] = None,
         temperature: Optional[float] = None,
         max_tokens: Optional[int] = None,
         response_format: Optional[ResponseFormatType] = None,
     ) -> SpiceResponse:
+        """
+        Asynchronously retrieves a chat completion response.
+
+        Args:
+            messages: The list of messages given as context for the completion.
+
+            model: The model to use. Must be a text based model. If no model is given, will use the default text model
+            the client was initialized with. If the model is unknown to Spice, a provider must be given.
+            Will raise an InvalidModelError if the model is not a text model, or if the model is unknown and no provider was given.
+
+            provider: The provider to use. If specified, will override the model's default provider if known. Must be specified if an unknown model is used.
+
+            temperature: The temperature to give the model.
+
+            max_tokens: The maximum tokens the model can output.
+
+            response_format: For valid models, will set the response format to 'text' or 'json'.
+            If the provider/model does not support response_format, this argument will be ignored.
+        """
+
         text_model = self._get_text_model(model)
-        client = self._get_client(text_model)
+        client = self._get_client(text_model, provider)
         call_args = self._fix_call_args(messages, text_model, False, temperature, max_tokens, response_format)
 
         start_time = timer()
@@ -230,12 +293,33 @@ class Spice:
         self,
         messages: List[SpiceMessage],
         model: Optional[TextModel | str] = None,
+        provider: Optional[Provider | str] = None,
         temperature: Optional[float] = None,
         max_tokens: Optional[int] = None,
         response_format: Optional[ResponseFormatType] = None,
     ) -> StreamingSpiceResponse:
+        """
+        Asynchronously retrieves a chat completion stream that can be iterated over asynchronously.
+
+        Args:
+            messages: The list of messages given as context for the completion.
+
+            model: The model to use. Must be a text based model. If no model is given, will use the default text model
+            the client was initialized with. If the model is unknown to Spice, a provider must be given.
+            Will raise an InvalidModelError if the model is not a text model, or if the model is unknown and no provider was given.
+
+            provider: The provider to use. If specified, will override the model's default provider if known. Must be specified if an unknown model is used.
+
+            temperature: The temperature to give the model.
+
+            max_tokens: The maximum tokens the model can output.
+
+            response_format: For valid models, will set the response format to 'text' or 'json'.
+            If the provider/model does not support response_format, this argument will be ignored.
+        """
+
         text_model = self._get_text_model(model)
-        client = self._get_client(text_model)
+        client = self._get_client(text_model, provider)
         call_args = self._fix_call_args(messages, text_model, True, temperature, max_tokens, response_format)
 
         with client.catch_and_convert_errors():
@@ -261,18 +345,50 @@ class Spice:
         return model
 
     async def get_embeddings(
-        self, input_texts: List[str], model: Optional[EmbeddingModel | str] = None
+        self,
+        input_texts: List[str],
+        model: Optional[EmbeddingModel | str] = None,
+        provider: Optional[Provider | str] = None,
     ) -> List[List[float]]:
+        """
+        Asynchronously retrieves embeddings for a list of text.
+
+        Args:
+            input_texts: The texts to generate embeddings for.
+
+            model: The embedding model to use. If no model is given, will use the default embedding model
+            the client was initialized with. If the model is unknown to Spice, a provider must be given.
+            Will raise an InvalidModelError if the model is not a embedding model, or if the model is unknown and no provider was given.
+
+            provider: The provider to use. If specified, will override the model's default provider if known. Must be specified if an unknown model is used.
+        """
+
         embedding_model = self._get_embedding_model(model)
-        client = self._get_client(embedding_model)
+        client = self._get_client(embedding_model, provider)
 
         return await client.get_embeddings(input_texts, embedding_model.name)
 
     def get_embeddings_sync(
-        self, input_texts: List[str], model: Optional[EmbeddingModel | str] = None
+        self,
+        input_texts: List[str],
+        model: Optional[EmbeddingModel | str] = None,
+        provider: Optional[Provider | str] = None,
     ) -> List[List[float]]:
+        """
+        Synchronously retrieves embeddings for a list of text.
+
+        Args:
+            input_texts: The texts to generate embeddings for.
+
+            model: The embedding model to use. If no model is given, will use the default embedding model
+            the client was initialized with. If the model is unknown to Spice, a provider must be given.
+            Will raise an InvalidModelError if the model is not a embedding model, or if the model is unknown and no provider was given.
+
+            provider: The provider to use. If specified, will override the model's default provider if known. Must be specified if an unknown model is used.
+        """
+
         embedding_model = self._get_embedding_model(model)
-        client = self._get_client(embedding_model)
+        client = self._get_client(embedding_model, provider)
 
         return client.get_embeddings_sync(input_texts, embedding_model.name)
 
@@ -288,8 +404,25 @@ class Spice:
 
         return model
 
-    async def get_transcription(self, audio_path: Path, model: TranscriptionModel | str) -> str:
+    async def get_transcription(
+        self,
+        audio_path: Path,
+        model: TranscriptionModel | str,
+        provider: Optional[Provider | str] = None,
+    ) -> str:
+        """
+        Asynchronously retrieves embeddings for a list of text.
+
+        Args:
+            audio_path: The path to the audio file to transcribe.
+
+            model: The model to use. Must be a transciption model. If the model is unknown to Spice, a provider must be given.
+            Will raise an InvalidModelError if the model is not a transciption model, or if the model is unknown and no provider was given.
+
+            provider: The provider to use. If specified, will override the model's default provider if known. Must be specified if an unknown model is used.
+        """
+
         transciption_model = self._get_transcription_model(model)
-        client = self._get_client(transciption_model)
+        client = self._get_client(transciption_model, provider)
 
         return await client.get_transcription(audio_path, transciption_model.name)
diff --git a/spice/wrapped_clients.py b/spice/wrapped_clients.py
index b2893f3..844deb6 100644
--- a/spice/wrapped_clients.py
+++ b/spice/wrapped_clients.py
@@ -158,8 +158,7 @@ class WrappedAnthropicClient(WrappedClient):
             {"temperature": call_args.temperature} if call_args.temperature is not None else {}
         )
 
-        # TODO:
-        # convert messages to anthropic format (images and system messages are handled differently than OpenAI, whose format we use)
+        # TODO: convert messages to anthropic format (images and system messages are handled differently than OpenAI, whose format we use)
         converted_messages: List[MessageParam] = []
         for message in messages:
             pass
