Skip to content
Merged
Changes from all 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
Comment thread
ianbbqzy marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import json
import os
import time
import uuid
import weakref
from collections.abc import Callable
from dataclasses import dataclass, field, replace
Expand All @@ -45,6 +46,9 @@
from livekit.agents.voice.io import TimedString

from .log import logger
from .version import __version__

USER_AGENT = f"livekit-agents-py/{__version__}"

DEFAULT_BIT_RATE = 64000
DEFAULT_ENCODING = "OGG_OPUS"
Expand Down Expand Up @@ -230,10 +234,19 @@ async def connect(self) -> None:
return

url = urljoin(self._ws_url, "/tts/v1/voice:streamBidirectional")
request_id = str(uuid.uuid4())
self._ws = await self._session.ws_connect(
url, headers={"Authorization": self._authorization}
url,
headers={
"Authorization": self._authorization,
"X-User-Agent": USER_AGENT,
"X-Request-Id": request_id,
},
)
logger.debug(
"Established Inworld TTS WebSocket connection (shared)",
extra={"request_id": request_id},
)
logger.debug("Established Inworld TTS WebSocket connection (shared)")

self._send_task = asyncio.create_task(self._send_loop())
self._recv_task = asyncio.create_task(self._recv_loop())
Expand Down Expand Up @@ -997,7 +1010,11 @@ async def list_voices(self, language: str | None = None) -> list[dict[str, Any]]

async with self._ensure_session().get(
url,
headers={"Authorization": self._authorization},
headers={
"Authorization": self._authorization,
"X-User-Agent": USER_AGENT,
"X-Request-Id": str(uuid.uuid4()),
},
params=params,
) as resp:
if not resp.ok:
Expand Down Expand Up @@ -1040,10 +1057,13 @@ async def _run(self, output_emitter: tts.AudioEmitter) -> None:
if utils.is_given(self._opts.text_normalization):
body_params["applyTextNormalization"] = self._opts.text_normalization

x_request_id = str(uuid.uuid4())
async with self._tts._ensure_session().post(
urljoin(self._tts._base_url, "/tts/v1/voice:stream"),
headers={
"Authorization": self._tts._authorization,
"X-User-Agent": USER_AGENT,
"X-Request-Id": x_request_id,
},
json=body_params,
timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout),
Expand Down Expand Up @@ -1085,14 +1105,14 @@ async def _run(self, output_emitter: tts.AudioEmitter) -> None:
raise APIStatusError(
message=error.get("message"),
status_code=error.get("code"),
request_id=request_id,
request_id=x_request_id,
body=None,
)
except asyncio.TimeoutError:
raise APITimeoutError() from None
except aiohttp.ClientResponseError as e:
raise APIStatusError(
message=e.message, status_code=e.status, request_id=None, body=None
message=e.message, status_code=e.status, request_id=x_request_id, body=None
) from None
except Exception as e:
raise APIConnectionError() from e
Comment on lines 1117 to 1118
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ APIStatusError from response error data is caught by generic Exception handler and re-wrapped as APIConnectionError

When the Inworld API returns an error in the response body (line 1104), the code raises an APIStatusError with the new x_request_id. However, this exception is caught by the except Exception handler at line 1117 and re-raised as a generic APIConnectionError, losing the status code, request ID, and error message.

Root Cause and Impact

The exception handler chain in ChunkedStream._run is:

  1. except asyncio.TimeoutError (line 1111)
  2. except aiohttp.ClientResponseError (line 1113)
  3. except Exception (line 1117)

Since APIStatusError inherits from APIError which inherits from Exception, the APIStatusError raised at line 1105 matches the except Exception clause. Compare with SynthesizeStream._run at livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py:1177 which correctly has except APIError: raise before except Exception.

Impact: The carefully-set x_request_id at line 1108 is effectively discarded. API errors from the Inworld service are misreported as connection errors, making debugging harder and potentially causing incorrect retry behavior (since APIStatusError with 4xx codes sets retryable=False, but APIConnectionError defaults to retryable=True).

Suggested change
except Exception as e:
raise APIConnectionError() from e
except (APIError, asyncio.TimeoutError, aiohttp.ClientResponseError):
raise
except Exception as e:
raise APIConnectionError() from e
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Expand Down
Loading