Skip to content

Commit 71283b6

Browse files
committed
fix:Update GenericTTS documentation and implementation to specify HTTP(S) support. Adjusted parameter requirements and added validation for URL format. Updated tests to reflect changes in vendor classification and serialization.
1 parent e7edda6 commit 71283b6

5 files changed

Lines changed: 59 additions & 24 deletions

File tree

docs/concepts/vendors.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Used with `agent.with_tts()`. Each TTS vendor produces audio at a specific sampl
6666
| `FishAudioTTS` | Fish Audio | `key`, `reference_id`, `backend` ||
6767
| `MurfTTS` | Murf | `key`, `voice_id`, `model` ||
6868
| `MiniMaxTTS` | MiniMax | `model` for supported Agora-managed global models; `key`, `group_id`, `model`, `voice_id`, `url` for BYOK ||
69-
| `GenericTTS` | Generic OpenAI-compatible TTS | `url`, `headers`, `model`, `voice` | Configurable |
69+
| `GenericTTS` | Generic OpenAI-compatible TTS over HTTP(S) | `url` | Configurable |
7070
| `DeepgramTTS` | Deepgram | `api_key`, `model` | Configurable |
7171
| `SarvamTTS` | Sarvam | `api_key` ||
7272
| `XaiTTS` | xAI | `api_key`, `language` | Configurable |
@@ -84,7 +84,7 @@ Used with `agent.with_tts()` when routing to `Area.CN`. Use `MiniMaxCNTTS` and `
8484
| `CosyVoiceTTS` | CosyVoice | `api_key`, `model`, `voice` ||
8585
| `BytedanceDuplexTTS` | ByteDance Duplex | `app_id`, `token`, `resource_id`, `speaker` ||
8686
| `StepFunTTS` | StepFun | `api_key`, `model`, `voice_id` ||
87-
| `GenericTTS` | Generic OpenAI-compatible TTS | `url`, `headers`, `model`, `voice` | Configurable |
87+
| `GenericTTS` | Generic OpenAI-compatible TTS over HTTP(S) | `url` | Configurable |
8888

8989
<!-- snippet: executable -->
9090
```python

docs/reference/vendors.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,14 @@ The SDK also includes named helpers for the remaining Agora-supported LLM provid
387387

388388
### `GenericTTS`
389389

390+
`GenericTTS` currently supports HTTP and HTTPS endpoints. WebSocket endpoints are rejected until a WebSocket-backed generic TTS implementation is available. AgentKit serializes the current HTTP implementation with the internal vendor value `generic_http`.
391+
390392
| Parameter | Type | Required | Default | Description |
391393
|---|---|---|---|---|
392-
| `url` | `str` | Yes || Callback address of the generic TTS service |
393-
| `headers` | `Dict[str, str]` | Yes | | Custom headers to include in requests to the generic TTS service |
394-
| `model` | `str` | Yes | | TTS model name |
395-
| `voice` | `str` | Yes | | Voice name |
394+
| `url` | `str` | Yes || HTTP(S) endpoint of the generic TTS service |
395+
| `headers` | `Dict[str, str]` | No | `None` | Custom headers to include in requests to the generic TTS service |
396+
| `model` | `str` | No | `None` | TTS model name |
397+
| `voice` | `str` | No | `None` | Voice name |
396398
| `api_key` | `str` | No | `None` | API key for the generic TTS service |
397399
| `speed` | `float` | No | `None` | Speech rate |
398400
| `sample_rate` | `int` | No | `None` | Output audio sample rate in Hz |

src/agora_agent/agentkit/vendors/tts.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any, Dict, List, Optional
2+
from urllib.parse import urlsplit
23

3-
from pydantic import ConfigDict, Field, model_validator
4+
from pydantic import ConfigDict, Field, field_validator, model_validator
45

56
from .base import BaseTTS, CartesiaSampleRate, ElevenLabsSampleRate, GoogleTTSSampleRate, MicrosoftSampleRate
67
from ..presets import MiniMaxPresetModels, OpenAITtsPresetModels
@@ -63,9 +64,9 @@ class MicrosoftTTS(BaseTTS):
6364
def to_config(self) -> Dict[str, Any]:
6465
params: Dict[str, Any] = dict(self.additional_params or {})
6566
params.update({
66-
"key": self.key,
67-
"region": self.region,
68-
"voice_name": self.voice_name,
67+
"key": self.key,
68+
"region": self.region,
69+
"voice_name": self.voice_name,
6970
})
7071

7172
if self.sample_rate is not None:
@@ -241,8 +242,8 @@ class DeepgramTTS(BaseTTS):
241242
def to_config(self) -> Dict[str, Any]:
242243
params: Dict[str, Any] = dict(self.additional_params or {})
243244
params.update({
244-
"api_key": self.api_key,
245-
"model": self.model,
245+
"api_key": self.api_key,
246+
"model": self.model,
246247
})
247248

248249
if self.base_url is not None:
@@ -516,10 +517,10 @@ def to_config(self) -> Dict[str, Any]:
516517
class GenericTTS(BaseTTS):
517518
model_config = ConfigDict(extra="forbid")
518519

519-
url: str = Field(..., description="Callback address of the generic TTS service")
520-
headers: Dict[str, str] = Field(..., description="Custom headers to include in requests to the generic TTS service")
521-
model: str = Field(..., description="TTS model name")
522-
voice: str = Field(..., description="Voice name")
520+
url: str = Field(..., description="HTTP(S) endpoint of the generic TTS service")
521+
headers: Optional[Dict[str, str]] = Field(default=None, description="Custom request headers")
522+
model: Optional[str] = Field(default=None, description="TTS model name")
523+
voice: Optional[str] = Field(default=None, description="Voice name")
523524
api_key: Optional[str] = Field(default=None, description="API key for the generic TTS service")
524525
speed: Optional[float] = Field(default=None, description="Speech rate")
525526
sample_rate: Optional[int] = Field(default=None, description="Output audio sample rate in Hz")
@@ -528,12 +529,22 @@ class GenericTTS(BaseTTS):
528529
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional generic TTS params")
529530
skip_patterns: Optional[List[int]] = Field(default=None)
530531

532+
@field_validator("url")
533+
@classmethod
534+
def validate_url(cls, value: str) -> str:
535+
parsed = urlsplit(value)
536+
if parsed.scheme.lower() not in {"http", "https"} or not parsed.netloc:
537+
raise ValueError("GenericTTS url must be a valid HTTP(S) endpoint")
538+
return value
539+
531540
def to_config(self) -> Dict[str, Any]:
532541
params: Dict[str, Any] = dict(self.additional_params or {})
533542
if self.api_key is not None:
534543
params["api_key"] = self.api_key
535-
params["model"] = self.model
536-
params["voice"] = self.voice
544+
if self.model is not None:
545+
params["model"] = self.model
546+
if self.voice is not None:
547+
params["voice"] = self.voice
537548
if self.speed is not None:
538549
params["speed"] = self.speed
539550
if self.sample_rate is not None:
@@ -544,11 +555,12 @@ def to_config(self) -> Dict[str, Any]:
544555
params["instruction"] = self.instruction
545556

546557
result: Dict[str, Any] = {
547-
"vendor": "generic",
558+
"vendor": "generic_http",
548559
"url": self.url,
549-
"headers": self.headers,
550560
"params": params,
551561
}
562+
if self.headers is not None:
563+
result["headers"] = self.headers
552564
if self.skip_patterns is not None:
553565
result["skip_patterns"] = self.skip_patterns
554566
return result

tests/custom/test_agentkit_vendors.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def test_xai_grok_emits_params_even_when_empty():
4242
assert XaiGrok(api_key="xai-key").to_config()["params"] == {}
4343

4444

45-
4645
def test_mllm_rejects_fields_not_in_core_contract():
4746
with pytest.raises(ValidationError):
4847
OpenAIRealtime(api_key="openai-key", predefined_tools=["_publish_message"])
@@ -224,7 +223,7 @@ def test_generic_tts_serializes() -> None:
224223
).to_config()
225224

226225
assert config == {
227-
"vendor": "generic",
226+
"vendor": "generic_http",
228227
"url": "https://tts.example.com/v1/audio",
229228
"headers": {"Authorization": "Bearer token"},
230229
"params": {
@@ -239,6 +238,28 @@ def test_generic_tts_serializes() -> None:
239238
}
240239

241240

241+
def test_generic_tts_only_requires_url() -> None:
242+
assert GenericTTS(url="https://tts.example.com/v1/audio").to_config() == {
243+
"vendor": "generic_http",
244+
"url": "https://tts.example.com/v1/audio",
245+
"params": {},
246+
}
247+
248+
249+
@pytest.mark.parametrize(
250+
"url",
251+
[
252+
"ws://tts.example.com/v1/audio",
253+
"wss://tts.example.com/v1/audio",
254+
"not-a-url",
255+
"https:///missing-host",
256+
],
257+
)
258+
def test_generic_tts_rejects_non_http_urls(url: str) -> None:
259+
with pytest.raises(ValidationError, match="valid HTTP"):
260+
GenericTTS(url=url)
261+
262+
242263
def test_xai_tts_serializes() -> None:
243264
config = XaiTTS(
244265
api_key="xai-tts-key",

tests/custom/test_regional_vendors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ def test_generic_tts_is_classified_as_shared_vendor() -> None:
168168

169169
assert cn_agent.__class__.__name__ == "CNAgent"
170170
assert global_agent.__class__.__name__ == "GlobalAgent"
171-
assert cn_agent.tts is not None and cn_agent.tts["vendor"] == "generic"
172-
assert global_agent.tts is not None and global_agent.tts["vendor"] == "generic"
171+
assert cn_agent.tts is not None and cn_agent.tts["vendor"] == "generic_http"
172+
assert global_agent.tts is not None and global_agent.tts["vendor"] == "generic_http"
173173

174174

175175
def test_xai_asr_and_tts_are_classified_as_global_vendors() -> None:

0 commit comments

Comments
 (0)