Repository: griptape-ai/griptape (language: python)

Issue:
Add `AzureOpenAiStructureConfig`
 - [x] I have read and agree to the [contributing guidelines](https://github.com/griptape-ai/griptape#contributing).

**Is your feature request related to a problem? Please describe.**
There is no option to use the newly created `StructureConfig` classes with Azure OpenAI

**Describe the solution you'd like**
Add an `AzureOpenAiStructureConfig` class

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 519708d18..aaa95e7a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## Unreleased
 
+### Added
+- `AzureOpenAiStructureConfig` for providing Structures with all Azure OpenAI Driver configuration.
+- `AzureOpenAiVisionImageQueryDriver` to support queries on images using Azure's OpenAI Vision models.
+
+### Changed
+- Default the value of `azure_deployment` on all Azure Drivers to the model the Driver is using.
+- Field `azure_ad_token` on all Azure Drivers is no longer serializable.
+
 ## [0.25.1] - 2024-05-15
 
 ### Fixed
diff --git a/docs/examples/multiple-agent-shared-memory.md b/docs/examples/multiple-agent-shared-memory.md
index ac69abcd4..7332de165 100644
--- a/docs/examples/multiple-agent-shared-memory.md
+++ b/docs/examples/multiple-agent-shared-memory.md
@@ -10,12 +10,12 @@ The `MongoDbAtlasVectorStoreDriver` assumes that you have a vector index configu
 import os
 from griptape.tools import WebScraper, VectorStoreClient, TaskMemoryClient
 from griptape.structures import Agent
-from griptape.drivers import AzureOpenAiChatPromptDriver, AzureOpenAiEmbeddingDriver, AzureMongoDbVectorStoreDriver
+from griptape.drivers import AzureOpenAiEmbeddingDriver, AzureMongoDbVectorStoreDriver
 from griptape.engines import VectorQueryEngine, PromptSummaryEngine, CsvExtractionEngine, JsonExtractionEngine
 from griptape.memory import TaskMemory 
 from griptape.artifacts import TextArtifact
 from griptape.memory.task.storage import TextArtifactStorage
-from griptape.config import StructureConfig
+from griptape.config import AzureOpenAiStructureConfig
 
 
 AZURE_OPENAI_ENDPOINT_1 = os.environ["AZURE_OPENAI_ENDPOINT_1"]
@@ -31,38 +31,32 @@ MONGODB_VECTOR_PATH = os.environ["MONGODB_VECTOR_PATH"]
 MONGODB_CONNECTION_STRING = f"mongodb+srv://{MONGODB_USERNAME}:{MONGODB_PASSWORD}@{MONGODB_HOST}/{MONGODB_DATABASE_NAME}?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"
 
 
-azure_embedding_driver = AzureOpenAiEmbeddingDriver(
+embedding_driver = AzureOpenAiEmbeddingDriver(
     model='text-embedding-ada-002',
     azure_endpoint=AZURE_OPENAI_ENDPOINT_1,
     api_key=AZURE_OPENAI_API_KEY_1,
-    azure_deployment='text-embedding-ada-002'
-)
-
-azure_prompt_driver = AzureOpenAiChatPromptDriver(
-    model='gpt-4',
-    azure_endpoint=AZURE_OPENAI_ENDPOINT_1,
-    api_key=AZURE_OPENAI_API_KEY_1,
-    azure_deployment='gpt-4'
 )
 
 mongo_driver = AzureMongoDbVectorStoreDriver(
     connection_string=MONGODB_CONNECTION_STRING,
     database_name=MONGODB_DATABASE_NAME,
     collection_name=MONGODB_COLLECTION_NAME,
-    embedding_driver=azure_embedding_driver,
+    embedding_driver=embedding_driver,
     index_name=MONGODB_INDEX_NAME,
-    vector_path=MONGODB_VECTOR_PATH
+    vector_path=MONGODB_VECTOR_PATH,
+)
+
+config = AzureOpenAiStructureConfig(
+    azure_endpoint=AZURE_OPENAI_ENDPOINT_1,
+    vector_store_driver=mongo_driver,
+    embedding_driver=embedding_driver,
 )
 
 loader = Agent(
     tools=[
-        WebScraper()
+        WebScraper(),
     ],
-    config=StructureConfig(
-        prompt_driver=azure_prompt_driver,
-        vector_store_driver=mongo_driver,
-        embedding_driver=azure_embedding_driver
-    ),
+    config=config,
 )
 asker = Agent(
     tools=[
@@ -70,6 +64,7 @@ asker = Agent(
     ],
     meta_memory=loader.meta_memory,
     task_memory=loader.task_memory,
+    config=config,
 )
 
 if __name__ == "__main__":
diff --git a/docs/griptape-framework/drivers/image-query-drivers.md b/docs/griptape-framework/drivers/image-query-drivers.md
index f643b09eb..7538496b1 100644
--- a/docs/griptape-framework/drivers/image-query-drivers.md
+++ b/docs/griptape-framework/drivers/image-query-drivers.md
@@ -62,7 +62,7 @@ print(result)
 ## OpenAiVisionImageQueryDriver
 
 !!! info
-    This Driver defaults to using the `gpt-4-vision-preview` model. As other multimodal models are released, they can be specified using the `model` field. While the `max_tokens` field is optional, it is recommended to set this to a value that corresponds to the desired response length. Without an explicit value, the model will default to very short responses. See [OpenAI's documentation](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them) for more information on how to relate token count to response length.
+    While the `max_tokens` field is optional, it is recommended to set this to a value that corresponds to the desired response length. Without an explicit value, the model will default to very short responses. See [OpenAI's documentation](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them) for more information on how to relate token count to response length.
 
 The [OpenAiVisionImageQueryDriver](../../reference/griptape/drivers/image_query/openai_vision_image_query_driver.md) is used to query images using the OpenAI Vision API. Here is an example of how to use it:
 
@@ -86,6 +86,37 @@ with open("tests/resources/mountain.png", "rb") as f:
 engine.run("Describe the weather in the image", [image_artifact])
 ```
 
+## AzureOpenAiVisionImageQueryDriver
+    
+!!! info
+    In order to use the `gpt-4-vision-preview` model on Azure OpenAI, the `gpt-4` model must be deployed with the version set to `vision-preview`. More information can be found in the [Azure OpenAI documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/gpt-with-vision).
+
+The [AzureOpenAiVisionImageQueryDriver](../../reference/griptape/drivers/image_query/azure_openai_vision_image_query_driver.md) is used to query images using the Azure OpenAI Vision API. Here is an example of how to use it:
+
+```python
+import os
+from griptape.drivers import AzureOpenAiVisionImageQueryDriver
+from griptape.engines import ImageQueryEngine
+from griptape.loaders import ImageLoader
+
+driver = AzureOpenAiVisionImageQueryDriver(
+    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT_3"],
+    api_key=os.environ["AZURE_OPENAI_API_KEY_3"],
+    model="gpt-4",
+    azure_deployment="gpt-4-vision-preview",
+    max_tokens=256,
+)
+
+engine = ImageQueryEngine(
+    image_query_driver=driver,
+)
+
+with open("tests/resources/mountain.png", "rb") as f:
+    image_artifact = ImageLoader().load(f.read())
+
+engine.run("Describe the weather in the image", [image_artifact])
+```
+
 ## AmazonBedrockImageQueryDriver
 
 The [Amazon Bedrock Image Query Driver](../../reference/griptape/drivers/image_query/amazon_bedrock_image_query_driver.md) provides multi-model access to image query models hosted by Amazon Bedrock. This Driver manages API calls to the Bedrock API, while the specific Model Drivers below format the API requests and parse the responses.
diff --git a/docs/griptape-framework/structures/config.md b/docs/griptape-framework/structures/config.md
index 817dfb7fe..374468639 100644
--- a/docs/griptape-framework/structures/config.md
+++ b/docs/griptape-framework/structures/config.md
@@ -22,6 +22,28 @@ agent = Agent(
 agent = Agent() # This is equivalent to the above
 ```
 
+#### Azure OpenAI
+
+The [Azure OpenAI Structure Config](../../reference/griptape/config/azure_openai_structure_config.md) provides default Drivers for Azure's OpenAI APIs.
+
+
+```python
+import os
+from griptape.structures import Agent
+from griptape.config import AzureOpenAiStructureConfig
+
+agent = Agent(
+    config=AzureOpenAiStructureConfig(
+        azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT_3"],
+        api_key=os.environ["AZURE_OPENAI_API_KEY_3"]
+    ).merge_config({
+        "image_query_driver": {
+            "azure_deployment": "gpt-4-vision-preview",
+        },
+    }),
+)
+```
+
 #### Amazon Bedrock
 The [Amazon Bedrock Structure Config](../../reference/griptape/config/amazon_bedrock_structure_config.md) provides default Drivers for Amazon Bedrock's APIs.
 
@@ -112,7 +134,11 @@ serialized_config = custom_config.to_json()
 deserialized_config = AmazonBedrockStructureConfig.from_json(serialized_config)
 
 agent = Agent(
-    config=deserialized_config,
+    config=deserialized_config.merge_config({
+        "prompt_driver" : {
+            "model": "anthropic.claude-3-sonnet-20240229-v1:0",
+        },
+    }),
 )
 ```
 
diff --git a/griptape/config/__init__.py b/griptape/config/__init__.py
index 849556017..7783b3886 100644
--- a/griptape/config/__init__.py
+++ b/griptape/config/__init__.py
@@ -4,6 +4,7 @@
 
 from .structure_config import StructureConfig
 from .openai_structure_config import OpenAiStructureConfig
+from .azure_openai_structure_config import AzureOpenAiStructureConfig
 from .amazon_bedrock_structure_config import AmazonBedrockStructureConfig
 from .anthropic_structure_config import AnthropicStructureConfig
 from .google_structure_config import GoogleStructureConfig
@@ -14,6 +15,7 @@
     "BaseStructureConfig",
     "StructureConfig",
     "OpenAiStructureConfig",
+    "AzureOpenAiStructureConfig",
     "AmazonBedrockStructureConfig",
     "AnthropicStructureConfig",
     "GoogleStructureConfig",
diff --git a/griptape/config/azure_openai_structure_config.py b/griptape/config/azure_openai_structure_config.py
new file mode 100644
index 000000000..8c7ce82d3
--- /dev/null
+++ b/griptape/config/azure_openai_structure_config.py
@@ -0,0 +1,102 @@
+from typing import Callable, Optional
+from attrs import Factory, define, field
+
+from griptape.config import StructureConfig
+from griptape.drivers import (
+    LocalVectorStoreDriver,
+    AzureOpenAiChatPromptDriver,
+    AzureOpenAiEmbeddingDriver,
+    AzureOpenAiImageGenerationDriver,
+    AzureOpenAiVisionImageQueryDriver,
+    BasePromptDriver,
+    BaseEmbeddingDriver,
+    BaseImageGenerationDriver,
+    BaseImageQueryDriver,
+    BaseVectorStoreDriver,
+)
+
+
+@define
+class AzureOpenAiStructureConfig(StructureConfig):
+    """Azure OpenAI Structure Configuration.
+
+    Attributes:
+        azure_endpoint: The endpoint for the Azure OpenAI instance.
+        azure_ad_token: An optional Azure Active Directory token.
+        azure_ad_token_provider: An optional Azure Active Directory token provider.
+        api_key: An optional Azure API key.
+        prompt_driver: An Azure OpenAI Chat Prompt Driver.
+        image_generation_driver: An Azure OpenAI Image Generation Driver.
+        image_query_driver: An Azure OpenAI Vision Image Query Driver.
+        embedding_driver: An Azure OpenAI Embedding Driver.
+        vector_store_driver: A Local Vector Store Driver.
+    """
+
+    azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
+    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
+    azure_ad_token_provider: Optional[Callable[[], str]] = field(
+        kw_only=True, default=None, metadata={"serializable": False}
+    )
+    api_key: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
+    prompt_driver: BasePromptDriver = field(
+        default=Factory(
+            lambda self: AzureOpenAiChatPromptDriver(
+                model="gpt-4",
+                azure_endpoint=self.azure_endpoint,
+                api_key=self.api_key,
+                azure_ad_token=self.azure_ad_token,
+                azure_ad_token_provider=self.azure_ad_token_provider,
+            ),
+            takes_self=True,
+        ),
+        metadata={"serializable": True},
+        kw_only=True,
+    )
+    image_generation_driver: BaseImageGenerationDriver = field(
+        default=Factory(
+            lambda self: AzureOpenAiImageGenerationDriver(
+                model="dall-e-2",
+                azure_endpoint=self.azure_endpoint,
+                api_key=self.api_key,
+                azure_ad_token=self.azure_ad_token,
+                azure_ad_token_provider=self.azure_ad_token_provider,
+                image_size="512x512",
+            ),
+            takes_self=True,
+        ),
+        metadata={"serializable": True},
+        kw_only=True,
+    )
+    image_query_driver: BaseImageQueryDriver = field(
+        default=Factory(
+            lambda self: AzureOpenAiVisionImageQueryDriver(
+                model="gpt-4",
+                azure_endpoint=self.azure_endpoint,
+                api_key=self.api_key,
+                azure_ad_token=self.azure_ad_token,
+                azure_ad_token_provider=self.azure_ad_token_provider,
+            ),
+            takes_self=True,
+        ),
+        metadata={"serializable": True},
+        kw_only=True,
+    )
+    embedding_driver: BaseEmbeddingDriver = field(
+        default=Factory(
+            lambda self: AzureOpenAiEmbeddingDriver(
+                model="text-embedding-3-small",
+                azure_endpoint=self.azure_endpoint,
+                api_key=self.api_key,
+                azure_ad_token=self.azure_ad_token,
+                azure_ad_token_provider=self.azure_ad_token_provider,
+            ),
+            takes_self=True,
+        ),
+        metadata={"serializable": True},
+        kw_only=True,
+    )
+    vector_store_driver: BaseVectorStoreDriver = field(
+        default=Factory(lambda self: LocalVectorStoreDriver(embedding_driver=self.embedding_driver), takes_self=True),
+        metadata={"serializable": True},
+        kw_only=True,
+    )
diff --git a/griptape/drivers/__init__.py b/griptape/drivers/__init__.py
index 7941ed61e..1d76ba30d 100644
--- a/griptape/drivers/__init__.py
+++ b/griptape/drivers/__init__.py
@@ -81,6 +81,7 @@
 from .image_query.dummy_image_query_driver import DummyImageQueryDriver
 from .image_query.openai_vision_image_query_driver import OpenAiVisionImageQueryDriver
 from .image_query.anthropic_image_query_driver import AnthropicImageQueryDriver
+from .image_query.azure_openai_vision_image_query_driver import AzureOpenAiVisionImageQueryDriver
 from .image_query.amazon_bedrock_image_query_driver import AmazonBedrockImageQueryDriver
 
 from .web_scraper.base_web_scraper_driver import BaseWebScraperDriver
@@ -175,6 +176,7 @@
     "BedrockClaudeImageQueryModelDriver",
     "BaseImageQueryDriver",
     "OpenAiVisionImageQueryDriver",
+    "AzureOpenAiVisionImageQueryDriver",
     "DummyImageQueryDriver",
     "AnthropicImageQueryDriver",
     "BaseMultiModelImageQueryDriver",
diff --git a/griptape/drivers/embedding/azure_openai_embedding_driver.py b/griptape/drivers/embedding/azure_openai_embedding_driver.py
index 3cf473c06..f2c3b8d39 100644
--- a/griptape/drivers/embedding/azure_openai_embedding_driver.py
+++ b/griptape/drivers/embedding/azure_openai_embedding_driver.py
@@ -11,7 +11,7 @@
 class AzureOpenAiEmbeddingDriver(OpenAiEmbeddingDriver):
     """
     Attributes:
-        azure_deployment: An Azure OpenAi deployment id.
+        azure_deployment: An optional Azure OpenAi deployment id. Defaults to the model name.
         azure_endpoint: An Azure OpenAi endpoint.
         azure_ad_token: An optional Azure Active Directory token.
         azure_ad_token_provider: An optional Azure Active Directory token provider.
@@ -20,7 +20,9 @@ class AzureOpenAiEmbeddingDriver(OpenAiEmbeddingDriver):
         client: An `openai.AzureOpenAI` client.
     """
 
-    azure_deployment: str = field(kw_only=True, metadata={"serializable": True})
+    azure_deployment: str = field(
+        kw_only=True, default=Factory(lambda self: self.model, takes_self=True), metadata={"serializable": True}
+    )
     azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
     azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
     azure_ad_token_provider: Optional[Callable[[], str]] = field(
diff --git a/griptape/drivers/image_generation/azure_openai_image_generation_driver.py b/griptape/drivers/image_generation/azure_openai_image_generation_driver.py
index d037656ca..f6cbbe1a9 100644
--- a/griptape/drivers/image_generation/azure_openai_image_generation_driver.py
+++ b/griptape/drivers/image_generation/azure_openai_image_generation_driver.py
@@ -12,7 +12,7 @@ class AzureOpenAiImageGenerationDriver(OpenAiImageGenerationDriver):
     """Driver for Azure-hosted OpenAI image generation API.
 
     Attributes:
-        azure_deployment: An Azure OpenAi deployment id.
+        azure_deployment: An optional Azure OpenAi deployment id. Defaults to the model name.
         azure_endpoint: An Azure OpenAi endpoint.
         azure_ad_token: An optional Azure Active Directory token.
         azure_ad_token_provider: An optional Azure Active Directory token provider.
@@ -20,9 +20,11 @@ class AzureOpenAiImageGenerationDriver(OpenAiImageGenerationDriver):
         client: An `openai.AzureOpenAI` client.
     """
 
-    azure_deployment: str = field(kw_only=True, metadata={"serializable": True})
+    azure_deployment: str = field(
+        kw_only=True, default=Factory(lambda self: self.model, takes_self=True), metadata={"serializable": True}
+    )
     azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
-    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": True})
+    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
     azure_ad_token_provider: Optional[Callable[[], str]] = field(
         kw_only=True, default=None, metadata={"serializable": False}
     )
diff --git a/griptape/drivers/image_query/azure_openai_vision_image_query_driver.py b/griptape/drivers/image_query/azure_openai_vision_image_query_driver.py
new file mode 100644
index 000000000..06b329c03
--- /dev/null
+++ b/griptape/drivers/image_query/azure_openai_vision_image_query_driver.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from typing import Callable, Optional
+
+from attr import define, field, Factory
+import openai
+from griptape.drivers.image_query.openai_vision_image_query_driver import OpenAiVisionImageQueryDriver
+
+
+@define
+class AzureOpenAiVisionImageQueryDriver(OpenAiVisionImageQueryDriver):
+    """Driver for Azure-hosted OpenAI image query API.
+
+    Attributes:
+        azure_deployment: An optional Azure OpenAi deployment id. Defaults to the model name.
+        azure_endpoint: An Azure OpenAi endpoint.
+        azure_ad_token: An optional Azure Active Directory token.
+        azure_ad_token_provider: An optional Azure Active Directory token provider.
+        api_version: An Azure OpenAi API version.
+        client: An `openai.AzureOpenAI` client.
+    """
+
+    azure_deployment: str = field(
+        kw_only=True, default=Factory(lambda self: self.model, takes_self=True), metadata={"serializable": True}
+    )
+    azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
+    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
+    azure_ad_token_provider: Optional[Callable[[], str]] = field(
+        kw_only=True, default=None, metadata={"serializable": False}
+    )
+    api_version: str = field(default="2024-02-01", kw_only=True, metadata={"serializable": True})
+    client: openai.AzureOpenAI = field(
+        default=Factory(
+            lambda self: openai.AzureOpenAI(
+                organization=self.organization,
+                api_key=self.api_key,
+                api_version=self.api_version,
+                azure_endpoint=self.azure_endpoint,
+                azure_deployment=self.azure_deployment,
+                azure_ad_token=self.azure_ad_token,
+                azure_ad_token_provider=self.azure_ad_token_provider,
+            ),
+            takes_self=True,
+        )
+    )
diff --git a/griptape/drivers/prompt/azure_openai_chat_prompt_driver.py b/griptape/drivers/prompt/azure_openai_chat_prompt_driver.py
index 9824c31a0..0583369c4 100644
--- a/griptape/drivers/prompt/azure_openai_chat_prompt_driver.py
+++ b/griptape/drivers/prompt/azure_openai_chat_prompt_driver.py
@@ -9,7 +9,7 @@
 class AzureOpenAiChatPromptDriver(OpenAiChatPromptDriver):
     """
     Attributes:
-        azure_deployment: An Azure OpenAi deployment id.
+        azure_deployment: An optional Azure OpenAi deployment id. Defaults to the model name.
         azure_endpoint: An Azure OpenAi endpoint.
         azure_ad_token: An optional Azure Active Directory token.
         azure_ad_token_provider: An optional Azure Active Directory token provider.
@@ -17,9 +17,11 @@ class AzureOpenAiChatPromptDriver(OpenAiChatPromptDriver):
         client: An `openai.AzureOpenAI` client.
     """
 
-    azure_deployment: str = field(kw_only=True, metadata={"serializable": True})
+    azure_deployment: str = field(
+        kw_only=True, default=Factory(lambda self: self.model, takes_self=True), metadata={"serializable": True}
+    )
     azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
-    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": True})
+    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
     azure_ad_token_provider: Optional[Callable[[], str]] = field(
         kw_only=True, default=None, metadata={"serializable": False}
     )
diff --git a/griptape/drivers/prompt/azure_openai_completion_prompt_driver.py b/griptape/drivers/prompt/azure_openai_completion_prompt_driver.py
index 805b45519..89f2651ef 100644
--- a/griptape/drivers/prompt/azure_openai_completion_prompt_driver.py
+++ b/griptape/drivers/prompt/azure_openai_completion_prompt_driver.py
@@ -8,7 +8,7 @@
 class AzureOpenAiCompletionPromptDriver(OpenAiCompletionPromptDriver):
     """
     Attributes:
-        azure_deployment: An Azure OpenAi deployment id.
+        azure_deployment: An optional Azure OpenAi deployment id. Defaults to the model name.
         azure_endpoint: An Azure OpenAi endpoint.
         azure_ad_token: An optional Azure Active Directory token.
         azure_ad_token_provider: An optional Azure Active Directory token provider.
@@ -16,9 +16,11 @@ class AzureOpenAiCompletionPromptDriver(OpenAiCompletionPromptDriver):
         client: An `openai.AzureOpenAI` client.
     """
 
-    azure_deployment: str = field(kw_only=True, metadata={"serializable": True})
+    azure_deployment: str = field(
+        kw_only=True, default=Factory(lambda self: self.model, takes_self=True), metadata={"serializable": True}
+    )
     azure_endpoint: str = field(kw_only=True, metadata={"serializable": True})
-    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": True})
+    azure_ad_token: Optional[str] = field(kw_only=True, default=None, metadata={"serializable": False})
     azure_ad_token_provider: Optional[Callable[[], str]] = field(
         kw_only=True, default=None, metadata={"serializable": False}
     )


Test patch:
diff --git a/.github/workflows/docs-integration-tests.yml b/.github/workflows/docs-integration-tests.yml
index 14051de7f..cb4add5ae 100644
--- a/.github/workflows/docs-integration-tests.yml
+++ b/.github/workflows/docs-integration-tests.yml
@@ -75,6 +75,8 @@ jobs:
       AZURE_OPENAI_API_KEY_1: ${{ secrets.INTEG_AZURE_OPENAI_API_KEY_1 }}
       AZURE_OPENAI_ENDPOINT_2: ${{ secrets.INTEG_AZURE_OPENAI_ENDPOINT_2 }}
       AZURE_OPENAI_API_KEY_2: ${{ secrets.INTEG_AZURE_OPENAI_API_KEY_2 }}
+      AZURE_OPENAI_ENDPOINT_3: ${{ secrets.INTEG_AZURE_OPENAI_ENDPOINT_3 }}
+      AZURE_OPENAI_API_KEY_3: ${{ secrets.INTEG_AZURE_OPENAI_API_KEY_3 }}
       AZURE_OPENAI_35_TURBO_16K_DEPLOYMENT_ID: ${{ secrets.INTEG_OPENAI_35_TURBO_16K_DEPLOYMENT_ID }}
       AZURE_OPENAI_DAVINCI_DEPLOYMENT_ID: ${{ secrets.INTEG_OPENAI_DAVINCI_DEPLOYMENT_ID }}
       AZURE_OPENAI_4_DEPLOYMENT_ID: ${{ secrets.INTEG_OPENAI_4_DEPLOYMENT_ID }}
diff --git a/tests/unit/config/test_azure_openai_structure_config.py b/tests/unit/config/test_azure_openai_structure_config.py
new file mode 100644
index 000000000..acec4526a
--- /dev/null
+++ b/tests/unit/config/test_azure_openai_structure_config.py
@@ -0,0 +1,97 @@
+from pytest import fixture
+from griptape.config import AzureOpenAiStructureConfig
+
+
+class TestAzureOpenAiStructureConfig:
+    @fixture(autouse=True)
+    def mock_openai(self, mocker):
+        return mocker.patch("openai.AzureOpenAI")
+
+    @fixture
+    def config(self):
+        return AzureOpenAiStructureConfig(
+            azure_endpoint="http://localhost:8080",
+            azure_ad_token="test-token",
+            azure_ad_token_provider=lambda: "test-provider",
+        )
+
+    def test_to_dict(self, config):
+        assert config.to_dict() == {
+            "type": "AzureOpenAiStructureConfig",
+            "azure_endpoint": "http://localhost:8080",
+            "prompt_driver": {
+                "type": "AzureOpenAiChatPromptDriver",
+                "base_url": None,
+                "model": "gpt-4",
+                "azure_deployment": "gpt-4",
+                "azure_endpoint": "http://localhost:8080",
+                "api_version": "2023-05-15",
+                "organization": None,
+                "response_format": None,
+                "seed": None,
+                "temperature": 0.1,
+                "max_tokens": None,
+                "stream": False,
+                "user": "",
+            },
+            "conversation_memory_driver": None,
+            "embedding_driver": {
+                "base_url": None,
+                "model": "text-embedding-3-small",
+                "api_version": "2023-05-15",
+                "azure_deployment": "text-embedding-3-small",
+                "azure_endpoint": "http://localhost:8080",
+                "organization": None,
+                "type": "AzureOpenAiEmbeddingDriver",
+            },
+            "image_generation_driver": {
+                "api_version": "2024-02-01",
+                "base_url": None,
+                "image_size": "512x512",
+                "model": "dall-e-2",
+                "azure_deployment": "dall-e-2",
+                "azure_endpoint": "http://localhost:8080",
+                "organization": None,
+                "quality": "standard",
+                "response_format": "b64_json",
+                "style": None,
+                "type": "AzureOpenAiImageGenerationDriver",
+            },
+            "text_to_speech_driver": {"type": "DummyTextToSpeechDriver"},
+            "image_query_driver": {
+                "base_url": None,
+                "image_quality": "auto",
+                "max_tokens": 256,
+                "model": "gpt-4",
+                "api_version": "2024-02-01",
+                "azure_deployment": "gpt-4",
+                "azure_endpoint": "http://localhost:8080",
+                "organization": None,
+                "type": "AzureOpenAiVisionImageQueryDriver",
+            },
+            "vector_store_driver": {
+                "embedding_driver": {
+                    "base_url": None,
+                    "model": "text-embedding-3-small",
+                    "api_version": "2023-05-15",
+                    "azure_deployment": "text-embedding-3-small",
+                    "azure_endpoint": "http://localhost:8080",
+                    "organization": None,
+                    "type": "AzureOpenAiEmbeddingDriver",
+                },
+                "type": "LocalVectorStoreDriver",
+            },
+        }
+
+    def test_from_dict(self, config: AzureOpenAiStructureConfig):
+        assert AzureOpenAiStructureConfig.from_dict(config.to_dict()).to_dict() == config.to_dict()
+
+        # override values in the dict config
+        # serialize and deserialize the config
+        new_config = config.merge_config(
+            {
+                "prompt_driver": {"azure_deployment": "new-test-gpt-4"},
+                "embedding_driver": {"model": "new-text-embedding-3-small"},
+            }
+        ).to_dict()
+        assert AzureOpenAiStructureConfig.from_dict(new_config).to_dict() == new_config
diff --git a/tests/unit/drivers/embedding/test_azure_openai_embedding_driver.py b/tests/unit/drivers/embedding/test_azure_openai_embedding_driver.py
index f22ad02f0..d2c20c043 100644
--- a/tests/unit/drivers/embedding/test_azure_openai_embedding_driver.py
+++ b/tests/unit/drivers/embedding/test_azure_openai_embedding_driver.py
@@ -23,6 +23,7 @@ def driver(self):
 
     def test_init(self, driver):
         assert driver
+        assert AzureOpenAiEmbeddingDriver(azure_endpoint="foobar", model="gpt-4").azure_deployment == "gpt-4"
 
     def test_embed_chunk(self, driver):
         assert driver.try_embed_chunk("foobar") == [0, 1, 0]
diff --git a/tests/unit/drivers/image_generation/test_azure_openai_image_generation_driver.py b/tests/unit/drivers/image_generation/test_azure_openai_image_generation_driver.py
index 13b122af7..2166bc28a 100644
--- a/tests/unit/drivers/image_generation/test_azure_openai_image_generation_driver.py
+++ b/tests/unit/drivers/image_generation/test_azure_openai_image_generation_driver.py
@@ -16,6 +16,12 @@ def driver(self):
 
     def test_init(self, driver):
         assert driver
+        assert (
+            AzureOpenAiImageGenerationDriver(
+                model="dall-e-3", client=Mock(), azure_endpoint="https://dalle.example.com", image_size="512x512"
+            ).azure_deployment
+            == "dall-e-3"
+        )
 
     def test_init_requires_endpoint(self):
         with pytest.raises(TypeError):
@@ -23,12 +29,6 @@ def test_init_requires_endpoint(self):
                 model="dall-e-3", client=Mock(), azure_deployment="dalle-deployment", image_size="512x512"
             )  # pyright: ignore
 
-    def test_init_requires_deployment(self):
-        with pytest.raises(TypeError):
-            AzureOpenAiImageGenerationDriver(
-                model="dall-e-3", client=Mock(), azure_endpoint="https://dalle.example.com", image_size="512x512"
-            )  # pyright: ignore
-
     def test_try_text_to_image(self, driver):
         driver.client.images.generate.return_value = Mock(data=[Mock(b64_json=b"aW1hZ2UgZGF0YQ==")])
 
diff --git a/tests/unit/drivers/image_query/test_azure_openai_image_query_driver.py b/tests/unit/drivers/image_query/test_azure_openai_image_query_driver.py
new file mode 100644
index 000000000..d3a34fe44
--- /dev/null
+++ b/tests/unit/drivers/image_query/test_azure_openai_image_query_driver.py
@@ -0,0 +1,70 @@
+import pytest
+from unittest.mock import Mock
+from griptape.drivers import AzureOpenAiVisionImageQueryDriver
+from griptape.artifacts import ImageArtifact
+
+
+class TestAzureOpenAiVisionImageQueryDriver:
+    @pytest.fixture
+    def mock_completion_create(self, mocker):
+        mock_chat_create = mocker.patch("openai.AzureOpenAI").return_value.chat.completions.create
+        mock_choice = Mock(message=Mock(content="expected_output_text"))
+        mock_chat_create.return_value.choices = [mock_choice]
+        return mock_chat_create
+
+    def test_init(self):
+        assert AzureOpenAiVisionImageQueryDriver(
+            azure_endpoint="test-endpoint", azure_deployment="test-deployment", model="gpt-4"
+        )
+        assert (
+            AzureOpenAiVisionImageQueryDriver(azure_endpoint="test-endpoint", model="gpt-4").azure_deployment == "gpt-4"
+        )
+
+    def test_try_query_defaults(self, mock_completion_create):
+        driver = AzureOpenAiVisionImageQueryDriver(
+            azure_endpoint="test-endpoint", azure_deployment="test-deployment", model="gpt-4"
+        )
+        test_prompt_string = "Prompt String"
+        test_binary_data = b"test-data"
+        test_image = ImageArtifact(value=test_binary_data, width=100, height=100, format="png")
+        text_artifact = driver.try_query(test_prompt_string, [test_image])
+
+        messages = self._expected_messages(test_prompt_string, test_image.base64)
+
+        mock_completion_create.assert_called_once_with(model=driver.model, messages=[messages], max_tokens=256)
+
+        assert text_artifact.value == "expected_output_text"
+
+    def test_try_query_max_tokens(self, mock_completion_create):
+        driver = AzureOpenAiVisionImageQueryDriver(
+            azure_endpoint="test-endpoint", azure_deployment="test-deployment", model="gpt-4", max_tokens=1024
+        )
+        test_prompt_string = "Prompt String"
+        test_binary_data = b"test-data"
+        test_image = ImageArtifact(value=test_binary_data, width=100, height=100, format="png")
+        driver.try_query(test_prompt_string, [test_image])
+
+        messages = self._expected_messages(test_prompt_string, test_image.base64)
+
+        mock_completion_create.assert_called_once_with(model=driver.model, messages=[messages], max_tokens=1024)
+
+    def test_try_query_multiple_choices(self, mock_completion_create):
+        mock_completion_create.return_value.choices.append(Mock(message=Mock(content="expected_output_text2")))
+        driver = AzureOpenAiVisionImageQueryDriver(
+            azure_endpoint="test-endpoint", azure_deployment="test-deployment", model="gpt-4"
+        )
+
+        with pytest.raises(Exception):
+            driver.try_query("Prompt String", [ImageArtifact(value=b"test-data", width=100, height=100, format="png")])
+
+    def _expected_messages(self, expected_prompt_string, expected_binary_data):
+        return {
+            "content": [
+                {"type": "text", "text": expected_prompt_string},
+                {
+                    "type": "image_url",
+                    "image_url": {"url": f"data:image/png;base64,{expected_binary_data}", "detail": "auto"},
+                },
+            ],
+            "role": "user",
+        }
diff --git a/tests/unit/drivers/prompt/test_azure_openai_chat_prompt_driver.py b/tests/unit/drivers/prompt/test_azure_openai_chat_prompt_driver.py
index ebf598570..9446d0520 100644
--- a/tests/unit/drivers/prompt/test_azure_openai_chat_prompt_driver.py
+++ b/tests/unit/drivers/prompt/test_azure_openai_chat_prompt_driver.py
@@ -26,6 +26,7 @@ def mock_chat_completion_stream_create(self, mocker):
 
     def test_init(self):
         assert AzureOpenAiChatPromptDriver(azure_endpoint="foobar", azure_deployment="foobar", model="gpt-4")
+        assert AzureOpenAiChatPromptDriver(azure_endpoint="foobar", model="gpt-4").azure_deployment == "gpt-4"
 
     def test_try_run(self, mock_chat_completion_create, prompt_stack, messages):
         # Given
diff --git a/tests/unit/drivers/prompt/test_azure_openai_completion_prompt_driver.py b/tests/unit/drivers/prompt/test_azure_openai_completion_prompt_driver.py
index cf92f64e9..65758843a 100644
--- a/tests/unit/drivers/prompt/test_azure_openai_completion_prompt_driver.py
+++ b/tests/unit/drivers/prompt/test_azure_openai_completion_prompt_driver.py
@@ -28,6 +28,10 @@ def test_init(self):
         assert AzureOpenAiCompletionPromptDriver(
             azure_endpoint="endpoint", azure_deployment="deployment", model="text-davinci-003"
         )
+        assert (
+            AzureOpenAiCompletionPromptDriver(azure_endpoint="endpoint", model="text-davinci-003").azure_deployment
+            == "text-davinci-003"
+        )
 
     def test_try_run(self, mock_completion_create, prompt_stack, prompt):
         # Given