-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix: expose OpenAI Realtime request IDs #6917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| import json | ||
| import os | ||
| import time | ||
| import uuid | ||
| import weakref | ||
| from collections.abc import Iterator | ||
| from dataclasses import dataclass, replace | ||
|
|
@@ -862,6 +863,9 @@ class RealtimeSession( | |
| - openai_client_event_queued: expose the raw client events sent to the OpenAI Realtime API | ||
| """ | ||
|
|
||
| _openai_request_id: str | None = None | ||
| _openai_client_request_id: str | None = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicates with that in |
||
|
|
||
| def __init__( | ||
| self, realtime_model: RealtimeModel, *, turn_detection_disabled: bool = False | ||
| ) -> None: | ||
|
|
@@ -879,6 +883,8 @@ def __init__( | |
| self._instructions: str | None = None | ||
| # set on aclose; trailing server events are ignored while it's set | ||
| self._closing = False | ||
| self._openai_request_id = None | ||
| self._openai_client_request_id = None | ||
| self._main_atask = asyncio.create_task(self._main_task(), name="RealtimeSession._main_task") | ||
| self.send_event(self._create_session_update_event()) | ||
|
|
||
|
|
@@ -925,6 +931,23 @@ def _reset_input_turn_state(self) -> None: | |
| # value cannot, because a late transcript would consume the next turn's value. | ||
| self._input_speech_started_at: dict[str, float] = {} | ||
|
|
||
| def _connection_request_ids(self) -> list[str]: | ||
| return [ | ||
| request_id | ||
| for request_id in (self._openai_request_id, self._openai_client_request_id) | ||
| if request_id | ||
| ] | ||
|
|
||
| def _connection_log_fields(self) -> dict[str, str | list[str]]: | ||
| fields: dict[str, str | list[str]] = { | ||
| "provider_request_ids": self._connection_request_ids() | ||
| } | ||
| if self._openai_request_id: | ||
| fields["request_id"] = self._openai_request_id | ||
| if self._openai_client_request_id: | ||
| fields["client_request_id"] = self._openai_client_request_id | ||
| return fields | ||
|
|
||
| @utils.log_exceptions(logger=logger) | ||
| async def _main_task(self) -> None: | ||
| num_retries: int = 0 | ||
|
|
@@ -933,7 +956,10 @@ async def _main_task(self) -> None: | |
| async def _reconnect() -> None: | ||
| logger.debug( | ||
| f"reconnecting to {self._realtime_model._provider_label}", | ||
| extra={"max_session_duration": self._opts.max_session_duration}, | ||
| extra={ | ||
| "max_session_duration": self._opts.max_session_duration, | ||
| **self._connection_log_fields(), | ||
| }, | ||
| ) | ||
|
|
||
| events: list[RealtimeClientEvent | dict[str, Any]] = [] | ||
|
|
@@ -990,7 +1016,10 @@ async def _reconnect() -> None: | |
| self._discarded_event_ids.clear() | ||
| self._close_current_generation("session reconnection") | ||
|
|
||
| logger.debug(f"reconnected to {self._realtime_model._provider_label}") | ||
| logger.debug( | ||
| f"reconnected to {self._realtime_model._provider_label}", | ||
| extra=self._connection_log_fields(), | ||
| ) | ||
| self.emit("session_reconnected", llm.RealtimeSessionReconnectedEvent()) | ||
|
|
||
| reconnecting = False | ||
|
|
@@ -1019,7 +1048,11 @@ async def _reconnect() -> None: | |
| logger.warning( | ||
| f"{self._realtime_model._provider_label} connection failed, retrying in {retry_interval}s", | ||
| exc_info=e, | ||
| extra={"attempt": num_retries, "max_retries": max_retries}, | ||
| extra={ | ||
| "attempt": num_retries, | ||
| "max_retries": max_retries, | ||
| **self._connection_log_fields(), | ||
| }, | ||
| ) | ||
| await asyncio.sleep(retry_interval) | ||
| num_retries += 1 | ||
|
|
@@ -1042,6 +1075,8 @@ async def _reconnect() -> None: | |
|
|
||
| async def _create_ws_conn(self) -> aiohttp.ClientWebSocketResponse: | ||
| headers = {"User-Agent": "LiveKit Agents"} | ||
| self._openai_request_id = None | ||
| self._openai_client_request_id = None | ||
| if self._opts.is_azure: | ||
| if self._opts.entra_token: | ||
| headers["Authorization"] = f"Bearer {self._opts.entra_token}" | ||
|
|
@@ -1050,6 +1085,8 @@ async def _create_ws_conn(self) -> aiohttp.ClientWebSocketResponse: | |
| headers["api-key"] = self._opts.api_key | ||
| else: | ||
| headers["Authorization"] = f"Bearer {self._opts.api_key}" | ||
| self._openai_client_request_id = str(uuid.uuid4()) | ||
| headers["X-Client-Request-Id"] = self._openai_client_request_id | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. qq: does xAI support this?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. they have an undocumented x-trace-id instead. |
||
|
|
||
| url = process_base_url( | ||
| self._opts.base_url, | ||
|
|
@@ -1068,9 +1105,15 @@ async def _create_ws_conn(self) -> aiohttp.ClientWebSocketResponse: | |
| self._realtime_model._ensure_http_session().ws_connect(url=url, headers=headers), | ||
| self._opts.conn_options.timeout, | ||
| ) | ||
| # aiohttp does not expose WebSocket upgrade headers publicly. | ||
| response = getattr(ws, "_response", None) | ||
| if response is not None: | ||
| self._openai_request_id = response.headers.get("x-request-id") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. The doc https://developers.openai.com/api/reference/overview#debugging-requests only mentions this as a HTTP response header, and it doesn't seem to support websocket/realtime endpoints. I will close the PR instead. |
||
| self._report_connection_acquired(time.perf_counter() - t0) | ||
| return ws | ||
| except aiohttp.ClientError as e: | ||
| if isinstance(e, aiohttp.ClientResponseError) and e.headers: | ||
| self._openai_request_id = e.headers.get("x-request-id") | ||
| raise APIConnectionError( | ||
| f"{self._realtime_model._provider_label} client connection error" | ||
| ) from e | ||
|
|
@@ -1860,6 +1903,7 @@ def _handle_response_created(self, event: ResponseCreatedEvent) -> None: | |
| function_stream=self._current_generation.function_ch, | ||
| user_initiated=False, | ||
| response_id=event.response.id, | ||
| provider_request_ids=self._connection_request_ids(), | ||
| ) | ||
|
|
||
| if client_event_id and (fut := self._response_created_futures.pop(client_event_id, None)): | ||
|
|
@@ -2298,7 +2342,7 @@ def _handle_error(self, event: RealtimeErrorEvent) -> None: | |
| provider_label = self._realtime_model._provider_label | ||
| logger.error( | ||
| f"{provider_label} returned an error: {event.error}", | ||
| extra={"error": event.error}, | ||
| extra={"error": event.error, **self._connection_log_fields()}, | ||
| ) | ||
| recoverable = not _is_fatal_error(event.error) | ||
| error = APIError( | ||
|
|
@@ -2324,5 +2368,6 @@ def _emit_error(self, error: Exception, recoverable: bool) -> None: | |
| label=self._realtime_model._label, | ||
| error=error, | ||
| recoverable=recoverable, | ||
| provider_request_ids=self._connection_request_ids(), | ||
| ), | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe just use
list(dict.fromkeys(filter(None, [generation_ev.response_id, *generation_ev.provider_request_ids])))