diff --git a/src/openai/azure/__init__.py b/src/openai/azure/__init__.py index 805d97a52f..4f5a97d36c 100644 --- a/src/openai/azure/__init__.py +++ b/src/openai/azure/__init__.py @@ -1,9 +1,11 @@ from ._sync_client import AzureOpenAIClient from ._async_client import AsyncAzureOpenAIClient from ._credential import TokenCredential +from ._exceptions import ContentPolicyError __all__ = [ "AzureOpenAIClient", "TokenCredential", "AsyncAzureOpenAIClient", + "ContentPolicyError", ] \ No newline at end of file diff --git a/src/openai/azure/_azuremodels.py b/src/openai/azure/_azuremodels.py index bfc2f31fd4..a2b31e3d53 100644 --- a/src/openai/azure/_azuremodels.py +++ b/src/openai/azure/_azuremodels.py @@ -1,5 +1,26 @@ -from typing import TypedDict +from typing import Optional +from typing_extensions import TypedDict, Literal +from openai._models import BaseModel as BaseModel + class ChatExtensionConfiguration(TypedDict): type: str parameters: object + +# TODO: just copying in from other PR +class ContentFilterResult(BaseModel): + severity: Literal["safe", "low", "medium", "high"] + filtered: bool + + +class Error(BaseModel): + code: str + message: str + + +class ContentFilterResults(BaseModel): + hate: Optional[ContentFilterResult] + self_harm: Optional[ContentFilterResult] + violence: Optional[ContentFilterResult] + sexual: Optional[ContentFilterResult] + error: Optional[Error] diff --git a/src/openai/azure/_exceptions.py b/src/openai/azure/_exceptions.py new file mode 100644 index 0000000000..17bd69f8e6 --- /dev/null +++ b/src/openai/azure/_exceptions.py @@ -0,0 +1,17 @@ +from __future__ import annotations +import httpx +from openai import BadRequestError +from ._azuremodels import ContentFilterResults + + +class ContentPolicyError(BadRequestError): + code: str + message: str + content_filter_result: ContentFilterResults + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message=message, response=response, body=body) + self.code = body["error"]["code"] + self.message = body["error"]["message"] + self.error = body["error"] + self.content_filter_result = ContentFilterResults.construct(**body["error"]["innererror"]["content_filter_result"]) diff --git a/src/openai/azure/_sync_client.py b/src/openai/azure/_sync_client.py index ba7faccf20..2b26d31e3a 100644 --- a/src/openai/azure/_sync_client.py +++ b/src/openai/azure/_sync_client.py @@ -1,10 +1,11 @@ from typing_extensions import Literal, override from typing import Any, Callable, cast, List, Mapping, Dict, Optional, overload, Union import time +import json import httpx -from openai import Client, OpenAIError +from openai import Client, OpenAIError, BadRequestError from openai.types import ImagesResponse # These are types used in the public API surface area that are not exported as public @@ -21,6 +22,7 @@ # Azure specific types from ._credential import TokenCredential from ._azuremodels import ChatExtensionConfiguration +from ._exceptions import ContentPolicyError TIMEOUT_SECS = 600 @@ -399,7 +401,21 @@ def _request(self, *, options: FinalRequestOptions, **kwargs: Any) -> Any: options.url = f'openai/deployments/{model}/extensions' + options.url else: options.url = f'openai/deployments/{model}' + options.url - return super()._request(options=options, **kwargs) + try: + return super()._request(options=options, **kwargs) + except BadRequestError as err: + try: + body = json.loads(err.response.text) + except Exception: + raise err + + if body.get('error') and body['error'].get('code') == 'content_filter': + raise ContentPolicyError( + message=err.message, + response=err.response, + body=body + ) + raise err # Internal azure specific "helper" methods def _check_polling_response(self, response: httpx.Response, predicate: Callable[[httpx.Response], bool]) -> bool: