From a9a8c5eb841f3c779112f0a42b3daace8bc8d273 Mon Sep 17 00:00:00 2001 From: yoni-oai <338284798+yoni-oai@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:56:52 +0000 Subject: [PATCH] feat(api): support audio fixture playback in Live sessions Castiron-Internal-PR: https://github.com/openai/openai-python-internal/pull/257 Castiron-Source-SHA: 3c683a02b15cfe933a1273c629f19eb5b73799a3 Castiron-Public-Base-SHA: e71d781ac5e7b3f365ac130bf338baaf13ae720d --- .castiron.stats.yml | 14 +-- api_reference/openapi.transformed.yml | 107 +++++++++++++++++- src/openai/_constants.py | 2 + src/openai/resources/live/api.md | 3 + src/openai/resources/live/forks.py | 65 ++++++++++- src/openai/resources/live/live.py | 65 ++++++++++- src/openai/resources/live/sideband.py | 65 ++++++++++- src/openai/types/live/__init__.py | 4 + src/openai/types/live/client_event.py | 2 + src/openai/types/live/client_event_param.py | 2 + src/openai/types/live/connect_client_event.py | 2 + .../types/live/connect_client_event_param.py | 2 + src/openai/types/live/connect_server_event.py | 4 + src/openai/types/live/fork_client_event.py | 2 + .../types/live/fork_client_event_param.py | 2 + src/openai/types/live/fork_server_event.py | 4 + .../live/output_audio_fixture_play_event.py | 29 +++++ .../output_audio_fixture_play_event_param.py | 29 +++++ .../live/output_audio_fixture_played_event.py | 25 ++++ .../output_audio_fixture_playing_event.py | 26 +++++ src/openai/types/live/server_event.py | 4 + tests/lib/live/test_websocket_roles.py | 28 +++++ 22 files changed, 460 insertions(+), 26 deletions(-) create mode 100644 src/openai/types/live/output_audio_fixture_play_event.py create mode 100644 src/openai/types/live/output_audio_fixture_play_event_param.py create mode 100644 src/openai/types/live/output_audio_fixture_played_event.py create mode 100644 src/openai/types/live/output_audio_fixture_playing_event.py diff --git a/.castiron.stats.yml b/.castiron.stats.yml index 0451291b29..eda98fcf29 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,8 +1,8 @@ schema_version: 1 -generation_id: 7ee6ff28-c1e4-4284-98f1-fa5d0c410e20 -openapi_spec_hash: e004507264375728a48a36cfa571544e -openapi_transformed_spec_hash: bb7d73817be9bf23b859a5dfd4e15b11 -config_hash: 47b922b06d16fe0e7595c5eacbbb5495 -codegen_sha: 17b4aa72eb7729595f4446e2310cabba7a743280 -codegen_hash: 38a88497ad58fa758bb623d316d312745e2e820216e6fb5b824185a693bd9abd -public_codegen_sha: 500f1e917e9dfac7d20c20857dc72ec1aac09686 +generation_id: 158c74da-f640-42bc-9051-dc5c15f35612 +openapi_spec_hash: e81fa0fd2133715dabef9f00a42557a9 +openapi_transformed_spec_hash: bea7a30d23deed3fc8f1c6413a38d354 +config_hash: c9c8b98008c6a5069191c1f38273d8ca +codegen_sha: 9144bd2d5b14c91d677edca7a1757c32af1e1baa +codegen_hash: ad31b796363b7ccd1828db887233e9f5090e1cd768c6b743a41b208ad4a447cb +public_codegen_sha: c22f6d6207a8f4b7a456392c48c283ed71e39164 diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml index 4e7b57bc46..ed9a47e028 100644 --- a/api_reference/openapi.transformed.yml +++ b/api_reference/openapi.transformed.yml @@ -50637,6 +50637,7 @@ components: - $ref: '#/components/schemas/LiveInputAudioAppendEvent' - $ref: '#/components/schemas/LiveInputAudioMuteParam' - $ref: '#/components/schemas/LiveInputAudioUnmuteParam' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlayParam' - $ref: '#/components/schemas/LiveInstructionsAppendParam' - $ref: '#/components/schemas/LiveThinkingAppendParam' - $ref: '#/components/schemas/LiveCommentaryAppendParam' @@ -50694,6 +50695,7 @@ components: - $ref: '#/components/schemas/LiveInputAudioAppendEvent' - $ref: '#/components/schemas/LiveInputAudioMuteParam' - $ref: '#/components/schemas/LiveInputAudioUnmuteParam' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlayParam' - $ref: '#/components/schemas/LiveInstructionsAppendParam' - $ref: '#/components/schemas/LiveThinkingAppendParam' - $ref: '#/components/schemas/LiveCommentaryAppendParam' @@ -50881,13 +50883,14 @@ components: - session - transport LiveSidebandClientEvent: - description: Client events accepted by an attached Live sideband WebSocket. The session is already started; send audio over the primary connection. + description: Client events accepted by an attached Live sideband WebSocket. The session has already started. Send input audio over the primary connection. discriminator: propertyName: type anyOf: - $ref: '#/components/schemas/LiveSessionUpdateParam' - $ref: '#/components/schemas/LiveInputAudioMuteParam' - $ref: '#/components/schemas/LiveInputAudioUnmuteParam' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlayParam' - $ref: '#/components/schemas/LiveInstructionsAppendParam' - $ref: '#/components/schemas/LiveThinkingAppendParam' - $ref: '#/components/schemas/LiveCommentaryAppendParam' @@ -50920,6 +50923,8 @@ components: - $ref: '#/components/schemas/LiveSessionUpdated' - $ref: '#/components/schemas/LiveInputAudioMuted' - $ref: '#/components/schemas/LiveInputAudioUnmuted' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlaying' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlayed' - $ref: '#/components/schemas/LiveInstructionsAppended' - $ref: '#/components/schemas/LiveThinkingAppended' - $ref: '#/components/schemas/LiveCommentaryAppended' @@ -92107,6 +92112,39 @@ components: example: type: session.input_audio.unmute event_id: evt_unmute_001 + LiveOutputAudioFixturePlayParam: + description: Play one complete audio fixture through the session output. Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + example: + type: session.output_audio.fixture.play + event_id: evt_play_001 + audio: AAAAAA== + type: object + properties: + event_id: + anyOf: + - description: Optional client identifier for correlating this command with a server event's client_event_id or error.client_event_id. + type: string + maxLength: 512 + - type: 'null' + type: + default: session.output_audio.fixture.play + x-stainless-const: true + type: string + enum: + - session.output_audio.fixture.play + audio: + description: Base64-encoded raw mono PCM16 audio, little-endian, at 24 kHz. Do not include a WAV or other container header. + type: string + minLength: 1 + maxLength: 1920000 + required: + - type + - audio + x-oaiMeta: + example: + type: session.output_audio.fixture.play + event_id: evt_play_001 + audio: AAAAAA== LiveInstructionsAppendParam: description: Append instructions to the Live conversation while it is running, optionally associating them with an existing client delegation. example: @@ -92624,6 +92662,62 @@ components: type: session.input_audio.unmuted event_id: evt_unmuted_001 client_event_id: evt_unmute_001 + LiveOutputAudioFixturePlaying: + description: The existing playback path has accepted the validated audio fixture. This acknowledgment does not identify when the listener receives the first sample. The optional client_event_id echoes the play command. + example: + type: session.output_audio.fixture.playing + event_id: evt_playing_001 + client_event_id: evt_play_001 + type: object + properties: + event_id: + description: The unique ID of the Live server event. + type: string + client_event_id: + description: The event_id of the client command associated with this server event, when supplied. + type: string + type: + default: session.output_audio.fixture.playing + x-stainless-const: true + type: string + enum: + - session.output_audio.fixture.playing + required: + - event_id + - type + x-oaiMeta: + example: + type: session.output_audio.fixture.playing + event_id: evt_playing_001 + client_event_id: evt_play_001 + LiveOutputAudioFixturePlayed: + description: The complete audio fixture has drained from the server mixer and normal inference has resumed. Transport buffers may still contain audio; this event does not prove listener-side completion. The optional client_event_id echoes the play command. This event is not emitted for canceled or failed playback. + example: + type: session.output_audio.fixture.played + event_id: evt_played_001 + client_event_id: evt_play_001 + type: object + properties: + event_id: + description: The unique ID of the Live server event. + type: string + client_event_id: + description: The event_id of the client command associated with this server event, when supplied. + type: string + type: + default: session.output_audio.fixture.played + x-stainless-const: true + type: string + enum: + - session.output_audio.fixture.played + required: + - event_id + - type + x-oaiMeta: + example: + type: session.output_audio.fixture.played + event_id: evt_played_001 + client_event_id: evt_play_001 LiveInstructionsAppended: description: Returned when a session.instructions.append command is accepted into the Live session timeline. Acknowledges the appended instructions without guaranteeing that the model has acted on them. example: @@ -93487,6 +93581,8 @@ components: - $ref: '#/components/schemas/LiveSessionUpdated' - $ref: '#/components/schemas/LiveInputAudioMuted' - $ref: '#/components/schemas/LiveInputAudioUnmuted' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlaying' + - $ref: '#/components/schemas/LiveOutputAudioFixturePlayed' - $ref: '#/components/schemas/LiveInstructionsAppended' - $ref: '#/components/schemas/LiveThinkingAppended' - $ref: '#/components/schemas/LiveCommentaryAppended' @@ -107960,6 +108056,9 @@ x-oaiMeta: - type: object key: LiveInputAudioUnmuteParam path: + - type: object + key: LiveOutputAudioFixturePlayParam + path: - type: object key: LiveInstructionsAppendParam path: @@ -108004,6 +108103,12 @@ x-oaiMeta: - type: object key: LiveInputAudioUnmuted path: + - type: object + key: LiveOutputAudioFixturePlaying + path: + - type: object + key: LiveOutputAudioFixturePlayed + path: - type: object key: LiveInstructionsAppended path: diff --git a/src/openai/_constants.py b/src/openai/_constants.py index 19d2d6b020..d5ab059f2b 100644 --- a/src/openai/_constants.py +++ b/src/openai/_constants.py @@ -7,6 +7,8 @@ DEFAULT_TIMEOUT = httpx2.Timeout(timeout=600, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx2.Limits(max_connections=1000, max_keepalive_connections=100) +# A 30-second Live PCM16 fixture takes 1,920,000 base64 bytes before its JSON envelope. +DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES = 2_097_152 INITIAL_RETRY_DELAY = 0.5 MAX_RETRY_DELAY = 8.0 diff --git a/src/openai/resources/live/api.md b/src/openai/resources/live/api.md index f0d2aeb89c..e3a8677db9 100644 --- a/src/openai/resources/live/api.md +++ b/src/openai/resources/live/api.md @@ -32,6 +32,9 @@ from openai.types.live import ( MediaSessionConfigParam, MediaSessionForkConfigParam, OutputAudioDeltaEvent, + OutputAudioFixturePlayEvent, + OutputAudioFixturePlayedEvent, + OutputAudioFixturePlayingEvent, OutputTranscriptDeltaEvent, ResponseCreateEvent, ResponseEvent, diff --git a/src/openai/resources/live/forks.py b/src/openai/resources/live/forks.py index fa65e1be92..bc16cecac9 100644 --- a/src/openai/resources/live/forks.py +++ b/src/openai/resources/live/forks.py @@ -16,6 +16,7 @@ from ..._compat import cached_property from ..._models import FinalRequestOptions, construct_type_unchecked from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._constants import DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES from ..._exceptions import OpenAIError, WebSocketConnectionClosedError from ..._send_queue import SendQueue from ..._base_client import _merge_mappings @@ -54,7 +55,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> ForksConnectionManager: """Fork a stored Live session over WebSocket. @@ -86,7 +87,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> AsyncForksConnectionManager: """Fork a stored Live session over WebSocket. @@ -136,7 +137,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=False) @@ -337,7 +340,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__session_id = session_id @@ -532,7 +535,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=True) @@ -727,7 +732,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__session_id = session_id @@ -910,6 +915,10 @@ def input_audio(self) -> ForksSessionInputAudioResource: def instructions(self) -> ForksSessionInstructionsResource: return ForksSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> ForksSessionOutputAudioResource: + return ForksSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> ForksSessionThinkingResource: return ForksSessionThinkingResource(self._connection) @@ -1021,6 +1030,12 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class ForksSessionOutputAudioResource(BaseForksConnectionResource): + @cached_property + def fixture(self) -> ForksSessionOutputAudioFixtureResource: + return ForksSessionOutputAudioFixtureResource(self._connection) + + class ForksSessionThinkingResource(BaseForksConnectionResource): def append(self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit) -> None: """ @@ -1041,6 +1056,20 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class ForksSessionOutputAudioFixtureResource(BaseForksConnectionResource): + def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + self._connection.send( + cast( + ForkClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class ForksResponseResource(BaseForksConnectionResource): @cached_property def item(self) -> ForksResponseItemResource: @@ -1090,6 +1119,10 @@ def input_audio(self) -> AsyncForksSessionInputAudioResource: def instructions(self) -> AsyncForksSessionInstructionsResource: return AsyncForksSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> AsyncForksSessionOutputAudioResource: + return AsyncForksSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> AsyncForksSessionThinkingResource: return AsyncForksSessionThinkingResource(self._connection) @@ -1205,6 +1238,12 @@ async def append( ) +class AsyncForksSessionOutputAudioResource(BaseAsyncForksConnectionResource): + @cached_property + def fixture(self) -> AsyncForksSessionOutputAudioFixtureResource: + return AsyncForksSessionOutputAudioFixtureResource(self._connection) + + class AsyncForksSessionThinkingResource(BaseAsyncForksConnectionResource): async def append( self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit @@ -1227,6 +1266,20 @@ async def append( ) +class AsyncForksSessionOutputAudioFixtureResource(BaseAsyncForksConnectionResource): + async def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + await self._connection.send( + cast( + ForkClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class AsyncForksResponseResource(BaseAsyncForksConnectionResource): @cached_property def item(self) -> AsyncForksResponseItemResource: diff --git a/src/openai/resources/live/live.py b/src/openai/resources/live/live.py index f89cac978a..3c345cc968 100644 --- a/src/openai/resources/live/live.py +++ b/src/openai/resources/live/live.py @@ -28,6 +28,7 @@ from ..._models import FinalRequestOptions, construct_type_unchecked from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ..._constants import DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES from ...types.live import live_create_params from ..._exceptions import OpenAIError, WebSocketConnectionClosedError from ..._send_queue import SendQueue @@ -147,7 +148,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> LiveConnectionManager: """Connect to Live without query parameters. @@ -256,7 +257,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> AsyncLiveConnectionManager: """Connect to Live without query parameters. @@ -357,7 +358,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=False) @@ -557,7 +560,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__connection: AsyncLiveConnection | None = None @@ -750,7 +753,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=True) @@ -943,7 +948,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__connection: LiveConnection | None = None @@ -1124,6 +1129,10 @@ def input_audio(self) -> LiveSessionInputAudioResource: def instructions(self) -> LiveSessionInstructionsResource: return LiveSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> LiveSessionOutputAudioResource: + return LiveSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> LiveSessionThinkingResource: return LiveSessionThinkingResource(self._connection) @@ -1228,6 +1237,12 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class LiveSessionOutputAudioResource(BaseLiveConnectionResource): + @cached_property + def fixture(self) -> LiveSessionOutputAudioFixtureResource: + return LiveSessionOutputAudioFixtureResource(self._connection) + + class LiveSessionThinkingResource(BaseLiveConnectionResource): def append(self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit) -> None: """ @@ -1248,6 +1263,20 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class LiveSessionOutputAudioFixtureResource(BaseLiveConnectionResource): + def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + self._connection.send( + cast( + ClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class LiveResponseResource(BaseLiveConnectionResource): @cached_property def item(self) -> LiveResponseItemResource: @@ -1296,6 +1325,10 @@ def input_audio(self) -> AsyncLiveSessionInputAudioResource: def instructions(self) -> AsyncLiveSessionInstructionsResource: return AsyncLiveSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> AsyncLiveSessionOutputAudioResource: + return AsyncLiveSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> AsyncLiveSessionThinkingResource: return AsyncLiveSessionThinkingResource(self._connection) @@ -1406,6 +1439,12 @@ async def append( ) +class AsyncLiveSessionOutputAudioResource(BaseAsyncLiveConnectionResource): + @cached_property + def fixture(self) -> AsyncLiveSessionOutputAudioFixtureResource: + return AsyncLiveSessionOutputAudioFixtureResource(self._connection) + + class AsyncLiveSessionThinkingResource(BaseAsyncLiveConnectionResource): async def append( self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit @@ -1428,6 +1467,20 @@ async def append( ) +class AsyncLiveSessionOutputAudioFixtureResource(BaseAsyncLiveConnectionResource): + async def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + await self._connection.send( + cast( + ClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class AsyncLiveResponseResource(BaseAsyncLiveConnectionResource): @cached_property def item(self) -> AsyncLiveResponseItemResource: diff --git a/src/openai/resources/live/sideband.py b/src/openai/resources/live/sideband.py index 9cd1c68340..0015c5387b 100644 --- a/src/openai/resources/live/sideband.py +++ b/src/openai/resources/live/sideband.py @@ -16,6 +16,7 @@ from ..._compat import cached_property from ..._models import FinalRequestOptions, construct_type_unchecked from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._constants import DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES from ..._exceptions import OpenAIError, WebSocketConnectionClosedError from ..._send_queue import SendQueue from ..._base_client import _merge_mappings @@ -54,7 +55,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> SidebandConnectionManager: """Attach to an existing Live session. @@ -88,7 +89,7 @@ def connect( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> AsyncSidebandConnectionManager: """Attach to an existing Live session. @@ -139,7 +140,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=False) @@ -343,7 +346,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__session_id = session_id @@ -547,7 +550,9 @@ def __init__( max_delay=max_delay, extra_query=extra_query, extra_headers=extra_headers, - send_queue=send_queue, + send_queue=( + send_queue if send_queue is not None else SendQueue(max_bytes=DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES) + ), logger=log, ) self._event_handler_registry = EventHandlerRegistry(use_lock=True) @@ -745,7 +750,7 @@ def __init__( max_retries: int = 5, initial_delay: float = 0.5, max_delay: float = 8.0, - max_queue_size: int = 1_048_576, + max_queue_size: int = DEFAULT_LIVE_SEND_QUEUE_MAX_BYTES, ) -> None: self.__client = client self.__session_id = session_id @@ -937,6 +942,10 @@ def input_audio(self) -> SidebandSessionInputAudioResource: def instructions(self) -> SidebandSessionInstructionsResource: return SidebandSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> SidebandSessionOutputAudioResource: + return SidebandSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> SidebandSessionThinkingResource: return SidebandSessionThinkingResource(self._connection) @@ -1023,6 +1032,12 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class SidebandSessionOutputAudioResource(BaseSidebandConnectionResource): + @cached_property + def fixture(self) -> SidebandSessionOutputAudioFixtureResource: + return SidebandSessionOutputAudioFixtureResource(self._connection) + + class SidebandSessionThinkingResource(BaseSidebandConnectionResource): def append(self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit) -> None: """ @@ -1043,6 +1058,20 @@ def append(self, *, content: str, delegation_id: Optional[str], event_id: Option ) +class SidebandSessionOutputAudioFixtureResource(BaseSidebandConnectionResource): + def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + self._connection.send( + cast( + ConnectClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class SidebandResponseResource(BaseSidebandConnectionResource): @cached_property def item(self) -> SidebandResponseItemResource: @@ -1092,6 +1121,10 @@ def input_audio(self) -> AsyncSidebandSessionInputAudioResource: def instructions(self) -> AsyncSidebandSessionInstructionsResource: return AsyncSidebandSessionInstructionsResource(self._connection) + @cached_property + def output_audio(self) -> AsyncSidebandSessionOutputAudioResource: + return AsyncSidebandSessionOutputAudioResource(self._connection) + @cached_property def thinking(self) -> AsyncSidebandSessionThinkingResource: return AsyncSidebandSessionThinkingResource(self._connection) @@ -1182,6 +1215,12 @@ async def append( ) +class AsyncSidebandSessionOutputAudioResource(BaseAsyncSidebandConnectionResource): + @cached_property + def fixture(self) -> AsyncSidebandSessionOutputAudioFixtureResource: + return AsyncSidebandSessionOutputAudioFixtureResource(self._connection) + + class AsyncSidebandSessionThinkingResource(BaseAsyncSidebandConnectionResource): async def append( self, *, content: str, delegation_id: Optional[str], event_id: Optional[str] | Omit = omit @@ -1204,6 +1243,20 @@ async def append( ) +class AsyncSidebandSessionOutputAudioFixtureResource(BaseAsyncSidebandConnectionResource): + async def play(self, *, audio: str, event_id: Optional[str] | Omit = omit) -> None: + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + await self._connection.send( + cast( + ConnectClientEventParam, + strip_not_given({"type": "session.output_audio.fixture.play", "audio": audio, "event_id": event_id}), + ) + ) + + class AsyncSidebandResponseResource(BaseAsyncSidebandConnectionResource): @cached_property def item(self) -> AsyncSidebandResponseItemResource: diff --git a/src/openai/types/live/__init__.py b/src/openai/types/live/__init__.py index 6d19983ed2..ab4efa56f1 100644 --- a/src/openai/types/live/__init__.py +++ b/src/openai/types/live/__init__.py @@ -87,9 +87,13 @@ from .input_audio_unmute_event_param import InputAudioUnmuteEventParam as InputAudioUnmuteEventParam from .instructions_append_event_param import InstructionsAppendEventParam as InstructionsAppendEventParam from .media_session_fork_config_param import MediaSessionForkConfigParam as MediaSessionForkConfigParam +from .output_audio_fixture_play_event import OutputAudioFixturePlayEvent as OutputAudioFixturePlayEvent from .response_item_create_event_param import ResponseItemCreateEventParam as ResponseItemCreateEventParam +from .output_audio_fixture_played_event import OutputAudioFixturePlayedEvent as OutputAudioFixturePlayedEvent from .responses_delegation_config_param import ResponsesDelegationConfigParam as ResponsesDelegationConfigParam +from .output_audio_fixture_playing_event import OutputAudioFixturePlayingEvent as OutputAudioFixturePlayingEvent from .responses_delegation_update_config import ResponsesDelegationUpdateConfig as ResponsesDelegationUpdateConfig +from .output_audio_fixture_play_event_param import OutputAudioFixturePlayEventParam as OutputAudioFixturePlayEventParam from .responses_delegation_update_config_param import ( ResponsesDelegationUpdateConfigParam as ResponsesDelegationUpdateConfigParam, ) diff --git a/src/openai/types/live/client_event.py b/src/openai/types/live/client_event.py index b804b896c2..824ec6e8a8 100644 --- a/src/openai/types/live/client_event.py +++ b/src/openai/types/live/client_event.py @@ -15,6 +15,7 @@ from .input_audio_unmute_event import InputAudioUnmuteEvent from .instructions_append_event import InstructionsAppendEvent from .response_item_create_event import ResponseItemCreateEvent +from .output_audio_fixture_play_event import OutputAudioFixturePlayEvent __all__ = ["ClientEvent"] @@ -25,6 +26,7 @@ InputAudioAppendEvent, InputAudioMuteEvent, InputAudioUnmuteEvent, + OutputAudioFixturePlayEvent, InstructionsAppendEvent, ThinkingAppendEvent, CommentaryAppendEvent, diff --git a/src/openai/types/live/client_event_param.py b/src/openai/types/live/client_event_param.py index be0ec9015a..84b946d4e3 100644 --- a/src/openai/types/live/client_event_param.py +++ b/src/openai/types/live/client_event_param.py @@ -16,6 +16,7 @@ from .input_audio_unmute_event_param import InputAudioUnmuteEventParam from .instructions_append_event_param import InstructionsAppendEventParam from .response_item_create_event_param import ResponseItemCreateEventParam +from .output_audio_fixture_play_event_param import OutputAudioFixturePlayEventParam __all__ = ["ClientEventParam"] @@ -25,6 +26,7 @@ InputAudioAppendEventParam, InputAudioMuteEventParam, InputAudioUnmuteEventParam, + OutputAudioFixturePlayEventParam, InstructionsAppendEventParam, ThinkingAppendEventParam, CommentaryAppendEventParam, diff --git a/src/openai/types/live/connect_client_event.py b/src/openai/types/live/connect_client_event.py index 30433d3069..8bf6a06d96 100644 --- a/src/openai/types/live/connect_client_event.py +++ b/src/openai/types/live/connect_client_event.py @@ -13,6 +13,7 @@ from .input_audio_unmute_event import InputAudioUnmuteEvent from .instructions_append_event import InstructionsAppendEvent from .response_item_create_event import ResponseItemCreateEvent +from .output_audio_fixture_play_event import OutputAudioFixturePlayEvent __all__ = ["ConnectClientEvent"] @@ -21,6 +22,7 @@ SessionUpdateEvent, InputAudioMuteEvent, InputAudioUnmuteEvent, + OutputAudioFixturePlayEvent, InstructionsAppendEvent, ThinkingAppendEvent, CommentaryAppendEvent, diff --git a/src/openai/types/live/connect_client_event_param.py b/src/openai/types/live/connect_client_event_param.py index 9fadbcb69e..5b3dd3b15a 100644 --- a/src/openai/types/live/connect_client_event_param.py +++ b/src/openai/types/live/connect_client_event_param.py @@ -14,6 +14,7 @@ from .input_audio_unmute_event_param import InputAudioUnmuteEventParam from .instructions_append_event_param import InstructionsAppendEventParam from .response_item_create_event_param import ResponseItemCreateEventParam +from .output_audio_fixture_play_event_param import OutputAudioFixturePlayEventParam __all__ = ["ConnectClientEventParam"] @@ -21,6 +22,7 @@ SessionUpdateEventParam, InputAudioMuteEventParam, InputAudioUnmuteEventParam, + OutputAudioFixturePlayEventParam, InstructionsAppendEventParam, ThinkingAppendEventParam, CommentaryAppendEventParam, diff --git a/src/openai/types/live/connect_server_event.py b/src/openai/types/live/connect_server_event.py index 6b5b57ae50..13d68b1db3 100644 --- a/src/openai/types/live/connect_server_event.py +++ b/src/openai/types/live/connect_server_event.py @@ -19,6 +19,8 @@ from .session_usage_updated_event import SessionUsageUpdatedEvent from .input_transcript_delta_event import InputTranscriptDeltaEvent from .output_transcript_delta_event import OutputTranscriptDeltaEvent +from .output_audio_fixture_played_event import OutputAudioFixturePlayedEvent +from .output_audio_fixture_playing_event import OutputAudioFixturePlayingEvent __all__ = ["ConnectServerEvent"] @@ -28,6 +30,8 @@ SessionUpdatedEvent, InputAudioMutedEvent, InputAudioUnmutedEvent, + OutputAudioFixturePlayingEvent, + OutputAudioFixturePlayedEvent, InstructionsAppendedEvent, ThinkingAppendedEvent, CommentaryAppendedEvent, diff --git a/src/openai/types/live/fork_client_event.py b/src/openai/types/live/fork_client_event.py index e6426835cc..f9e2feb0f4 100644 --- a/src/openai/types/live/fork_client_event.py +++ b/src/openai/types/live/fork_client_event.py @@ -15,6 +15,7 @@ from .input_audio_unmute_event import InputAudioUnmuteEvent from .instructions_append_event import InstructionsAppendEvent from .response_item_create_event import ResponseItemCreateEvent +from .output_audio_fixture_play_event import OutputAudioFixturePlayEvent __all__ = ["ForkClientEvent"] @@ -25,6 +26,7 @@ InputAudioAppendEvent, InputAudioMuteEvent, InputAudioUnmuteEvent, + OutputAudioFixturePlayEvent, InstructionsAppendEvent, ThinkingAppendEvent, CommentaryAppendEvent, diff --git a/src/openai/types/live/fork_client_event_param.py b/src/openai/types/live/fork_client_event_param.py index dd691511f6..1056c3af81 100644 --- a/src/openai/types/live/fork_client_event_param.py +++ b/src/openai/types/live/fork_client_event_param.py @@ -16,6 +16,7 @@ from .input_audio_unmute_event_param import InputAudioUnmuteEventParam from .instructions_append_event_param import InstructionsAppendEventParam from .response_item_create_event_param import ResponseItemCreateEventParam +from .output_audio_fixture_play_event_param import OutputAudioFixturePlayEventParam __all__ = ["ForkClientEventParam"] @@ -25,6 +26,7 @@ InputAudioAppendEventParam, InputAudioMuteEventParam, InputAudioUnmuteEventParam, + OutputAudioFixturePlayEventParam, InstructionsAppendEventParam, ThinkingAppendEventParam, CommentaryAppendEventParam, diff --git a/src/openai/types/live/fork_server_event.py b/src/openai/types/live/fork_server_event.py index 197eadf91c..c8ca0073d0 100644 --- a/src/openai/types/live/fork_server_event.py +++ b/src/openai/types/live/fork_server_event.py @@ -21,6 +21,8 @@ from .session_usage_updated_event import SessionUsageUpdatedEvent from .input_transcript_delta_event import InputTranscriptDeltaEvent from .output_transcript_delta_event import OutputTranscriptDeltaEvent +from .output_audio_fixture_played_event import OutputAudioFixturePlayedEvent +from .output_audio_fixture_playing_event import OutputAudioFixturePlayingEvent __all__ = [ "ForkServerEvent", @@ -141,6 +143,8 @@ class TransportFailed(BaseModel): SessionUpdatedEvent, InputAudioMutedEvent, InputAudioUnmutedEvent, + OutputAudioFixturePlayingEvent, + OutputAudioFixturePlayedEvent, InstructionsAppendedEvent, ThinkingAppendedEvent, CommentaryAppendedEvent, diff --git a/src/openai/types/live/output_audio_fixture_play_event.py b/src/openai/types/live/output_audio_fixture_play_event.py new file mode 100644 index 0000000000..879741bddf --- /dev/null +++ b/src/openai/types/live/output_audio_fixture_play_event.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["OutputAudioFixturePlayEvent"] + + +class OutputAudioFixturePlayEvent(BaseModel): + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + + audio: str + """Base64-encoded raw mono PCM16 audio, little-endian, at 24 kHz. + + Do not include a WAV or other container header. + """ + + type: Literal["session.output_audio.fixture.play"] + + event_id: Optional[str] = None + """ + Optional client identifier for correlating this command with a server event's + client_event_id or error.client_event_id. + """ diff --git a/src/openai/types/live/output_audio_fixture_play_event_param.py b/src/openai/types/live/output_audio_fixture_play_event_param.py new file mode 100644 index 0000000000..d1cc247209 --- /dev/null +++ b/src/openai/types/live/output_audio_fixture_play_event_param.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["OutputAudioFixturePlayEventParam"] + + +class OutputAudioFixturePlayEventParam(TypedDict, total=False): + """Play one complete audio fixture through the session output. + + Send this event over the primary WebSocket or a server sideband. Audio must be raw mono PCM16 little-endian at 24 kHz, regardless of the session audio format, and at most 30 seconds. The entire payload is validated before model speech is interrupted and input is muted. Only one fixture can be active. fixture.playing acknowledges acceptance and fixture.played reports server mixer drain. These notifications are not exact transport-media boundaries. Input arriving during playback is dropped and not replayed. + """ + + audio: Required[str] + """Base64-encoded raw mono PCM16 audio, little-endian, at 24 kHz. + + Do not include a WAV or other container header. + """ + + type: Required[Literal["session.output_audio.fixture.play"]] + + event_id: Optional[str] + """ + Optional client identifier for correlating this command with a server event's + client_event_id or error.client_event_id. + """ diff --git a/src/openai/types/live/output_audio_fixture_played_event.py b/src/openai/types/live/output_audio_fixture_played_event.py new file mode 100644 index 0000000000..761052880f --- /dev/null +++ b/src/openai/types/live/output_audio_fixture_played_event.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["OutputAudioFixturePlayedEvent"] + + +class OutputAudioFixturePlayedEvent(BaseModel): + """ + The complete audio fixture has drained from the server mixer and normal inference has resumed. Transport buffers may still contain audio; this event does not prove listener-side completion. The optional client_event_id echoes the play command. This event is not emitted for canceled or failed playback. + """ + + event_id: str + """The unique ID of the Live server event.""" + + type: Literal["session.output_audio.fixture.played"] + + client_event_id: Optional[str] = None + """ + The event_id of the client command associated with this server event, when + supplied. + """ diff --git a/src/openai/types/live/output_audio_fixture_playing_event.py b/src/openai/types/live/output_audio_fixture_playing_event.py new file mode 100644 index 0000000000..0ff1cf860d --- /dev/null +++ b/src/openai/types/live/output_audio_fixture_playing_event.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["OutputAudioFixturePlayingEvent"] + + +class OutputAudioFixturePlayingEvent(BaseModel): + """The existing playback path has accepted the validated audio fixture. + + This acknowledgment does not identify when the listener receives the first sample. The optional client_event_id echoes the play command. + """ + + event_id: str + """The unique ID of the Live server event.""" + + type: Literal["session.output_audio.fixture.playing"] + + client_event_id: Optional[str] = None + """ + The event_id of the client command associated with this server event, when + supplied. + """ diff --git a/src/openai/types/live/server_event.py b/src/openai/types/live/server_event.py index 7da7359e58..54f4872cf0 100644 --- a/src/openai/types/live/server_event.py +++ b/src/openai/types/live/server_event.py @@ -21,6 +21,8 @@ from .session_usage_updated_event import SessionUsageUpdatedEvent from .input_transcript_delta_event import InputTranscriptDeltaEvent from .output_transcript_delta_event import OutputTranscriptDeltaEvent +from .output_audio_fixture_played_event import OutputAudioFixturePlayedEvent +from .output_audio_fixture_playing_event import OutputAudioFixturePlayingEvent __all__ = [ "ServerEvent", @@ -141,6 +143,8 @@ class TransportFailed(BaseModel): SessionUpdatedEvent, InputAudioMutedEvent, InputAudioUnmutedEvent, + OutputAudioFixturePlayingEvent, + OutputAudioFixturePlayedEvent, InstructionsAppendedEvent, ThinkingAppendedEvent, CommentaryAppendedEvent, diff --git a/tests/lib/live/test_websocket_roles.py b/tests/lib/live/test_websocket_roles.py index 3b62f73e04..f68195d4b6 100644 --- a/tests/lib/live/test_websocket_roles.py +++ b/tests/lib/live/test_websocket_roles.py @@ -18,6 +18,7 @@ SessionStartedEvent, SessionUpdatedEvent, OutputTranscriptDeltaEvent, + OutputAudioFixturePlayEventParam, ) from openai._exceptions import WebSocketQueueFullError from openai.resources.live.live import LiveConnection, AsyncLiveConnection @@ -34,6 +35,33 @@ def bypass_loopback_proxy(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("no_proxy", "127.0.0.1") +@pytest.mark.parametrize("client_type", [OpenAI, AsyncOpenAI]) +async def test_default_live_queue_accepts_a_full_size_audio_fixture( + client_type: type[OpenAI] | type[AsyncOpenAI], +) -> None: + fixture: OutputAudioFixturePlayEventParam = { + "type": "session.output_audio.fixture.play", + "audio": "A" * (30 * 24_000 * 2 * 4 // 3), + } + client = ( + OpenAI(api_key="fake-live-key", http_client=httpx2.Client(trust_env=False)) + if client_type is OpenAI + else AsyncOpenAI(api_key="fake-live-key", http_client=httpx2.AsyncClient(trust_env=False)) + ) + try: + for manager in ( + client.live.connect(), + client.live.forks.connect(session_id="stored"), + client.live.sideband.connect(session_id="stored"), + ): + manager.send(fixture) + finally: + if isinstance(client, AsyncOpenAI): + await client.close() + else: + client.close() + + @pytest.mark.parametrize("role", ["primary", "fork", "sideband"]) @pytest.mark.parametrize("mode", ["sync", "async"]) async def test_empty_live_manager_queue_keeps_budget_and_caller_messages(role: str, mode: str) -> None: