Commit 53e5ba4b

t-asutedjo <100279393+t-asutedjo@users.noreply.github.com>
2022-06-24 15:53:19
Add Azure AD authentication support (#92)
* Add Azure AD authentication support * Add azure_ad code examples * Remove debug print statement * Bump version
1 parent d4f2d0f
examples/azure/finetuning.ipynb
@@ -40,6 +40,35 @@
     "openai.api_version = '2022-03-01-preview' # this may change in the future"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Microsoft Active Directory Authentication\n",
+    "Instead of key based authentication, you can use Active Directory to authenticate using credential tokens. Uncomment the next code section to use credential based authentication:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "\"\"\"\n",
+    "from azure.identity import DefaultAzureCredential\n",
+    "\n",
+    "default_credential = DefaultAzureCredential()\n",
+    "token = default_credential.get_token(\"https://cognitiveservices.azure.com\")\n",
+    "\n",
+    "openai.api_type = 'azure_ad'\n",
+    "openai.api_key = token.token\n",
+    "openai.api_version = '2022-03-01-preview' # this may change in the future\n",
+    "\n",
+    "\n",
+    "openai.api_base = '' # Please add your endpoint here\n",
+    "\"\"\""
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -418,10 +447,10 @@
  ],
  "metadata": {
   "interpreter": {
-   "hash": "1efaa68c6557ae864f04a55d1c611eb06843d0ca160c97bf33f135c19475264d"
+   "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
   },
   "kernelspec": {
-   "display_name": "Python 3.8.10 ('openai-env')",
+   "display_name": "Python 3.8.10 64-bit",
    "language": "python",
    "name": "python3"
   },
openai/api_resources/abstract/api_resource.py
@@ -49,12 +49,12 @@ class APIResource(OpenAIObject):
         api_version = self.api_version or openai.api_version
         extn = quote_plus(id)
 
-        if self.typed_api_type == ApiType.AZURE:
+        if self.typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             if not api_version:
                 raise error.InvalidRequestError(
                     "An API version is required for the Azure API type."
                 )
-            
+
             if not operation:
                 base = self.class_url()
                 return "/%s%s/%s?api-version=%s" % (
@@ -72,13 +72,13 @@ class APIResource(OpenAIObject):
                 api_version
             )
 
-
         elif self.typed_api_type == ApiType.OPEN_AI:
             base = self.class_url()
             return "%s/%s" % (base, extn)
 
         else:
-            raise error.InvalidAPIType("Unsupported API type %s" % self.api_type)
+            raise error.InvalidAPIType(
+                "Unsupported API type %s" % self.api_type)
 
     # The `method_` and `url_` arguments are suffixed with an underscore to
     # avoid conflicting with actual request parameters in `params`.
@@ -111,7 +111,7 @@ class APIResource(OpenAIObject):
 
     @classmethod
     def _get_api_type_and_version(cls, api_type: str, api_version: str):
-        typed_api_type = ApiType.from_str(api_type) if api_type else ApiType.from_str(openai.api_type)
+        typed_api_type = ApiType.from_str(
+            api_type) if api_type else ApiType.from_str(openai.api_type)
         typed_api_version = api_version or openai.api_version
         return (typed_api_type, typed_api_version)
-
openai/api_resources/abstract/createable_api_resource.py
@@ -24,15 +24,17 @@ class CreateableAPIResource(APIResource):
             api_version=api_version,
             organization=organization,
         )
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             base = cls.class_url()
-            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix, base, api_version)
+            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix,
+                                            base, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = cls.class_url()
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
 
         response, _, api_key = requestor.request(
             "post", url, params, request_id=request_id
openai/api_resources/abstract/deletable_api_resource.py
@@ -4,21 +4,25 @@ from openai import error
 from openai.api_resources.abstract.api_resource import APIResource
 from openai.util import ApiType
 
+
 class DeletableAPIResource(APIResource):
     @classmethod
     def delete(cls, sid, api_type=None, api_version=None, **params):
         if isinstance(cls, APIResource):
-            raise ValueError(".delete may only be called as a class method now.")
+            raise ValueError(
+                ".delete may only be called as a class method now.")
 
         base = cls.class_url()
         extn = quote_plus(sid)
 
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
-        if typed_api_type == ApiType.AZURE:
-            url = "/%s%s/%s?api-version=%s" % (cls.azure_api_prefix, base, extn, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
+            url = "/%s%s/%s?api-version=%s" % (
+                cls.azure_api_prefix, base, extn, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = "%s/%s" % (base, extn)
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
-        
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
+
         return cls._static_request("delete", url, api_type=api_type, api_version=api_version, **params)
openai/api_resources/abstract/engine_api_resource.py
@@ -29,9 +29,10 @@ class EngineAPIResource(APIResource):
         # Namespaces are separated in object names with periods (.) and in URLs
         # with forward slashes (/), so replace the former with the latter.
         base = cls.OBJECT_NAME.replace(".", "/")  # type: ignore
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             if not api_version:
                 raise error.InvalidRequestError(
                     "An API version is required for the Azure API type."
@@ -107,7 +108,8 @@ class EngineAPIResource(APIResource):
         )
 
         if stream:
-            assert not isinstance(response, OpenAIResponse)  # must be an iterator
+            # must be an iterator
+            assert not isinstance(response, OpenAIResponse)
             return (
                 util.convert_to_openai_object(
                     line,
@@ -146,7 +148,7 @@ class EngineAPIResource(APIResource):
         extn = quote_plus(id)
         params_connector = '?'
 
-        if self.typed_api_type == ApiType.AZURE:
+        if self.typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             api_version = self.api_version or openai.api_version
             if not api_version:
                 raise error.InvalidRequestError(
@@ -163,13 +165,13 @@ class EngineAPIResource(APIResource):
             )
             params_connector = '&'
 
-
         elif self.typed_api_type == ApiType.OPEN_AI:
             base = self.class_url(self.engine, self.api_type, self.api_version)
             url = "%s/%s" % (base, extn)
 
         else:
-            raise error.InvalidAPIType("Unsupported API type %s" % self.api_type)
+            raise error.InvalidAPIType(
+                "Unsupported API type %s" % self.api_type)
 
         timeout = self.get("timeout")
         if timeout is not None:
openai/api_resources/abstract/listable_api_resource.py
@@ -27,15 +27,17 @@ class ListableAPIResource(APIResource):
             organization=organization,
         )
 
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             base = cls.class_url()
-            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix, base, api_version)
+            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix,
+                                            base, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = cls.class_url()
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
 
         response, _, api_key = requestor.request(
             "get", url, params, request_id=request_id
openai/api_resources/deployment.py
@@ -12,9 +12,11 @@ class Deployment(CreateableAPIResource, ListableAPIResource, DeletableAPIResourc
         """
         Creates a new deployment for the provided prompt and parameters.
         """
-        typed_api_type, _ = cls._get_api_type_and_version(kwargs.get("api_type", None), None)
-        if typed_api_type != util.ApiType.AZURE:
-            raise APIError("Deployment operations are only available for the Azure API type.")  
+        typed_api_type, _ = cls._get_api_type_and_version(
+            kwargs.get("api_type", None), None)
+        if typed_api_type not in (util.ApiType.AZURE, util.ApiType.AZURE_AD):
+            raise APIError(
+                "Deployment operations are only available for the Azure API type.")
 
         if kwargs.get("model", None) is None:
             raise InvalidRequestError(
@@ -28,9 +30,9 @@ class Deployment(CreateableAPIResource, ListableAPIResource, DeletableAPIResourc
                 "Must provide a 'scale_settings' parameter to create a Deployment.",
                 param="scale_settings",
             )
-        
+
         if "scale_type" not in scale_settings or \
-            (scale_settings["scale_type"].lower() == 'manual' and "capacity" not in scale_settings):
+                (scale_settings["scale_type"].lower() == 'manual' and "capacity" not in scale_settings):
             raise InvalidRequestError(
                 "The 'scale_settings' parameter contains invalid or incomplete values.",
                 param="scale_settings",
@@ -40,24 +42,30 @@ class Deployment(CreateableAPIResource, ListableAPIResource, DeletableAPIResourc
 
     @classmethod
     def list(cls, *args, **kwargs):
-        typed_api_type, _ = cls._get_api_type_and_version(kwargs.get("api_type", None), None)
-        if typed_api_type != util.ApiType.AZURE:
-            raise APIError("Deployment operations are only available for the Azure API type.")  
+        typed_api_type, _ = cls._get_api_type_and_version(
+            kwargs.get("api_type", None), None)
+        if typed_api_type not in (util.ApiType.AZURE, util.ApiType.AZURE_AD):
+            raise APIError(
+                "Deployment operations are only available for the Azure API type.")
 
         return super().list(*args, **kwargs)
 
     @classmethod
     def delete(cls, *args, **kwargs):
-        typed_api_type, _ = cls._get_api_type_and_version(kwargs.get("api_type", None), None)
-        if typed_api_type != util.ApiType.AZURE:
-            raise APIError("Deployment operations are only available for the Azure API type.")  
+        typed_api_type, _ = cls._get_api_type_and_version(
+            kwargs.get("api_type", None), None)
+        if typed_api_type not in (util.ApiType.AZURE, util.ApiType.AZURE_AD):
+            raise APIError(
+                "Deployment operations are only available for the Azure API type.")
 
         return super().delete(*args, **kwargs)
 
     @classmethod
     def retrieve(cls, *args, **kwargs):
-        typed_api_type, _ = cls._get_api_type_and_version(kwargs.get("api_type", None), None)
-        if typed_api_type != util.ApiType.AZURE:
-            raise APIError("Deployment operations are only available for the Azure API type.")  
+        typed_api_type, _ = cls._get_api_type_and_version(
+            kwargs.get("api_type", None), None)
+        if typed_api_type not in (util.ApiType.AZURE, util.ApiType.AZURE_AD):
+            raise APIError(
+                "Deployment operations are only available for the Azure API type.")
 
         return super().retrieve(*args, **kwargs)
openai/api_resources/engine.py
@@ -28,7 +28,7 @@ class Engine(ListableAPIResource, UpdateableAPIResource):
                 util.log_info("Waiting for model to warm up", error=e)
 
     def search(self, **params):
-        if self.typed_api_type == ApiType.AZURE:
+        if self.typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             return self.request("post", self.instance_url("search"), params)
         elif self.typed_api_type == ApiType.OPEN_AI:
             return self.request("post", self.instance_url() + "/search", params)
openai/api_resources/file.py
@@ -25,7 +25,8 @@ class File(ListableAPIResource, DeletableAPIResource):
         user_provided_filename=None,
     ):
         if purpose != "search" and model is not None:
-            raise ValueError("'model' is only meaningful if 'purpose' is 'search'")
+            raise ValueError(
+                "'model' is only meaningful if 'purpose' is 'search'")
         requestor = api_requestor.APIRequestor(
             api_key,
             api_base=api_base or openai.api_base,
@@ -33,15 +34,17 @@ class File(ListableAPIResource, DeletableAPIResource):
             api_version=api_version,
             organization=organization,
         )
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             base = cls.class_url()
-            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix, base, api_version)
+            url = "/%s%s?api-version=%s" % (cls.azure_api_prefix,
+                                            base, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = cls.class_url()
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
 
         # Set the filename on 'purpose' and 'model' to None so they are
         # interpreted as form data.
@@ -49,7 +52,8 @@ class File(ListableAPIResource, DeletableAPIResource):
         if model is not None:
             files.append(("model", (None, model)))
         if user_provided_filename is not None:
-            files.append(("file", (user_provided_filename, file, 'application/octet-stream')))
+            files.append(
+                ("file", (user_provided_filename, file, 'application/octet-stream')))
         else:
             files.append(("file", ("file", file, 'application/octet-stream')))
         response, _, api_key = requestor.request("post", url, files=files)
@@ -59,12 +63,12 @@ class File(ListableAPIResource, DeletableAPIResource):
 
     @classmethod
     def download(
-        cls, 
-        id, 
-        api_key=None, 
+        cls,
+        id,
+        api_key=None,
         api_base=None,
         api_type=None,
-        api_version=None, 
+        api_version=None,
         organization=None
     ):
         requestor = api_requestor.APIRequestor(
@@ -74,16 +78,18 @@ class File(ListableAPIResource, DeletableAPIResource):
             api_version=api_version,
             organization=organization,
         )
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
             base = cls.class_url()
-            url = "/%s%s/%s/content?api-version=%s" % (cls.azure_api_prefix, base, id, api_version)
+            url = "/%s%s/%s/content?api-version=%s" % (
+                cls.azure_api_prefix, base, id, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = f"{cls.class_url()}/{id}/content"
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
-        
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
+
         result = requestor.request_raw("get", url)
         if not 200 <= result.status_code < 300:
             raise requestor.handle_error_response(
openai/api_resources/fine_tune.py
@@ -28,13 +28,15 @@ class FineTune(ListableAPIResource, CreateableAPIResource, DeletableAPIResource)
         base = cls.class_url()
         extn = quote_plus(id)
 
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
-        if typed_api_type == ApiType.AZURE:
-            url = "/%s%s/%s/cancel?api-version=%s" % (cls.azure_api_prefix, base, extn, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
+            url = "/%s%s/%s/cancel?api-version=%s" % (
+                cls.azure_api_prefix, base, extn, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = "%s/%s/cancel" % (base, extn)
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
 
         instance = cls(id, api_key, **params)
         return instance.request("post", url, request_id=request_id)
@@ -62,15 +64,17 @@ class FineTune(ListableAPIResource, CreateableAPIResource, DeletableAPIResource)
             organization=organization,
         )
 
-        typed_api_type, api_version = cls._get_api_type_and_version(api_type, api_version)
+        typed_api_type, api_version = cls._get_api_type_and_version(
+            api_type, api_version)
 
-        if typed_api_type == ApiType.AZURE:
-            url = "/%s%s/%s/events?stream=true&api-version=%s" % (cls.azure_api_prefix, base, extn, api_version)
+        if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
+            url = "/%s%s/%s/events?stream=true&api-version=%s" % (
+                cls.azure_api_prefix, base, extn, api_version)
         elif typed_api_type == ApiType.OPEN_AI:
             url = "%s/%s/events?stream=true" % (base, extn)
         else:
-            raise error.InvalidAPIType('Unsupported API type %s' % api_type)            
-        
+            raise error.InvalidAPIType('Unsupported API type %s' % api_type)
+
         response, _, api_key = requestor.request(
             "get", url, params, stream=True, request_id=request_id
         )
openai/tests/test_api_requestor.py
@@ -37,7 +37,6 @@ def test_requestor_open_ai_headers() -> None:
     headers = api_requestor.request_headers(
         method="get", extra=headers, request_id="test_id"
     )
-    print(headers)
     assert "Test_Header" in headers
     assert headers["Test_Header"] == "Unit_Test_Header"
     assert "Authorization" in headers
@@ -51,8 +50,20 @@ def test_requestor_azure_headers() -> None:
     headers = api_requestor.request_headers(
         method="get", extra=headers, request_id="test_id"
     )
-    print(headers)
     assert "Test_Header" in headers
     assert headers["Test_Header"] == "Unit_Test_Header"
     assert "api-key" in headers
     assert headers["api-key"] == "test_key"
+
+
+@pytest.mark.requestor
+def test_requestor_azure_ad_headers() -> None:
+    api_requestor = APIRequestor(key="test_key", api_type="azure_ad")
+    headers = {"Test_Header": "Unit_Test_Header"}
+    headers = api_requestor.request_headers(
+        method="get", extra=headers, request_id="test_id"
+    )
+    assert "Test_Header" in headers
+    assert headers["Test_Header"] == "Unit_Test_Header"
+    assert "Authorization" in headers
+    assert headers["Authorization"] == "Bearer test_key"
openai/tests/test_url_composition.py
@@ -15,6 +15,15 @@ def test_completions_url_composition_azure() -> None:
     )
 
 
+@pytest.mark.url
+def test_completions_url_composition_azure_ad() -> None:
+    url = Completion.class_url("test_engine", "azure_ad", "2021-11-01-preview")
+    assert (
+        url
+        == "/openai/deployments/test_engine/completions?api-version=2021-11-01-preview"
+    )
+
+
 @pytest.mark.url
 def test_completions_url_composition_default() -> None:
     url = Completion.class_url("test_engine")
@@ -48,6 +57,21 @@ def test_completions_url_composition_instance_url_azure() -> None:
     )
 
 
+@pytest.mark.url
+def test_completions_url_composition_instance_url_azure_ad() -> None:
+    completion = Completion(
+        id="test_id",
+        engine="test_engine",
+        api_type="azure_ad",
+        api_version="2021-11-01-preview",
+    )
+    url = completion.instance_url()
+    assert (
+        url
+        == "/openai/deployments/test_engine/completions/test_id?api-version=2021-11-01-preview"
+    )
+
+
 @pytest.mark.url
 def test_completions_url_composition_instance_url_azure_no_version() -> None:
     completion = Completion(
@@ -78,7 +102,8 @@ def test_completions_url_composition_instance_url_open_ai() -> None:
 
 @pytest.mark.url
 def test_completions_url_composition_instance_url_invalid() -> None:
-    completion = Completion(id="test_id", engine="test_engine", api_type="invalid")
+    completion = Completion(
+        id="test_id", engine="test_engine", api_type="invalid")
     with pytest.raises(Exception):
         url = completion.instance_url()
 
@@ -101,7 +126,8 @@ def test_completions_url_composition_instance_url_timeout_azure() -> None:
 
 @pytest.mark.url
 def test_completions_url_composition_instance_url_timeout_openai() -> None:
-    completion = Completion(id="test_id", engine="test_engine", api_type="open_ai")
+    completion = Completion(
+        id="test_id", engine="test_engine", api_type="open_ai")
     completion["timeout"] = 12
     url = completion.instance_url()
     assert url == "/engines/test_engine/completions/test_id?timeout=12"
@@ -109,7 +135,8 @@ def test_completions_url_composition_instance_url_timeout_openai() -> None:
 
 @pytest.mark.url
 def test_engine_search_url_composition_azure() -> None:
-    engine = Engine(id="test_id", api_type="azure", api_version="2021-11-01-preview")
+    engine = Engine(id="test_id", api_type="azure",
+                    api_version="2021-11-01-preview")
     assert engine.api_type == "azure"
     assert engine.typed_api_type == ApiType.AZURE
     url = engine.instance_url("test_operation")
@@ -119,6 +146,19 @@ def test_engine_search_url_composition_azure() -> None:
     )
 
 
+@pytest.mark.url
+def test_engine_search_url_composition_azure_ad() -> None:
+    engine = Engine(id="test_id", api_type="azure_ad",
+                    api_version="2021-11-01-preview")
+    assert engine.api_type == "azure_ad"
+    assert engine.typed_api_type == ApiType.AZURE_AD
+    url = engine.instance_url("test_operation")
+    assert (
+        url
+        == "/openai/deployments/test_id/test_operation?api-version=2021-11-01-preview"
+    )
+
+
 @pytest.mark.url
 def test_engine_search_url_composition_azure_no_version() -> None:
     engine = Engine(id="test_id", api_type="azure", api_version=None)
@@ -130,11 +170,13 @@ def test_engine_search_url_composition_azure_no_version() -> None:
 
 @pytest.mark.url
 def test_engine_search_url_composition_azure_no_operation() -> None:
-    engine = Engine(id="test_id", api_type="azure", api_version="2021-11-01-preview")
+    engine = Engine(id="test_id", api_type="azure",
+                    api_version="2021-11-01-preview")
     assert engine.api_type == "azure"
     assert engine.typed_api_type == ApiType.AZURE
     assert engine.instance_url() == "/openai/engines/test_id?api-version=2021-11-01-preview"
 
+
 @pytest.mark.url
 def test_engine_search_url_composition_default() -> None:
     engine = Engine(id="test_id")
openai/__init__.py
@@ -32,7 +32,8 @@ api_key_path: Optional[str] = os.environ.get("OPENAI_API_KEY_PATH")
 organization = os.environ.get("OPENAI_ORGANIZATION")
 api_base = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
 api_type = os.environ.get("OPENAI_API_TYPE", "open_ai")
-api_version = "2022-03-01-preview" if api_type == "azure" else None
+api_version = "2022-03-01-preview" if api_type in (
+    "azure", "azure_ad", "azuread") else None
 verify_ssl_certs = True  # No effect. Certificates are always verified.
 proxy = None
 app_info = None
openai/util.py
@@ -20,7 +20,7 @@ __all__ = [
 
 api_key_to_header = (
     lambda api, key: {"Authorization": f"Bearer {key}"}
-    if api == ApiType.OPEN_AI
+    if api in (ApiType.OPEN_AI, ApiType.AZURE_AD)
     else {"api-key": f"{key}"}
 )
 
@@ -28,11 +28,14 @@ api_key_to_header = (
 class ApiType(Enum):
     AZURE = 1
     OPEN_AI = 2
+    AZURE_AD = 3
 
     @staticmethod
     def from_str(label):
         if label.lower() == "azure":
             return ApiType.AZURE
+        elif label.lower() in ("azure_ad", "azuread"):
+            return ApiType.AZURE_AD
         elif label.lower() in ("open_ai", "openai"):
             return ApiType.OPEN_AI
         else:
@@ -175,7 +178,8 @@ def default_api_key() -> str:
         with open(openai.api_key_path, "rt") as k:
             api_key = k.read().strip()
             if not api_key.startswith("sk-"):
-                raise ValueError(f"Malformed API key in {openai.api_key_path}.")
+                raise ValueError(
+                    f"Malformed API key in {openai.api_key_path}.")
             return api_key
     elif openai.api_key is not None:
         return openai.api_key
openai/version.py
@@ -1,1 +1,1 @@
-VERSION = "0.20.1"
+VERSION = "0.21.0"
README.md
@@ -65,13 +65,13 @@ openai.api_base = "https://example-endpoint.openai.azure.com"
 openai.api_version = "2021-11-01-preview"
 
 # create a completion
-completion = openai.Completion.create(engine="deployment-namme", prompt="Hello world")
+completion = openai.Completion.create(engine="deployment-name", prompt="Hello world")
 
 # print the completion
 print(completion.choices[0].text)
 
 # create a search and pass the deployment-name as the engine Id.
-search = openai.Engine(id="deployment-namme").search(documents=["White House", "hospital", "school"], query ="the president")
+search = openai.Engine(id="deployment-name").search(documents=["White House", "hospital", "school"], query ="the president")
 
 # print the search
 print(search)
@@ -81,6 +81,27 @@ Please note that for the moment, the Microsoft Azure endpoints can only be used
 For a detailed example on how to use fine-tuning and other operations using Azure endpoints, please check out the following Jupyter notebook:
 [Using Azure fine-tuning](https://github.com/openai/openai-python/blob/main/examples/azure/finetuning.ipynb)
 
+### Microsoft Azure Active Directory Authentication
+
+In order to use Microsoft Active Directory to authenticate to your Azure endpoint, you need to set the api_type to "azure_ad" and pass the acquired credential token to api_key. The rest of the parameters need to be set as specified in the previous section.
+
+
+```python
+from azure.identity import DefaultAzureCredential
+import openai
+
+# Request credential
+default_credential = DefaultAzureCredential()
+token = default_credential.get_token("https://cognitiveservices.azure.com")
+
+# Setup parameters
+openai.api_type = "azure_ad"
+openai.api_key = token.token
+openai.api_base = "https://example-endpoint.openai.azure.com/"
+openai.api_version = "2022-03-01-preview"
+
+# ...
+```
 ### Command-line interface
 
 This library additionally provides an `openai` command-line utility