From 7f7925b4074ecbf879714698000e10fa0519d51a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:13:26 +0000 Subject: [PATCH 1/2] feat(api): add support for realtime calls --- .stats.yml | 8 +- api.md | 10 + src/openai/lib/_realtime.py | 92 +++ src/openai/resources/realtime/__init__.py | 14 + src/openai/resources/realtime/calls.py | 734 ++++++++++++++++++ src/openai/resources/realtime/realtime.py | 70 +- src/openai/types/realtime/__init__.py | 4 + .../types/realtime/call_accept_params.py | 107 +++ .../types/realtime/call_create_params.py | 17 + .../types/realtime/call_refer_params.py | 15 + .../types/realtime/call_reject_params.py | 15 + .../types/realtime/realtime_connect_params.py | 6 +- tests/api_resources/realtime/test_calls.py | 692 +++++++++++++++++ 13 files changed, 1770 insertions(+), 14 deletions(-) create mode 100644 src/openai/lib/_realtime.py create mode 100644 src/openai/resources/realtime/calls.py create mode 100644 src/openai/types/realtime/call_accept_params.py create mode 100644 src/openai/types/realtime/call_create_params.py create mode 100644 src/openai/types/realtime/call_refer_params.py create mode 100644 src/openai/types/realtime/call_reject_params.py create mode 100644 tests/api_resources/realtime/test_calls.py diff --git a/.stats.yml b/.stats.yml index 27f2ffc6db..d974760a99 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 118 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-e205b1f2da6a1f2caa229efa9ede63f2d3d2fedeeb2dd6ed3d880bafdcb0ab88.yml -openapi_spec_hash: c8aee2469a749f6a838b40c57e4b7b06 -config_hash: 45dcba51451ba532959c020a0ddbf23c +configured_endpoints: 123 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-fadefdc7c7e30df47c09df323669b242ff90ee08e51f304175ace5274e0aab49.yml +openapi_spec_hash: 6d20f639d9ff8a097a34962da6218231 +config_hash: 902654e60f5d659f2bfcfd903e17c46d diff --git a/api.md b/api.md index 1c80bd6e5f..d5331803ce 100644 --- a/api.md +++ b/api.md @@ -989,6 +989,16 @@ Methods: - client.realtime.client_secrets.create(\*\*params) -> ClientSecretCreateResponse +## Calls + +Methods: + +- client.realtime.calls.create(\*\*params) -> HttpxBinaryResponseContent +- client.realtime.calls.accept(call_id, \*\*params) -> None +- client.realtime.calls.hangup(call_id) -> None +- client.realtime.calls.refer(call_id, \*\*params) -> None +- client.realtime.calls.reject(call_id, \*\*params) -> None + # Conversations Types: diff --git a/src/openai/lib/_realtime.py b/src/openai/lib/_realtime.py new file mode 100644 index 0000000000..999d1e4463 --- /dev/null +++ b/src/openai/lib/_realtime.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import json +from typing_extensions import override + +import httpx + +from openai import _legacy_response +from openai._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from openai._utils import maybe_transform, async_maybe_transform +from openai._base_client import make_request_options +from openai.resources.realtime.calls import Calls, AsyncCalls +from openai.types.realtime.realtime_session_create_request_param import RealtimeSessionCreateRequestParam + +__all__ = ["_Calls", "_AsyncCalls"] + + +# Custom code to override the `create` method to have correct behavior with +# application/sdp and multipart/form-data. +# Ideally we can cutover to the generated code this overrides eventually and remove this. +class _Calls(Calls): + @override + def create( + self, + *, + sdp: str, + session: RealtimeSessionCreateRequestParam | Omit = omit, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> _legacy_response.HttpxBinaryResponseContent: + if session is omit: + extra_headers = {"Accept": "application/sdp", "Content-Type": "application/sdp", **(extra_headers or {})} + return self._post( + "/realtime/calls", + body=sdp.encode("utf-8"), + options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + extra_headers = {"Accept": "application/sdp", "Content-Type": "multipart/form-data", **(extra_headers or {})} + session_payload = maybe_transform(session, RealtimeSessionCreateRequestParam) + files = [ + ("sdp", (None, sdp.encode("utf-8"), "application/sdp")), + ("session", (None, json.dumps(session_payload).encode("utf-8"), "application/json")), + ] + return self._post( + "/realtime/calls", + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + +class _AsyncCalls(AsyncCalls): + @override + async def create( + self, + *, + sdp: str, + session: RealtimeSessionCreateRequestParam | Omit = omit, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> _legacy_response.HttpxBinaryResponseContent: + if session is omit: + extra_headers = {"Accept": "application/sdp", "Content-Type": "application/sdp", **(extra_headers or {})} + return await self._post( + "/realtime/calls", + body=sdp.encode("utf-8"), + options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + extra_headers = {"Accept": "application/sdp", "Content-Type": "multipart/form-data", **(extra_headers or {})} + session_payload = await async_maybe_transform(session, RealtimeSessionCreateRequestParam) + files = [ + ("sdp", (None, sdp.encode("utf-8"), "application/sdp")), + ("session", (None, json.dumps(session_payload).encode("utf-8"), "application/json")), + ] + return await self._post( + "/realtime/calls", + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) diff --git a/src/openai/resources/realtime/__init__.py b/src/openai/resources/realtime/__init__.py index 7a41de8648..c11841017f 100644 --- a/src/openai/resources/realtime/__init__.py +++ b/src/openai/resources/realtime/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .calls import ( + Calls, + AsyncCalls, + CallsWithRawResponse, + AsyncCallsWithRawResponse, + CallsWithStreamingResponse, + AsyncCallsWithStreamingResponse, +) from .realtime import ( Realtime, AsyncRealtime, @@ -24,6 +32,12 @@ "AsyncClientSecretsWithRawResponse", "ClientSecretsWithStreamingResponse", "AsyncClientSecretsWithStreamingResponse", + "Calls", + "AsyncCalls", + "CallsWithRawResponse", + "AsyncCallsWithRawResponse", + "CallsWithStreamingResponse", + "AsyncCallsWithStreamingResponse", "Realtime", "AsyncRealtime", "RealtimeWithRawResponse", diff --git a/src/openai/resources/realtime/calls.py b/src/openai/resources/realtime/calls.py new file mode 100644 index 0000000000..7dcea6b5cf --- /dev/null +++ b/src/openai/resources/realtime/calls.py @@ -0,0 +1,734 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_streamed_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.realtime import ( + call_refer_params, + call_accept_params, + call_create_params, + call_reject_params, +) +from ...types.responses.response_prompt_param import ResponsePromptParam +from ...types.realtime.realtime_truncation_param import RealtimeTruncationParam +from ...types.realtime.realtime_audio_config_param import RealtimeAudioConfigParam +from ...types.realtime.realtime_tools_config_param import RealtimeToolsConfigParam +from ...types.realtime.realtime_tracing_config_param import RealtimeTracingConfigParam +from ...types.realtime.realtime_tool_choice_config_param import RealtimeToolChoiceConfigParam +from ...types.realtime.realtime_session_create_request_param import RealtimeSessionCreateRequestParam + +__all__ = ["Calls", "AsyncCalls"] + + +class Calls(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CallsWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/openai/openai-python#accessing-raw-response-data-eg-headers + """ + return CallsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CallsWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/openai/openai-python#with_streaming_response + """ + return CallsWithStreamingResponse(self) + + def create( + self, + *, + sdp: str, + session: RealtimeSessionCreateRequestParam | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Create a new Realtime API call over WebRTC and receive the SDP answer needed to + complete the peer connection. + + Args: + sdp: WebRTC Session Description Protocol (SDP) offer generated by the caller. + + session: Realtime session object configuration. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/sdp", **(extra_headers or {})} + return self._post( + "/realtime/calls", + body=maybe_transform( + { + "sdp": sdp, + "session": session, + }, + call_create_params.CallCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + def accept( + self, + call_id: str, + *, + type: Literal["realtime"], + audio: RealtimeAudioConfigParam | Omit = omit, + include: List[Literal["item.input_audio_transcription.logprobs"]] | Omit = omit, + instructions: str | Omit = omit, + max_output_tokens: Union[int, Literal["inf"]] | Omit = omit, + model: Union[ + str, + Literal[ + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + ], + ] + | Omit = omit, + output_modalities: List[Literal["text", "audio"]] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + tool_choice: RealtimeToolChoiceConfigParam | Omit = omit, + tools: RealtimeToolsConfigParam | Omit = omit, + tracing: Optional[RealtimeTracingConfigParam] | Omit = omit, + truncation: RealtimeTruncationParam | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Accept an incoming SIP call and configure the realtime session that will handle + it. + + Args: + type: The type of session to create. Always `realtime` for the Realtime API. + + audio: Configuration for input and output audio. + + include: Additional fields to include in server outputs. + + `item.input_audio_transcription.logprobs`: Include logprobs for input audio + transcription. + + instructions: The default system instructions (i.e. system message) prepended to model calls. + This field allows the client to guide the model on desired responses. The model + can be instructed on response content and format, (e.g. "be extremely succinct", + "act friendly", "here are examples of good responses") and on audio behavior + (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + instructions are not guaranteed to be followed by the model, but they provide + guidance to the model on the desired behavior. + + Note that the server sets default instructions which will be used if this field + is not set and are visible in the `session.created` event at the start of the + session. + + max_output_tokens: Maximum number of output tokens for a single assistant response, inclusive of + tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + `inf` for the maximum available tokens for a given model. Defaults to `inf`. + + model: The Realtime model used for this session. + + output_modalities: The set of modalities the model can respond with. It defaults to `["audio"]`, + indicating that the model will respond with audio plus a transcript. `["text"]` + can be used to make the model respond with text only. It is not possible to + request both `text` and `audio` at the same time. + + prompt: Reference to a prompt template and its variables. + [Learn more](https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts). + + tool_choice: How the model chooses tools. Provide one of the string modes or force a specific + function/MCP tool. + + tools: Tools available to the model. + + tracing: Realtime API can write session traces to the + [Traces Dashboard](/logs?api=traces). Set to null to disable tracing. Once + tracing is enabled for a session, the configuration cannot be modified. + + `auto` will create a trace for the session with default values for the workflow + name, group id, and metadata. + + truncation: Controls how the realtime conversation is truncated prior to model inference. + The default is `auto`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/realtime/calls/{call_id}/accept", + body=maybe_transform( + { + "type": type, + "audio": audio, + "include": include, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "model": model, + "output_modalities": output_modalities, + "prompt": prompt, + "tool_choice": tool_choice, + "tools": tools, + "tracing": tracing, + "truncation": truncation, + }, + call_accept_params.CallAcceptParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def hangup( + self, + call_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + End an active Realtime API call, whether it was initiated over SIP or WebRTC. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/realtime/calls/{call_id}/hangup", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def refer( + self, + call_id: str, + *, + target_uri: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Transfer an active SIP call to a new destination using the SIP REFER verb. + + Args: + target_uri: URI that should appear in the SIP Refer-To header. Supports values like + `tel:+14155550123` or `sip:agent@example.com`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/realtime/calls/{call_id}/refer", + body=maybe_transform({"target_uri": target_uri}, call_refer_params.CallReferParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def reject( + self, + call_id: str, + *, + status_code: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Decline an incoming SIP call by returning a SIP status code to the caller. + + Args: + status_code: SIP response code to send back to the caller. Defaults to `603` (Decline) when + omitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/realtime/calls/{call_id}/reject", + body=maybe_transform({"status_code": status_code}, call_reject_params.CallRejectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncCalls(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCallsWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/openai/openai-python#accessing-raw-response-data-eg-headers + """ + return AsyncCallsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCallsWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/openai/openai-python#with_streaming_response + """ + return AsyncCallsWithStreamingResponse(self) + + async def create( + self, + *, + sdp: str, + session: RealtimeSessionCreateRequestParam | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Create a new Realtime API call over WebRTC and receive the SDP answer needed to + complete the peer connection. + + Args: + sdp: WebRTC Session Description Protocol (SDP) offer generated by the caller. + + session: Realtime session object configuration. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/sdp", **(extra_headers or {})} + return await self._post( + "/realtime/calls", + body=await async_maybe_transform( + { + "sdp": sdp, + "session": session, + }, + call_create_params.CallCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + async def accept( + self, + call_id: str, + *, + type: Literal["realtime"], + audio: RealtimeAudioConfigParam | Omit = omit, + include: List[Literal["item.input_audio_transcription.logprobs"]] | Omit = omit, + instructions: str | Omit = omit, + max_output_tokens: Union[int, Literal["inf"]] | Omit = omit, + model: Union[ + str, + Literal[ + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + ], + ] + | Omit = omit, + output_modalities: List[Literal["text", "audio"]] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + tool_choice: RealtimeToolChoiceConfigParam | Omit = omit, + tools: RealtimeToolsConfigParam | Omit = omit, + tracing: Optional[RealtimeTracingConfigParam] | Omit = omit, + truncation: RealtimeTruncationParam | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Accept an incoming SIP call and configure the realtime session that will handle + it. + + Args: + type: The type of session to create. Always `realtime` for the Realtime API. + + audio: Configuration for input and output audio. + + include: Additional fields to include in server outputs. + + `item.input_audio_transcription.logprobs`: Include logprobs for input audio + transcription. + + instructions: The default system instructions (i.e. system message) prepended to model calls. + This field allows the client to guide the model on desired responses. The model + can be instructed on response content and format, (e.g. "be extremely succinct", + "act friendly", "here are examples of good responses") and on audio behavior + (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + instructions are not guaranteed to be followed by the model, but they provide + guidance to the model on the desired behavior. + + Note that the server sets default instructions which will be used if this field + is not set and are visible in the `session.created` event at the start of the + session. + + max_output_tokens: Maximum number of output tokens for a single assistant response, inclusive of + tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + `inf` for the maximum available tokens for a given model. Defaults to `inf`. + + model: The Realtime model used for this session. + + output_modalities: The set of modalities the model can respond with. It defaults to `["audio"]`, + indicating that the model will respond with audio plus a transcript. `["text"]` + can be used to make the model respond with text only. It is not possible to + request both `text` and `audio` at the same time. + + prompt: Reference to a prompt template and its variables. + [Learn more](https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts). + + tool_choice: How the model chooses tools. Provide one of the string modes or force a specific + function/MCP tool. + + tools: Tools available to the model. + + tracing: Realtime API can write session traces to the + [Traces Dashboard](/logs?api=traces). Set to null to disable tracing. Once + tracing is enabled for a session, the configuration cannot be modified. + + `auto` will create a trace for the session with default values for the workflow + name, group id, and metadata. + + truncation: Controls how the realtime conversation is truncated prior to model inference. + The default is `auto`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/realtime/calls/{call_id}/accept", + body=await async_maybe_transform( + { + "type": type, + "audio": audio, + "include": include, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "model": model, + "output_modalities": output_modalities, + "prompt": prompt, + "tool_choice": tool_choice, + "tools": tools, + "tracing": tracing, + "truncation": truncation, + }, + call_accept_params.CallAcceptParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def hangup( + self, + call_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + End an active Realtime API call, whether it was initiated over SIP or WebRTC. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/realtime/calls/{call_id}/hangup", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def refer( + self, + call_id: str, + *, + target_uri: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Transfer an active SIP call to a new destination using the SIP REFER verb. + + Args: + target_uri: URI that should appear in the SIP Refer-To header. Supports values like + `tel:+14155550123` or `sip:agent@example.com`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/realtime/calls/{call_id}/refer", + body=await async_maybe_transform({"target_uri": target_uri}, call_refer_params.CallReferParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def reject( + self, + call_id: str, + *, + status_code: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Decline an incoming SIP call by returning a SIP status code to the caller. + + Args: + status_code: SIP response code to send back to the caller. Defaults to `603` (Decline) when + omitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not call_id: + raise ValueError(f"Expected a non-empty value for `call_id` but received {call_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/realtime/calls/{call_id}/reject", + body=await async_maybe_transform({"status_code": status_code}, call_reject_params.CallRejectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class CallsWithRawResponse: + def __init__(self, calls: Calls) -> None: + self._calls = calls + + self.create = _legacy_response.to_raw_response_wrapper( + calls.create, + ) + self.accept = _legacy_response.to_raw_response_wrapper( + calls.accept, + ) + self.hangup = _legacy_response.to_raw_response_wrapper( + calls.hangup, + ) + self.refer = _legacy_response.to_raw_response_wrapper( + calls.refer, + ) + self.reject = _legacy_response.to_raw_response_wrapper( + calls.reject, + ) + + +class AsyncCallsWithRawResponse: + def __init__(self, calls: AsyncCalls) -> None: + self._calls = calls + + self.create = _legacy_response.async_to_raw_response_wrapper( + calls.create, + ) + self.accept = _legacy_response.async_to_raw_response_wrapper( + calls.accept, + ) + self.hangup = _legacy_response.async_to_raw_response_wrapper( + calls.hangup, + ) + self.refer = _legacy_response.async_to_raw_response_wrapper( + calls.refer, + ) + self.reject = _legacy_response.async_to_raw_response_wrapper( + calls.reject, + ) + + +class CallsWithStreamingResponse: + def __init__(self, calls: Calls) -> None: + self._calls = calls + + self.create = to_custom_streamed_response_wrapper( + calls.create, + StreamedBinaryAPIResponse, + ) + self.accept = to_streamed_response_wrapper( + calls.accept, + ) + self.hangup = to_streamed_response_wrapper( + calls.hangup, + ) + self.refer = to_streamed_response_wrapper( + calls.refer, + ) + self.reject = to_streamed_response_wrapper( + calls.reject, + ) + + +class AsyncCallsWithStreamingResponse: + def __init__(self, calls: AsyncCalls) -> None: + self._calls = calls + + self.create = async_to_custom_streamed_response_wrapper( + calls.create, + AsyncStreamedBinaryAPIResponse, + ) + self.accept = async_to_streamed_response_wrapper( + calls.accept, + ) + self.hangup = async_to_streamed_response_wrapper( + calls.hangup, + ) + self.refer = async_to_streamed_response_wrapper( + calls.refer, + ) + self.reject = async_to_streamed_response_wrapper( + calls.reject, + ) diff --git a/src/openai/resources/realtime/realtime.py b/src/openai/resources/realtime/realtime.py index 9d61fa25e0..7b4486d502 100644 --- a/src/openai/resources/realtime/realtime.py +++ b/src/openai/resources/realtime/realtime.py @@ -11,6 +11,14 @@ import httpx from pydantic import BaseModel +from .calls import ( + Calls, + AsyncCalls, + CallsWithRawResponse, + AsyncCallsWithRawResponse, + CallsWithStreamingResponse, + AsyncCallsWithStreamingResponse, +) from ..._types import Omit, Query, Headers, omit from ..._utils import ( is_azure_client, @@ -56,6 +64,11 @@ class Realtime(SyncAPIResource): def client_secrets(self) -> ClientSecrets: return ClientSecrets(self._client) + @cached_property + def calls(self) -> Calls: + from ...lib._realtime import _Calls + return _Calls(self._client) + @cached_property def with_raw_response(self) -> RealtimeWithRawResponse: """ @@ -78,7 +91,8 @@ def with_streaming_response(self) -> RealtimeWithStreamingResponse: def connect( self, *, - model: str, + call_id: str | Omit = omit, + model: str | Omit = omit, extra_query: Query = {}, extra_headers: Headers = {}, websocket_connection_options: WebsocketConnectionOptions = {}, @@ -99,6 +113,7 @@ def connect( extra_query=extra_query, extra_headers=extra_headers, websocket_connection_options=websocket_connection_options, + call_id=call_id, model=model, ) @@ -108,6 +123,11 @@ class AsyncRealtime(AsyncAPIResource): def client_secrets(self) -> AsyncClientSecrets: return AsyncClientSecrets(self._client) + @cached_property + def calls(self) -> AsyncCalls: + from ...lib._realtime import _AsyncCalls + return _AsyncCalls(self._client) + @cached_property def with_raw_response(self) -> AsyncRealtimeWithRawResponse: """ @@ -130,7 +150,8 @@ def with_streaming_response(self) -> AsyncRealtimeWithStreamingResponse: def connect( self, *, - model: str, + call_id: str | Omit = omit, + model: str | Omit = omit, extra_query: Query = {}, extra_headers: Headers = {}, websocket_connection_options: WebsocketConnectionOptions = {}, @@ -151,6 +172,7 @@ def connect( extra_query=extra_query, extra_headers=extra_headers, websocket_connection_options=websocket_connection_options, + call_id=call_id, model=model, ) @@ -163,6 +185,10 @@ def __init__(self, realtime: Realtime) -> None: def client_secrets(self) -> ClientSecretsWithRawResponse: return ClientSecretsWithRawResponse(self._realtime.client_secrets) + @cached_property + def calls(self) -> CallsWithRawResponse: + return CallsWithRawResponse(self._realtime.calls) + class AsyncRealtimeWithRawResponse: def __init__(self, realtime: AsyncRealtime) -> None: @@ -172,6 +198,10 @@ def __init__(self, realtime: AsyncRealtime) -> None: def client_secrets(self) -> AsyncClientSecretsWithRawResponse: return AsyncClientSecretsWithRawResponse(self._realtime.client_secrets) + @cached_property + def calls(self) -> AsyncCallsWithRawResponse: + return AsyncCallsWithRawResponse(self._realtime.calls) + class RealtimeWithStreamingResponse: def __init__(self, realtime: Realtime) -> None: @@ -181,6 +211,10 @@ def __init__(self, realtime: Realtime) -> None: def client_secrets(self) -> ClientSecretsWithStreamingResponse: return ClientSecretsWithStreamingResponse(self._realtime.client_secrets) + @cached_property + def calls(self) -> CallsWithStreamingResponse: + return CallsWithStreamingResponse(self._realtime.calls) + class AsyncRealtimeWithStreamingResponse: def __init__(self, realtime: AsyncRealtime) -> None: @@ -190,6 +224,10 @@ def __init__(self, realtime: AsyncRealtime) -> None: def client_secrets(self) -> AsyncClientSecretsWithStreamingResponse: return AsyncClientSecretsWithStreamingResponse(self._realtime.client_secrets) + @cached_property + def calls(self) -> AsyncCallsWithStreamingResponse: + return AsyncCallsWithStreamingResponse(self._realtime.calls) + class AsyncRealtimeConnection: """Represents a live websocket connection to the Realtime API""" @@ -290,12 +328,14 @@ def __init__( self, *, client: AsyncOpenAI, - model: str, + call_id: str | Omit = omit, + model: str | Omit = omit, extra_query: Query, extra_headers: Headers, websocket_connection_options: WebsocketConnectionOptions, ) -> None: self.__client = client + self.__call_id = call_id self.__model = model self.__connection: AsyncRealtimeConnection | None = None self.__extra_query = extra_query @@ -323,13 +363,19 @@ async def __aenter__(self) -> AsyncRealtimeConnection: extra_query = self.__extra_query await self.__client._refresh_api_key() auth_headers = self.__client.auth_headers + if self.__call_id is not omit: + extra_query = {**extra_query, "call_id": self.__call_id} if is_async_azure_client(self.__client): - url, auth_headers = await self.__client._configure_realtime(self.__model, extra_query) + model = self.__model + if not model: + raise OpenAIError("`model` is required for Azure Realtime API") + else: + url, auth_headers = await self.__client._configure_realtime(model, extra_query) else: url = self._prepare_url().copy_with( params={ **self.__client.base_url.params, - "model": self.__model, + **({"model": self.__model} if self.__model is not omit else {}), **extra_query, }, ) @@ -470,12 +516,14 @@ def __init__( self, *, client: OpenAI, - model: str, + call_id: str | Omit = omit, + model: str | Omit = omit, extra_query: Query, extra_headers: Headers, websocket_connection_options: WebsocketConnectionOptions, ) -> None: self.__client = client + self.__call_id = call_id self.__model = model self.__connection: RealtimeConnection | None = None self.__extra_query = extra_query @@ -503,13 +551,19 @@ def __enter__(self) -> RealtimeConnection: extra_query = self.__extra_query self.__client._refresh_api_key() auth_headers = self.__client.auth_headers + if self.__call_id is not omit: + extra_query = {**extra_query, "call_id": self.__call_id} if is_azure_client(self.__client): - url, auth_headers = self.__client._configure_realtime(self.__model, extra_query) + model = self.__model + if not model: + raise OpenAIError("`model` is required for Azure Realtime API") + else: + url, auth_headers = self.__client._configure_realtime(model, extra_query) else: url = self._prepare_url().copy_with( params={ **self.__client.base_url.params, - "model": self.__model, + **({"model": self.__model} if self.__model is not omit else {}), **extra_query, }, ) diff --git a/src/openai/types/realtime/__init__.py b/src/openai/types/realtime/__init__.py index 2d947c8a2f..83e81a034a 100644 --- a/src/openai/types/realtime/__init__.py +++ b/src/openai/types/realtime/__init__.py @@ -3,8 +3,12 @@ from __future__ import annotations from .realtime_error import RealtimeError as RealtimeError +from .call_refer_params import CallReferParams as CallReferParams from .conversation_item import ConversationItem as ConversationItem from .realtime_response import RealtimeResponse as RealtimeResponse +from .call_accept_params import CallAcceptParams as CallAcceptParams +from .call_create_params import CallCreateParams as CallCreateParams +from .call_reject_params import CallRejectParams as CallRejectParams from .audio_transcription import AudioTranscription as AudioTranscription from .log_prob_properties import LogProbProperties as LogProbProperties from .realtime_truncation import RealtimeTruncation as RealtimeTruncation diff --git a/src/openai/types/realtime/call_accept_params.py b/src/openai/types/realtime/call_accept_params.py new file mode 100644 index 0000000000..1780572e89 --- /dev/null +++ b/src/openai/types/realtime/call_accept_params.py @@ -0,0 +1,107 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from typing_extensions import Literal, Required, TypedDict + +from .realtime_truncation_param import RealtimeTruncationParam +from .realtime_audio_config_param import RealtimeAudioConfigParam +from .realtime_tools_config_param import RealtimeToolsConfigParam +from .realtime_tracing_config_param import RealtimeTracingConfigParam +from ..responses.response_prompt_param import ResponsePromptParam +from .realtime_tool_choice_config_param import RealtimeToolChoiceConfigParam + +__all__ = ["CallAcceptParams"] + + +class CallAcceptParams(TypedDict, total=False): + type: Required[Literal["realtime"]] + """The type of session to create. Always `realtime` for the Realtime API.""" + + audio: RealtimeAudioConfigParam + """Configuration for input and output audio.""" + + include: List[Literal["item.input_audio_transcription.logprobs"]] + """Additional fields to include in server outputs. + + `item.input_audio_transcription.logprobs`: Include logprobs for input audio + transcription. + """ + + instructions: str + """The default system instructions (i.e. + + system message) prepended to model calls. This field allows the client to guide + the model on desired responses. The model can be instructed on response content + and format, (e.g. "be extremely succinct", "act friendly", "here are examples of + good responses") and on audio behavior (e.g. "talk quickly", "inject emotion + into your voice", "laugh frequently"). The instructions are not guaranteed to be + followed by the model, but they provide guidance to the model on the desired + behavior. + + Note that the server sets default instructions which will be used if this field + is not set and are visible in the `session.created` event at the start of the + session. + """ + + max_output_tokens: Union[int, Literal["inf"]] + """ + Maximum number of output tokens for a single assistant response, inclusive of + tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + `inf` for the maximum available tokens for a given model. Defaults to `inf`. + """ + + model: Union[ + str, + Literal[ + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + ], + ] + """The Realtime model used for this session.""" + + output_modalities: List[Literal["text", "audio"]] + """The set of modalities the model can respond with. + + It defaults to `["audio"]`, indicating that the model will respond with audio + plus a transcript. `["text"]` can be used to make the model respond with text + only. It is not possible to request both `text` and `audio` at the same time. + """ + + prompt: Optional[ResponsePromptParam] + """ + Reference to a prompt template and its variables. + [Learn more](https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts). + """ + + tool_choice: RealtimeToolChoiceConfigParam + """How the model chooses tools. + + Provide one of the string modes or force a specific function/MCP tool. + """ + + tools: RealtimeToolsConfigParam + """Tools available to the model.""" + + tracing: Optional[RealtimeTracingConfigParam] + """ + Realtime API can write session traces to the + [Traces Dashboard](/logs?api=traces). Set to null to disable tracing. Once + tracing is enabled for a session, the configuration cannot be modified. + + `auto` will create a trace for the session with default values for the workflow + name, group id, and metadata. + """ + + truncation: RealtimeTruncationParam + """ + Controls how the realtime conversation is truncated prior to model inference. + The default is `auto`. + """ diff --git a/src/openai/types/realtime/call_create_params.py b/src/openai/types/realtime/call_create_params.py new file mode 100644 index 0000000000..a378092a66 --- /dev/null +++ b/src/openai/types/realtime/call_create_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .realtime_session_create_request_param import RealtimeSessionCreateRequestParam + +__all__ = ["CallCreateParams"] + + +class CallCreateParams(TypedDict, total=False): + sdp: Required[str] + """WebRTC Session Description Protocol (SDP) offer generated by the caller.""" + + session: RealtimeSessionCreateRequestParam + """Realtime session object configuration.""" diff --git a/src/openai/types/realtime/call_refer_params.py b/src/openai/types/realtime/call_refer_params.py new file mode 100644 index 0000000000..3d8623855b --- /dev/null +++ b/src/openai/types/realtime/call_refer_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CallReferParams"] + + +class CallReferParams(TypedDict, total=False): + target_uri: Required[str] + """URI that should appear in the SIP Refer-To header. + + Supports values like `tel:+14155550123` or `sip:agent@example.com`. + """ diff --git a/src/openai/types/realtime/call_reject_params.py b/src/openai/types/realtime/call_reject_params.py new file mode 100644 index 0000000000..f12222cded --- /dev/null +++ b/src/openai/types/realtime/call_reject_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CallRejectParams"] + + +class CallRejectParams(TypedDict, total=False): + status_code: int + """SIP response code to send back to the caller. + + Defaults to `603` (Decline) when omitted. + """ diff --git a/src/openai/types/realtime/realtime_connect_params.py b/src/openai/types/realtime/realtime_connect_params.py index 76474f3de4..950f36212f 100644 --- a/src/openai/types/realtime/realtime_connect_params.py +++ b/src/openai/types/realtime/realtime_connect_params.py @@ -2,10 +2,12 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["RealtimeConnectParams"] class RealtimeConnectParams(TypedDict, total=False): - model: Required[str] + call_id: str + + model: str diff --git a/tests/api_resources/realtime/test_calls.py b/tests/api_resources/realtime/test_calls.py new file mode 100644 index 0000000000..5495a58a4e --- /dev/null +++ b/tests/api_resources/realtime/test_calls.py @@ -0,0 +1,692 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +import openai._legacy_response as _legacy_response +from openai import OpenAI, AsyncOpenAI +from tests.utils import assert_matches_type + +# pyright: reportDeprecated=false + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCalls: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_create(self, client: OpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + call = client.realtime.calls.create( + sdp="sdp", + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert call.json() == {"foo": "bar"} + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_create_with_all_params(self, client: OpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + call = client.realtime.calls.create( + sdp="sdp", + session={ + "type": "realtime", + "audio": { + "input": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "noise_reduction": {"type": "near_field"}, + "transcription": { + "language": "language", + "model": "whisper-1", + "prompt": "prompt", + }, + "turn_detection": { + "type": "server_vad", + "create_response": True, + "idle_timeout_ms": 5000, + "interrupt_response": True, + "prefix_padding_ms": 0, + "silence_duration_ms": 0, + "threshold": 0, + }, + }, + "output": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "speed": 0.25, + "voice": "ash", + }, + }, + "include": ["item.input_audio_transcription.logprobs"], + "instructions": "instructions", + "max_output_tokens": 0, + "model": "string", + "output_modalities": ["text"], + "prompt": { + "id": "id", + "variables": {"foo": "string"}, + "version": "version", + }, + "tool_choice": "none", + "tools": [ + { + "description": "description", + "name": "name", + "parameters": {}, + "type": "function", + } + ], + "tracing": "auto", + "truncation": "auto", + }, + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert call.json() == {"foo": "bar"} + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_create(self, client: OpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.realtime.calls.with_raw_response.create( + sdp="sdp", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert_matches_type(_legacy_response.HttpxBinaryResponseContent, call, path=["response"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_create(self, client: OpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.realtime.calls.with_streaming_response.create( + sdp="sdp", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = response.parse() + assert_matches_type(bytes, call, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_accept(self, client: OpenAI) -> None: + call = client.realtime.calls.accept( + call_id="call_id", + type="realtime", + ) + assert call is None + + @parametrize + def test_method_accept_with_all_params(self, client: OpenAI) -> None: + call = client.realtime.calls.accept( + call_id="call_id", + type="realtime", + audio={ + "input": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "noise_reduction": {"type": "near_field"}, + "transcription": { + "language": "language", + "model": "whisper-1", + "prompt": "prompt", + }, + "turn_detection": { + "type": "server_vad", + "create_response": True, + "idle_timeout_ms": 5000, + "interrupt_response": True, + "prefix_padding_ms": 0, + "silence_duration_ms": 0, + "threshold": 0, + }, + }, + "output": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "speed": 0.25, + "voice": "ash", + }, + }, + include=["item.input_audio_transcription.logprobs"], + instructions="instructions", + max_output_tokens=0, + model="string", + output_modalities=["text"], + prompt={ + "id": "id", + "variables": {"foo": "string"}, + "version": "version", + }, + tool_choice="none", + tools=[ + { + "description": "description", + "name": "name", + "parameters": {}, + "type": "function", + } + ], + tracing="auto", + truncation="auto", + ) + assert call is None + + @parametrize + def test_raw_response_accept(self, client: OpenAI) -> None: + response = client.realtime.calls.with_raw_response.accept( + call_id="call_id", + type="realtime", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + def test_streaming_response_accept(self, client: OpenAI) -> None: + with client.realtime.calls.with_streaming_response.accept( + call_id="call_id", + type="realtime", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_accept(self, client: OpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + client.realtime.calls.with_raw_response.accept( + call_id="", + type="realtime", + ) + + @parametrize + def test_method_hangup(self, client: OpenAI) -> None: + call = client.realtime.calls.hangup( + "call_id", + ) + assert call is None + + @parametrize + def test_raw_response_hangup(self, client: OpenAI) -> None: + response = client.realtime.calls.with_raw_response.hangup( + "call_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + def test_streaming_response_hangup(self, client: OpenAI) -> None: + with client.realtime.calls.with_streaming_response.hangup( + "call_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_hangup(self, client: OpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + client.realtime.calls.with_raw_response.hangup( + "", + ) + + @parametrize + def test_method_refer(self, client: OpenAI) -> None: + call = client.realtime.calls.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) + assert call is None + + @parametrize + def test_raw_response_refer(self, client: OpenAI) -> None: + response = client.realtime.calls.with_raw_response.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + def test_streaming_response_refer(self, client: OpenAI) -> None: + with client.realtime.calls.with_streaming_response.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_refer(self, client: OpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + client.realtime.calls.with_raw_response.refer( + call_id="", + target_uri="tel:+14155550123", + ) + + @parametrize + def test_method_reject(self, client: OpenAI) -> None: + call = client.realtime.calls.reject( + call_id="call_id", + ) + assert call is None + + @parametrize + def test_method_reject_with_all_params(self, client: OpenAI) -> None: + call = client.realtime.calls.reject( + call_id="call_id", + status_code=486, + ) + assert call is None + + @parametrize + def test_raw_response_reject(self, client: OpenAI) -> None: + response = client.realtime.calls.with_raw_response.reject( + call_id="call_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + def test_streaming_response_reject(self, client: OpenAI) -> None: + with client.realtime.calls.with_streaming_response.reject( + call_id="call_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_reject(self, client: OpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + client.realtime.calls.with_raw_response.reject( + call_id="", + ) + + +class TestAsyncCalls: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_create(self, async_client: AsyncOpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + call = await async_client.realtime.calls.create( + sdp="sdp", + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert call.json() == {"foo": "bar"} + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_create_with_all_params(self, async_client: AsyncOpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + call = await async_client.realtime.calls.create( + sdp="sdp", + session={ + "type": "realtime", + "audio": { + "input": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "noise_reduction": {"type": "near_field"}, + "transcription": { + "language": "language", + "model": "whisper-1", + "prompt": "prompt", + }, + "turn_detection": { + "type": "server_vad", + "create_response": True, + "idle_timeout_ms": 5000, + "interrupt_response": True, + "prefix_padding_ms": 0, + "silence_duration_ms": 0, + "threshold": 0, + }, + }, + "output": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "speed": 0.25, + "voice": "ash", + }, + }, + "include": ["item.input_audio_transcription.logprobs"], + "instructions": "instructions", + "max_output_tokens": 0, + "model": "string", + "output_modalities": ["text"], + "prompt": { + "id": "id", + "variables": {"foo": "string"}, + "version": "version", + }, + "tool_choice": "none", + "tools": [ + { + "description": "description", + "name": "name", + "parameters": {}, + "type": "function", + } + ], + "tracing": "auto", + "truncation": "auto", + }, + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert call.json() == {"foo": "bar"} + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_create(self, async_client: AsyncOpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.realtime.calls.with_raw_response.create( + sdp="sdp", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert_matches_type(_legacy_response.HttpxBinaryResponseContent, call, path=["response"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_create(self, async_client: AsyncOpenAI, respx_mock: MockRouter) -> None: + respx_mock.post("/realtime/calls").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.realtime.calls.with_streaming_response.create( + sdp="sdp", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = await response.parse() + assert_matches_type(bytes, call, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_accept(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.accept( + call_id="call_id", + type="realtime", + ) + assert call is None + + @parametrize + async def test_method_accept_with_all_params(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.accept( + call_id="call_id", + type="realtime", + audio={ + "input": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "noise_reduction": {"type": "near_field"}, + "transcription": { + "language": "language", + "model": "whisper-1", + "prompt": "prompt", + }, + "turn_detection": { + "type": "server_vad", + "create_response": True, + "idle_timeout_ms": 5000, + "interrupt_response": True, + "prefix_padding_ms": 0, + "silence_duration_ms": 0, + "threshold": 0, + }, + }, + "output": { + "format": { + "rate": 24000, + "type": "audio/pcm", + }, + "speed": 0.25, + "voice": "ash", + }, + }, + include=["item.input_audio_transcription.logprobs"], + instructions="instructions", + max_output_tokens=0, + model="string", + output_modalities=["text"], + prompt={ + "id": "id", + "variables": {"foo": "string"}, + "version": "version", + }, + tool_choice="none", + tools=[ + { + "description": "description", + "name": "name", + "parameters": {}, + "type": "function", + } + ], + tracing="auto", + truncation="auto", + ) + assert call is None + + @parametrize + async def test_raw_response_accept(self, async_client: AsyncOpenAI) -> None: + response = await async_client.realtime.calls.with_raw_response.accept( + call_id="call_id", + type="realtime", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + async def test_streaming_response_accept(self, async_client: AsyncOpenAI) -> None: + async with async_client.realtime.calls.with_streaming_response.accept( + call_id="call_id", + type="realtime", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = await response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_accept(self, async_client: AsyncOpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + await async_client.realtime.calls.with_raw_response.accept( + call_id="", + type="realtime", + ) + + @parametrize + async def test_method_hangup(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.hangup( + "call_id", + ) + assert call is None + + @parametrize + async def test_raw_response_hangup(self, async_client: AsyncOpenAI) -> None: + response = await async_client.realtime.calls.with_raw_response.hangup( + "call_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + async def test_streaming_response_hangup(self, async_client: AsyncOpenAI) -> None: + async with async_client.realtime.calls.with_streaming_response.hangup( + "call_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = await response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_hangup(self, async_client: AsyncOpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + await async_client.realtime.calls.with_raw_response.hangup( + "", + ) + + @parametrize + async def test_method_refer(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) + assert call is None + + @parametrize + async def test_raw_response_refer(self, async_client: AsyncOpenAI) -> None: + response = await async_client.realtime.calls.with_raw_response.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + async def test_streaming_response_refer(self, async_client: AsyncOpenAI) -> None: + async with async_client.realtime.calls.with_streaming_response.refer( + call_id="call_id", + target_uri="tel:+14155550123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = await response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_refer(self, async_client: AsyncOpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + await async_client.realtime.calls.with_raw_response.refer( + call_id="", + target_uri="tel:+14155550123", + ) + + @parametrize + async def test_method_reject(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.reject( + call_id="call_id", + ) + assert call is None + + @parametrize + async def test_method_reject_with_all_params(self, async_client: AsyncOpenAI) -> None: + call = await async_client.realtime.calls.reject( + call_id="call_id", + status_code=486, + ) + assert call is None + + @parametrize + async def test_raw_response_reject(self, async_client: AsyncOpenAI) -> None: + response = await async_client.realtime.calls.with_raw_response.reject( + call_id="call_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + call = response.parse() + assert call is None + + @parametrize + async def test_streaming_response_reject(self, async_client: AsyncOpenAI) -> None: + async with async_client.realtime.calls.with_streaming_response.reject( + call_id="call_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + call = await response.parse() + assert call is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_reject(self, async_client: AsyncOpenAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `call_id` but received ''"): + await async_client.realtime.calls.with_raw_response.reject( + call_id="", + ) From 8a864874cfcb006f7c67384f8a0a3622faf96cb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:13:57 +0000 Subject: [PATCH 2/2] release: 2.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- src/openai/_version.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cf7239890c..656a2ef17d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.1" + ".": "2.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb9926c29..76d700f05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.1.0 (2025-10-02) + +Full Changelog: [v2.0.1...v2.1.0](https://github.com/openai/openai-python/compare/v2.0.1...v2.1.0) + +### Features + +* **api:** add support for realtime calls ([7f7925b](https://github.com/openai/openai-python/commit/7f7925b4074ecbf879714698000e10fa0519d51a)) + ## 2.0.1 (2025-10-01) Full Changelog: [v2.0.0...v2.0.1](https://github.com/openai/openai-python/compare/v2.0.0...v2.0.1) diff --git a/pyproject.toml b/pyproject.toml index a83bca2dcd..d8deac4e61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "2.0.1" +version = "2.1.0" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/_version.py b/src/openai/_version.py index f20794ac19..2860526ced 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__ = "2.0.1" # x-release-please-version +__version__ = "2.1.0" # x-release-please-version