Files
MoFin/venv/lib/python3.12/site-packages/litellm/llms/openai/realtime/handler.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
  Privoxy对node122:18003返回500,直连正常
2026-06-30 02:56:35 +08:00

189 lines
7.2 KiB
Python

"""
This file contains the calling OpenAI's `/v1/realtime` endpoint.
This requires websockets, and is currently only supported on LiteLLM Proxy.
"""
from typing import Any, Optional, cast
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,
client_sent_openai_beta_realtime_header,
)
from ....llms.custom_httpx.http_handler import get_shared_realtime_ssl_context
from ..openai import OpenAIChatCompletion
class OpenAIRealtime(OpenAIChatCompletion):
"""
Base handler for OpenAI-compatible realtime WebSocket connections.
Subclasses can override template methods to customize:
- _get_default_api_base(): Default API base URL
- _get_additional_headers(): Extra headers beyond Authorization
- _get_ssl_config(): SSL configuration for WebSocket connection
"""
def _get_default_api_base(self) -> str:
"""
Get the default API base URL for this provider.
Override this in subclasses to set provider-specific defaults.
"""
return "https://api.openai.com/"
def _get_additional_headers(
self,
api_key: str,
*,
openai_beta_realtime: bool = False,
) -> dict:
"""
Headers for the upstream OpenAI Realtime WebSocket.
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.
"""
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:
"""
Get SSL configuration for WebSocket connection.
Override this in subclasses to customize SSL behavior.
Args:
url: WebSocket URL (ws:// or wss://)
Returns:
SSL configuration (None, True, or SSLContext)
"""
if url.startswith("ws://"):
return None
# Use the shared SSL context which respects custom CA certs and SSL settings
ssl_config = get_shared_realtime_ssl_context()
# If ssl_config is False (ssl_verify=False), websockets library needs True instead
# to establish connection without verification (False would fail)
if ssl_config is False:
return True
return ssl_config
def _construct_url(self, api_base: str, query_params: RealtimeQueryParams) -> str:
"""
Construct the backend websocket URL with all query parameters (including 'model').
"""
from httpx import URL
api_base = api_base.replace("https://", "wss://")
api_base = api_base.replace("http://", "ws://")
url = URL(api_base)
# Set the correct path
url = url.copy_with(path="/v1/realtime")
# Include all query parameters including 'model'
if query_params:
url = url.copy_with(params=query_params)
return str(url)
async def async_realtime(
self,
model: str,
websocket: Any,
logging_obj: LiteLLMLogging,
api_base: Optional[str] = None,
api_key: Optional[str] = None,
client: Optional[Any] = None,
timeout: Optional[float] = None,
query_params: Optional[RealtimeQueryParams] = None,
user_api_key_dict: Optional[Any] = None,
litellm_metadata: Optional[dict] = None,
**kwargs: Any,
):
import websockets
from websockets.asyncio.client import ClientConnection
if api_base is None:
api_base = self._get_default_api_base()
if api_key is None:
raise ValueError("api_key is required for OpenAI realtime calls")
# Use all query params if provided, else fallback to just model
if query_params is None:
query_params = {"model": model}
url = self._construct_url(api_base, query_params)
try:
# Get provider-specific SSL configuration
ssl_config = self._get_ssl_config(url)
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
)
# Log a masked request preview consistent with other endpoints.
logging_obj.pre_call(
input=None,
api_key=api_key,
additional_args={
"api_base": url,
"headers": headers,
"complete_input_dict": {"query_params": query_params},
},
)
async with websockets.connect( # type: ignore
url,
additional_headers=headers, # type: ignore
max_size=REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES,
ssl=ssl_config,
) as backend_ws:
realtime_streaming = RealTimeStreaming(
websocket,
cast(ClientConnection, backend_ws),
logging_obj,
model=model,
user_api_key_dict=user_api_key_dict,
request_data={"litellm_metadata": litellm_metadata or {}},
force_transcription_model=(
model
if (query_params or {}).get("intent") == "transcription"
else None
),
)
await realtime_streaming.bidirectional_forward()
except websockets.exceptions.InvalidStatusCode as e: # type: ignore
await websocket.close(code=e.status_code, reason=_redact_string(str(e)))
except Exception as e:
try:
await websocket.close(
code=1011, reason=_redact_string(f"Internal server error: {str(e)}")
)
except RuntimeError as close_error:
if "already completed" in str(close_error) or "websocket.close" in str(
close_error
):
# The WebSocket is already closed or the response is completed, so we can ignore this error
pass
else:
# If it's a different RuntimeError, we might want to log it or handle it differently
raise Exception(
f"Unexpected error while closing WebSocket: {close_error}"
)