diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 73a4167d2d..451b00c101 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.58.1" + ".": "1.59.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 12219ccaa1..1a7a7a5269 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 69 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-0d64ca9e45f51b4279f87b205eeb3a3576df98407698ce053f2e2302c1c08df1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-a39aca84ed97ebafb707ebd5221e2787c5a42ff3d98f2ffaea8a0dcd84cbcbcb.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6519747179..1f411fc397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 1.59.0 (2024-12-21) + +Full Changelog: [v1.58.1...v1.59.0](https://github.com/openai/openai-python/compare/v1.58.1...v1.59.0) + +### Features + +* **azure:** support for the Realtime API ([#1963](https://github.com/openai/openai-python/issues/1963)) ([9fda141](https://github.com/openai/openai-python/commit/9fda14172abdb66fe240aa7b4dc7cfae4faf1d73)) + + +### Chores + +* **realtime:** update docstrings ([#1964](https://github.com/openai/openai-python/issues/1964)) ([3dee863](https://github.com/openai/openai-python/commit/3dee863554d28272103e90a6a199ac196e92ff05)) + ## 1.58.1 (2024-12-17) Full Changelog: [v1.58.0...v1.58.1](https://github.com/openai/openai-python/compare/v1.58.0...v1.58.1) diff --git a/pyproject.toml b/pyproject.toml index fd55cf2b5d..127213c372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "1.58.1" +version = "1.59.0" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/_utils/__init__.py b/src/openai/_utils/__init__.py index af2c9bb77e..bd01c088dc 100644 --- a/src/openai/_utils/__init__.py +++ b/src/openai/_utils/__init__.py @@ -25,6 +25,7 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, parse_datetime as parse_datetime, + is_azure_client as is_azure_client, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,6 +33,7 @@ get_required_header as get_required_header, maybe_coerce_boolean as maybe_coerce_boolean, maybe_coerce_integer as maybe_coerce_integer, + is_async_azure_client as is_async_azure_client, ) from ._typing import ( is_list_type as is_list_type, diff --git a/src/openai/_utils/_utils.py b/src/openai/_utils/_utils.py index e5811bba42..d6734e6b8f 100644 --- a/src/openai/_utils/_utils.py +++ b/src/openai/_utils/_utils.py @@ -5,6 +5,7 @@ import inspect import functools from typing import ( + TYPE_CHECKING, Any, Tuple, Mapping, @@ -30,6 +31,9 @@ _SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) CallableT = TypeVar("CallableT", bound=Callable[..., Any]) +if TYPE_CHECKING: + from ..lib.azure import AzureOpenAI, AsyncAzureOpenAI + def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: return [item for sublist in t for item in sublist] @@ -412,3 +416,15 @@ def json_safe(data: object) -> object: return data.isoformat() return data + + +def is_azure_client(client: object) -> TypeGuard[AzureOpenAI]: + from ..lib.azure import AzureOpenAI + + return isinstance(client, AzureOpenAI) + + +def is_async_azure_client(client: object) -> TypeGuard[AsyncAzureOpenAI]: + from ..lib.azure import AsyncAzureOpenAI + + return isinstance(client, AsyncAzureOpenAI) diff --git a/src/openai/_version.py b/src/openai/_version.py index c08e68e11b..7719866b19 100644 --- a/src/openai/_version.py +++ b/src/openai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openai" -__version__ = "1.58.1" # x-release-please-version +__version__ = "1.59.0" # x-release-please-version diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 13d9f31838..f857d76e51 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -7,7 +7,7 @@ import httpx -from .._types import NOT_GIVEN, Omit, Timeout, NotGiven +from .._types import NOT_GIVEN, Omit, Query, Timeout, NotGiven from .._utils import is_given, is_mapping from .._client import OpenAI, AsyncOpenAI from .._compat import model_copy @@ -307,6 +307,21 @@ def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: return options + def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Query, dict[str, str]]: + auth_headers = {} + query = { + **extra_query, + "api-version": self._api_version, + "deployment": model, + } + if self.api_key != "": + auth_headers = {"api-key": self.api_key} + else: + token = self._get_azure_ad_token() + if token: + auth_headers = {"Authorization": f"Bearer {token}"} + return query, auth_headers + class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], AsyncOpenAI): @overload @@ -555,3 +570,18 @@ async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOp raise ValueError("Unable to handle auth") return options + + async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[Query, dict[str, str]]: + auth_headers = {} + query = { + **extra_query, + "api-version": self._api_version, + "deployment": model, + } + if self.api_key != "": + auth_headers = {"api-key": self.api_key} + else: + token = await self._get_azure_ad_token() + if token: + auth_headers = {"Authorization": f"Bearer {token}"} + return query, auth_headers diff --git a/src/openai/resources/beta/realtime/realtime.py b/src/openai/resources/beta/realtime/realtime.py index c79fd46217..b39b410ecf 100644 --- a/src/openai/resources/beta/realtime/realtime.py +++ b/src/openai/resources/beta/realtime/realtime.py @@ -21,9 +21,11 @@ ) from ...._types import NOT_GIVEN, Query, Headers, NotGiven from ...._utils import ( + is_azure_client, maybe_transform, strip_not_given, async_maybe_transform, + is_async_azure_client, ) from ...._compat import cached_property from ...._models import construct_type_unchecked @@ -319,11 +321,16 @@ async def __aenter__(self) -> AsyncRealtimeConnection: except ImportError as exc: raise OpenAIError("You need to install `openai[realtime]` to use this method") from exc + extra_query = self.__extra_query + auth_headers = self.__client.auth_headers + if is_async_azure_client(self.__client): + extra_query, auth_headers = await self.__client._configure_realtime(self.__model, extra_query) + url = self._prepare_url().copy_with( params={ **self.__client.base_url.params, "model": self.__model, - **self.__extra_query, + **extra_query, }, ) log.debug("Connecting to %s", url) @@ -336,7 +343,7 @@ async def __aenter__(self) -> AsyncRealtimeConnection: user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { - **self.__client.auth_headers, + **auth_headers, "OpenAI-Beta": "realtime=v1", }, self.__extra_headers, @@ -496,11 +503,16 @@ def __enter__(self) -> RealtimeConnection: except ImportError as exc: raise OpenAIError("You need to install `openai[realtime]` to use this method") from exc + extra_query = self.__extra_query + auth_headers = self.__client.auth_headers + if is_azure_client(self.__client): + extra_query, auth_headers = self.__client._configure_realtime(self.__model, extra_query) + url = self._prepare_url().copy_with( params={ **self.__client.base_url.params, "model": self.__model, - **self.__extra_query, + **extra_query, }, ) log.debug("Connecting to %s", url) @@ -513,7 +525,7 @@ def __enter__(self) -> RealtimeConnection: user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { - **self.__client.auth_headers, + **auth_headers, "OpenAI-Beta": "realtime=v1", }, self.__extra_headers, diff --git a/src/openai/types/beta/realtime/conversation_item_content.py b/src/openai/types/beta/realtime/conversation_item_content.py index b854aa0e0f..ab40a4a1a7 100644 --- a/src/openai/types/beta/realtime/conversation_item_content.py +++ b/src/openai/types/beta/realtime/conversation_item_content.py @@ -11,8 +11,9 @@ class ConversationItemContent(BaseModel): id: Optional[str] = None """ - ID of a previous conversation item (like a model response), used for - `item_reference` content types. + ID of a previous conversation item to reference (for `item_reference` content + types in `response.create` events). These can reference both client and server + created items. """ audio: Optional[str] = None diff --git a/src/openai/types/beta/realtime/conversation_item_content_param.py b/src/openai/types/beta/realtime/conversation_item_content_param.py index b354d78971..7a3a92a39d 100644 --- a/src/openai/types/beta/realtime/conversation_item_content_param.py +++ b/src/openai/types/beta/realtime/conversation_item_content_param.py @@ -10,8 +10,9 @@ class ConversationItemContentParam(TypedDict, total=False): id: str """ - ID of a previous conversation item (like a model response), used for - `item_reference` content types. + ID of a previous conversation item to reference (for `item_reference` content + types in `response.create` events). These can reference both client and server + created items. """ audio: str diff --git a/src/openai/types/beta/realtime/response_create_event.py b/src/openai/types/beta/realtime/response_create_event.py index 00ba1e5dad..e4e5e7c68f 100644 --- a/src/openai/types/beta/realtime/response_create_event.py +++ b/src/openai/types/beta/realtime/response_create_event.py @@ -89,7 +89,8 @@ class Response(BaseModel): tool_choice: Optional[str] = None """How the model chooses tools. - Options are `auto`, `none`, `required`, or specify a function. + Options are `auto`, `none`, `required`, or specify a function, like + `{"type": "function", "function": {"name": "my_function"}}`. """ tools: Optional[List[ResponseTool]] = None diff --git a/src/openai/types/beta/realtime/response_create_event_param.py b/src/openai/types/beta/realtime/response_create_event_param.py index 7c92b32df1..7a4b5f086a 100644 --- a/src/openai/types/beta/realtime/response_create_event_param.py +++ b/src/openai/types/beta/realtime/response_create_event_param.py @@ -90,7 +90,8 @@ class Response(TypedDict, total=False): tool_choice: str """How the model chooses tools. - Options are `auto`, `none`, `required`, or specify a function. + Options are `auto`, `none`, `required`, or specify a function, like + `{"type": "function", "function": {"name": "my_function"}}`. """ tools: Iterable[ResponseTool]