feat(tts): add websocket transport option for xAI TTS (#548)

* feat(tts): add websocket transport option for xAI TTS

xAI's pipecat service ships two implementations: XAIHttpTTSService
(batch REST, used by the existing xAI integration) and XAITTSService
(realtime WebSocket streaming). This adds a transport field on
XAITTSConfiguration ("http", default, unchanged behavior | "websocket")
and wires the latter into create_tts_service(), so live voice calls can
opt into lower time-to-first-byte streaming instead of request/response.

Originally built and battle-tested independently against v1.41.0 in a
production deployment (7 live Telnyx calls, 2026-07-15) before this PR
existed; ported onto current main and given a config toggle instead of
a second provider entry so it composes with the existing xAI HTTP path
rather than duplicating it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(tts): use websocket-only xAI TTS, drop the transport toggle

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
Flowentlabs 2026-07-21 08:00:49 +02:00 committed by GitHub
parent 177bbe6885
commit 94dce42209
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 13 additions and 27 deletions

View file

@ -1332,7 +1332,6 @@ class XAITTSConfiguration(BaseServiceConfiguration):
description="BCP-47 language code for synthesis (e.g. 'en', 'fr', 'de'), or 'auto' for automatic language detection.",
json_schema_extra={"allow_custom_input": True},
)
@computed_field
@property
def model(self) -> str:

View file

@ -86,7 +86,7 @@ from pipecat.services.speechmatics.stt import (
SpeechmaticsSTTService,
SpeechmaticsSTTSettings,
)
from pipecat.services.xai.tts import XAIHttpTTSService, XAITTSSettings
from pipecat.services.xai.tts import XAITTSService, XAIWebsocketTTSSettings
from pipecat.transcriptions.language import Language
from pipecat.utils.text.xml_function_tag_filter import XMLFunctionTagFilter
@ -817,11 +817,9 @@ def create_tts_service(
pipecat_language = Language(language_code)
except ValueError:
pipecat_language = Language.EN
return XAIHttpTTSService(
return XAITTSService(
api_key=user_config.tts.api_key,
sample_rate=audio_config.transport_out_sample_rate,
encoding="pcm",
settings=XAITTSSettings(
settings=XAIWebsocketTTSSettings(
voice=voice,
language=pipecat_language,
),

View file

@ -24,10 +24,7 @@ def test_xai_tts_configuration_defaults():
assert XAI_TTS_VOICES == ["eve", "ara", "leo", "rex", "sal"]
@pytest.mark.parametrize("transport_out_sample_rate", [8000, 16000])
def test_create_xai_tts_service_uses_pipeline_compatible_audio_format(
transport_out_sample_rate,
):
def test_create_xai_tts_service_uses_websocket_streaming():
user_config = SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.XAI.value,
@ -38,20 +35,18 @@ def test_create_xai_tts_service_uses_pipeline_compatible_audio_format(
)
)
audio_config = SimpleNamespace(
transport_out_sample_rate=transport_out_sample_rate,
transport_out_sample_rate=24000,
transport_in_sample_rate=16000,
)
with patch(
"api.services.pipecat.service_factory.XAIHttpTTSService"
) as mock_service:
with patch("api.services.pipecat.service_factory.XAITTSService") as mock_service:
create_tts_service(user_config, audio_config)
assert mock_service.call_count == 1
kwargs = mock_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["sample_rate"] == transport_out_sample_rate
assert kwargs["encoding"] == "pcm"
# Sample rate is resolved from the pipeline StartFrame, like other providers.
assert "sample_rate" not in kwargs
assert kwargs["settings"].voice == "rex"
assert kwargs["settings"].language == Language.EN
@ -71,9 +66,7 @@ def test_create_xai_tts_service_converts_language():
transport_in_sample_rate=16000,
)
with patch(
"api.services.pipecat.service_factory.XAIHttpTTSService"
) as mock_service:
with patch("api.services.pipecat.service_factory.XAITTSService") as mock_service:
create_tts_service(user_config, audio_config)
kwargs = mock_service.call_args.kwargs
@ -95,9 +88,7 @@ def test_create_xai_tts_service_falls_back_to_english_for_unknown_language():
transport_in_sample_rate=16000,
)
with patch(
"api.services.pipecat.service_factory.XAIHttpTTSService"
) as mock_service:
with patch("api.services.pipecat.service_factory.XAITTSService") as mock_service:
create_tts_service(user_config, audio_config)
kwargs = mock_service.call_args.kwargs
@ -119,9 +110,7 @@ def test_create_xai_tts_service_preserves_auto_language():
transport_in_sample_rate=16000,
)
with patch(
"api.services.pipecat.service_factory.XAIHttpTTSService"
) as mock_service:
with patch("api.services.pipecat.service_factory.XAITTSService") as mock_service:
create_tts_service(user_config, audio_config)
kwargs = mock_service.call_args.kwargs

4
ui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "ui",
"version": "1.41.0",
"version": "1.42.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "1.41.0",
"version": "1.42.0",
"dependencies": {
"@calcom/embed-react": "^1.5.3",
"@dagrejs/dagre": "^1.1.4",