Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 278 additions & 30 deletions litellm/litellm_core_utils/realtime_streaming.py

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions litellm/llms/azure/realtime/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ async def async_realtime(

if api_base is None:
raise ValueError("api_base is required for Azure OpenAI calls")
if api_version is None and (
backend_uses_beta_protocol = (
realtime_protocol is None or realtime_protocol.upper() not in ("GA", "V1")
):
)
if api_version is None and backend_uses_beta_protocol:
raise ValueError("api_version is required for Azure OpenAI calls")

url = self._construct_url(
Expand All @@ -114,6 +115,7 @@ async def async_realtime(
logging_obj,
user_api_key_dict=user_api_key_dict,
request_data={"litellm_metadata": litellm_metadata or {}},
backend_uses_beta_protocol=backend_uses_beta_protocol,
)
await realtime_streaming.bidirectional_forward()

Expand Down
1 change: 0 additions & 1 deletion litellm/llms/custom_httpx/llm_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5255,7 +5255,6 @@ async def async_realtime_client_secret_handler(
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"OpenAI-Beta": "realtime=v1",
}
Comment thread
Sameerlite marked this conversation as resolved.

if extra_headers:
Expand Down
46 changes: 30 additions & 16 deletions litellm/llms/openai/realtime/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

from typing import Any, Optional, cast

from litellm._logging import _redact_string
from litellm._logging import _redact_string, verbose_logger
from litellm.constants import REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES
from litellm.types.realtime import RealtimeQueryParams

from ....litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
from ....litellm_core_utils.realtime_streaming import RealTimeStreaming
from ....litellm_core_utils.realtime_streaming import (
RealTimeStreaming,
client_sent_openai_beta_realtime_header,
)
from ....llms.custom_httpx.http_handler import get_shared_realtime_ssl_context
from ..openai import OpenAIChatCompletion

Expand All @@ -33,21 +36,24 @@ def _get_default_api_base(self) -> str:
"""
return "https://api.openai.com/"

def _get_additional_headers(self, api_key: str) -> dict:
def _get_additional_headers(
self,
api_key: str,
*,
openai_beta_realtime: bool = False,
) -> dict:
"""
Get additional headers beyond Authorization.
Override this in subclasses to customize headers (e.g., remove OpenAI-Beta).

Args:
api_key: API key for authentication
Headers for the upstream OpenAI Realtime WebSocket.

Returns:
Dictionary of additional headers
When the client sent ``OpenAI-Beta: realtime=v1`` on the proxy WebSocket,
``openai_beta_realtime`` is True and the same header is forwarded upstream
so the legacy beta API is used. GA clients omit that header on the client
connection and must send GA-shaped ``session.update`` payloads.
"""
return {
"Authorization": f"Bearer {api_key}",
"OpenAI-Beta": "realtime=v1",
}
headers: dict = {"Authorization": f"Bearer {api_key}"}
if openai_beta_realtime:
headers["OpenAI-Beta"] = "realtime=v1"
return headers

def _get_ssl_config(self, url: str) -> Any:
"""
Expand Down Expand Up @@ -120,8 +126,16 @@ async def async_realtime(
# Get provider-specific SSL configuration
ssl_config = self._get_ssl_config(url)

# Get provider-specific headers
headers = self._get_additional_headers(api_key)
openai_beta_realtime = client_sent_openai_beta_realtime_header(websocket)
if not openai_beta_realtime:
verbose_logger.debug(
"OpenAI Realtime: connecting with GA protocol (no OpenAI-Beta header). "
"If your client expects beta event names, add 'OpenAI-Beta: realtime=v1' "
"to the WebSocket headers sent to the LiteLLM proxy."
)
headers = self._get_additional_headers(
api_key, openai_beta_realtime=openai_beta_realtime
)

Comment thread
Sameerlite marked this conversation as resolved.
# Log a masked request preview consistent with other endpoints.
logging_obj.pre_call(
Expand Down
7 changes: 6 additions & 1 deletion litellm/llms/xai/realtime/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ def _get_default_api_base(self) -> str:
"""xAI uses a different API base URL."""
return XAI_API_BASE

def _get_additional_headers(self, api_key: str) -> dict:
def _get_additional_headers(
self,
api_key: str,
*,
openai_beta_realtime: bool = False,
) -> dict:
"""
xAI does NOT require the OpenAI-Beta header.
Only send Authorization header.
Expand Down
83 changes: 71 additions & 12 deletions litellm/types/llms/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -1878,14 +1878,24 @@ class OpenAIRealtimeStreamSessionEvents(TypedDict):

class OpenAIRealtimeStreamResponseOutputItemContent(TypedDict, total=False):
audio: str
"""Base64-encoded audio bytes, used for 'input_audio' content types"""
"""Base64-encoded audio bytes, used for 'input_audio' / 'audio' / 'output_audio' content types"""
id: str
"""The ID of the previous conversation item for reference"""
text: str
"""The text content, used for 'input_text' and 'text' content types"""
"""The text content, used for 'input_text' / 'text' / 'output_text' content types"""
transcript: str
"""The transcript content, used for 'input_audio' content types"""
type: Literal["input_audio", "input_text", "text", "item_reference", "audio"]
"""The transcript content, used for 'input_audio' / 'audio' content types"""
type: Literal[
"input_audio",
"input_text",
# Beta assistant content types
"text",
"audio",
"item_reference",
# GA assistant content types (aligns with Responses API)
"output_text",
"output_audio",
]
"""The type of content"""


Expand Down Expand Up @@ -1945,23 +1955,46 @@ class OpenAIRealtimeConversationCreated(TypedDict, total=False):


class OpenAIRealtimeConversationItemCreated(TypedDict, total=False):
"""Beta: single event emitted when a conversation item is created."""

type: Required[Literal["conversation.item.created"]]
item: OpenAIRealtimeStreamResponseOutputItem
event_id: str
previous_item_id: str
previous_item_id: Optional[str] # None when this is the first item


class OpenAIRealtimeConversationItemAdded(TypedDict, total=False):
"""GA: emitted immediately when a conversation item is added (replaces .created)."""

type: Required[Literal["conversation.item.added"]]
item: OpenAIRealtimeStreamResponseOutputItem
event_id: str
previous_item_id: Optional[str] # None when this is the first item


class OpenAIRealtimeConversationItemDone(TypedDict, total=False):
"""GA: emitted when a conversation item is fully complete (e.g. transcription done)."""

type: Required[Literal["conversation.item.done"]]
item: OpenAIRealtimeStreamResponseOutputItem
event_id: str
previous_item_id: Optional[str] # None when this is the first item


class OpenAIRealtimeResponseContentPart(TypedDict, total=False):
audio: str
"""Base64-encoded audio bytes, if type is 'audio'"""
"""Base64-encoded audio bytes, if type is 'audio' or 'output_audio'"""

text: str
"""The text content, if type is 'text'"""
"""The text content, if type is 'text' or 'output_text'"""

transcript: str
"""The transcript content, if type is 'audio'"""
"""The transcript content, if type is 'audio' or 'output_audio'"""

type: Literal["audio", "text"]
type: Union[
Literal["audio", "text"], # beta
Literal["output_audio", "output_text"], # GA
]
"""The type of content"""


Expand All @@ -1982,7 +2015,14 @@ class OpenAIRealtimeResponseDelta(TypedDict):
item_id: str
output_index: int
response_id: str
type: Union[Literal["response.text.delta"], Literal["response.audio.delta"]]
type: Union[
Literal["response.text.delta"],
Literal["response.audio.delta"],
# GA renamed events
Literal["response.output_text.delta"],
Literal["response.output_audio.delta"],
Literal["response.output_audio_transcript.delta"],
]


class OpenAIRealtimeResponseTextDone(TypedDict):
Expand All @@ -1992,7 +2032,10 @@ class OpenAIRealtimeResponseTextDone(TypedDict):
output_index: int
response_id: str
text: str
type: Literal["response.text.done"]
type: Union[
Literal["response.text.done"],
Literal["response.output_text.done"], # GA rename
]


class OpenAIRealtimeResponseAudioDone(TypedDict):
Expand All @@ -2001,7 +2044,11 @@ class OpenAIRealtimeResponseAudioDone(TypedDict):
item_id: str
output_index: int
response_id: str
type: Literal["response.audio.done"]
type: Union[
Literal["response.audio.done"],
Literal["response.output_audio.done"], # GA rename
Literal["response.output_audio_transcript.done"], # GA rename
]


class OpenAIRealtimeContentPartDone(TypedDict):
Expand Down Expand Up @@ -2046,10 +2093,18 @@ class OpenAIRealtimeDoneEvent(TypedDict):

class OpenAIRealtimeEventTypes(Enum):
SESSION_CREATED = "session.created"
# Beta delta event names
RESPONSE_TEXT_DELTA = "response.text.delta"
RESPONSE_AUDIO_DELTA = "response.audio.delta"
RESPONSE_TEXT_DONE = "response.text.done"
RESPONSE_AUDIO_DONE = "response.audio.done"
# GA renamed delta event names
RESPONSE_OUTPUT_TEXT_DELTA = "response.output_text.delta"
RESPONSE_OUTPUT_AUDIO_DELTA = "response.output_audio.delta"
RESPONSE_OUTPUT_AUDIO_TRANSCRIPT_DELTA = "response.output_audio_transcript.delta"
RESPONSE_OUTPUT_TEXT_DONE = "response.output_text.done"
RESPONSE_OUTPUT_AUDIO_DONE = "response.output_audio.done"
RESPONSE_OUTPUT_AUDIO_TRANSCRIPT_DONE = "response.output_audio_transcript.done"
RESPONSE_DONE = "response.done"
RESPONSE_OUTPUT_ITEM_ADDED = "response.output_item.added"
RESPONSE_CONTENT_PART_ADDED = "response.content_part.added"
Expand All @@ -2060,7 +2115,11 @@ class OpenAIRealtimeEventTypes(Enum):
OpenAIRealtimeStreamSessionEvents,
OpenAIRealtimeStreamResponseOutputItemAdded,
OpenAIRealtimeResponseContentPartAdded,
# Beta conversation item event
OpenAIRealtimeConversationItemCreated,
# GA conversation item events
OpenAIRealtimeConversationItemAdded,
OpenAIRealtimeConversationItemDone,
OpenAIRealtimeConversationCreated,
OpenAIRealtimeResponseDelta,
OpenAIRealtimeResponseTextDone,
Expand Down
Loading
Loading