Skip to content

Commit d1d70d1

Browse files
authored
Merge pull request #84 from lfnovo/fix/tts-factory-config-support
fix: align create_text_to_speech signature with other factory methods
2 parents 642f6be + 89c0528 commit d1d70d1

File tree

4 files changed

+163
-10
lines changed

4 files changed

+163
-10
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.19.2] - 2026-02-14
11+
12+
### Fixed
13+
14+
- **TTS Factory Config Support** - Aligned `create_text_to_speech()` signature with other factory methods by adding `config` dict parameter. Previously, `config={"api_key": "..."}` was silently ignored, causing authentication failures for callers using the standard config pattern.
15+
16+
### Deprecated
17+
18+
- Direct `api_key` and `base_url` parameters on `create_text_to_speech()` — use `config={"api_key": "...", "base_url": "..."}` instead.
19+
- Removed `api_key` parameter from `create_tts()` deprecated alias.
20+
1021
## [2.19.1] - 2026-02-12
1122

1223
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "esperanto"
3-
version = "2.19.1"
3+
version = "2.19.2"
44
description = "A light-weight, production-ready, unified interface for various AI model providers"
55
authors = [
66
{ name = "LUIS NOVO", email = "lfnovo@gmail.com" }

src/esperanto/factory.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def create_text_to_speech(
300300
cls,
301301
provider: str,
302302
model_name: Optional[str] = None,
303+
config: Optional[Dict[str, Any]] = None,
303304
api_key: Optional[str] = None,
304305
base_url: Optional[str] = None,
305306
**kwargs,
@@ -309,8 +310,9 @@ def create_text_to_speech(
309310
Args:
310311
provider: Provider name (openai, elevenlabs, google)
311312
model_name: Name of the model to use
312-
api_key: API key for the provider
313-
base_url: Optional base URL for the API
313+
config: Optional configuration dict for the model
314+
api_key: Deprecated. Use config={"api_key": "..."} instead.
315+
base_url: Deprecated. Use config={"base_url": "..."} instead.
314316
**kwargs: Additional provider-specific configuration
315317
316318
Returns:
@@ -320,9 +322,28 @@ def create_text_to_speech(
320322
ValueError: If provider is not supported
321323
ImportError: If provider module is not installed
322324
"""
323-
provider_class = cls._import_provider_class("text_to_speech", provider)
324-
return provider_class(
325-
model_name=model_name, api_key=api_key, base_url=base_url, **kwargs
325+
config = config or {}
326+
327+
if api_key is not None:
328+
warnings.warn(
329+
"Passing api_key directly to create_text_to_speech() is deprecated. "
330+
'Use config={"api_key": "..."} instead.',
331+
DeprecationWarning,
332+
stacklevel=2,
333+
)
334+
config["api_key"] = api_key
335+
336+
if base_url is not None:
337+
warnings.warn(
338+
"Passing base_url directly to create_text_to_speech() is deprecated. "
339+
'Use config={"base_url": "..."} instead.',
340+
DeprecationWarning,
341+
stacklevel=2,
342+
)
343+
config["base_url"] = base_url
344+
345+
return cls._create_instance(
346+
"text_to_speech", provider, model_name=model_name, **config, **kwargs
326347
)
327348

328349
@classmethod
@@ -356,15 +377,13 @@ def create_tts(
356377
provider: str,
357378
model_name: Optional[str] = None,
358379
config: Optional[Dict[str, Any]] = None,
359-
api_key: Optional[str] = None,
360380
) -> TextToSpeechModel:
361381
"""Create a text-to-speech model instance (alias for create_text_to_speech).
362382
363383
Args:
364384
provider: Provider name
365385
model_name: Optional name of the model to use
366386
config: Optional configuration for the model
367-
api_key: Optional API key for authentication
368387
369388
Returns:
370389
Text-to-speech model instance
@@ -376,7 +395,7 @@ def create_tts(
376395
stacklevel=2,
377396
)
378397
return cls.create_text_to_speech(
379-
provider, model_name=model_name, config=config, api_key=api_key
398+
provider, model_name=model_name, config=config
380399
)
381400

382401
@classmethod

tests/test_factory.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,133 @@
11
"""Tests for the AIFactory class."""
22

3+
import warnings
4+
35
import pytest
46
from unittest.mock import patch, MagicMock
57

68
from esperanto.factory import AIFactory
79

810

911
# Note: Caching functionality has been removed from AIFactory.
10-
# Tests now verify that factory creates new instances each time.
12+
# Tests now verify that factory creates new instances each time.
13+
14+
15+
class TestCreateTextToSpeech:
16+
"""Tests for create_text_to_speech config dict support."""
17+
18+
@patch.object(AIFactory, "_import_provider_class")
19+
def test_config_dict_passes_api_key_and_base_url(self, mock_import):
20+
mock_cls = MagicMock()
21+
mock_import.return_value = mock_cls
22+
23+
AIFactory.create_text_to_speech(
24+
"openai",
25+
model_name="tts-1",
26+
config={"api_key": "sk-test", "base_url": "https://custom.api"},
27+
)
28+
29+
mock_cls.assert_called_once_with(
30+
model_name="tts-1",
31+
api_key="sk-test",
32+
base_url="https://custom.api",
33+
)
34+
35+
@patch.object(AIFactory, "_import_provider_class")
36+
def test_direct_api_key_emits_deprecation_warning(self, mock_import):
37+
mock_cls = MagicMock()
38+
mock_import.return_value = mock_cls
39+
40+
with warnings.catch_warnings(record=True) as w:
41+
warnings.simplefilter("always")
42+
AIFactory.create_text_to_speech(
43+
"openai", model_name="tts-1", api_key="sk-test"
44+
)
45+
46+
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
47+
assert len(deprecation_warnings) == 1
48+
assert "api_key" in str(deprecation_warnings[0].message)
49+
50+
mock_cls.assert_called_once_with(
51+
model_name="tts-1",
52+
api_key="sk-test",
53+
)
54+
55+
@patch.object(AIFactory, "_import_provider_class")
56+
def test_direct_base_url_emits_deprecation_warning(self, mock_import):
57+
mock_cls = MagicMock()
58+
mock_import.return_value = mock_cls
59+
60+
with warnings.catch_warnings(record=True) as w:
61+
warnings.simplefilter("always")
62+
AIFactory.create_text_to_speech(
63+
"openai", model_name="tts-1", base_url="https://custom.api"
64+
)
65+
66+
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
67+
assert len(deprecation_warnings) == 1
68+
assert "base_url" in str(deprecation_warnings[0].message)
69+
70+
@patch.object(AIFactory, "_import_provider_class")
71+
def test_direct_params_override_config(self, mock_import):
72+
"""Direct api_key/base_url take precedence when config also has them."""
73+
mock_cls = MagicMock()
74+
mock_import.return_value = mock_cls
75+
76+
with warnings.catch_warnings(record=True):
77+
warnings.simplefilter("always")
78+
AIFactory.create_text_to_speech(
79+
"openai",
80+
model_name="tts-1",
81+
config={"api_key": "config-key", "base_url": "https://config.api"},
82+
api_key="direct-key",
83+
base_url="https://direct.api",
84+
)
85+
86+
mock_cls.assert_called_once_with(
87+
model_name="tts-1",
88+
api_key="direct-key",
89+
base_url="https://direct.api",
90+
)
91+
92+
@patch.object(AIFactory, "_import_provider_class")
93+
def test_config_without_api_key(self, mock_import):
94+
"""Config dict works even without api_key/base_url."""
95+
mock_cls = MagicMock()
96+
mock_import.return_value = mock_cls
97+
98+
AIFactory.create_text_to_speech(
99+
"openai",
100+
model_name="tts-1",
101+
config={"voice": "alloy"},
102+
)
103+
104+
mock_cls.assert_called_once_with(
105+
model_name="tts-1",
106+
voice="alloy",
107+
)
108+
109+
@patch.object(AIFactory, "_import_provider_class")
110+
def test_create_tts_alias_forwards_config(self, mock_import):
111+
"""The deprecated create_tts alias correctly forwards config."""
112+
mock_cls = MagicMock()
113+
mock_import.return_value = mock_cls
114+
115+
with warnings.catch_warnings(record=True) as w:
116+
warnings.simplefilter("always")
117+
AIFactory.create_tts(
118+
"openai",
119+
model_name="tts-1",
120+
config={"api_key": "sk-test"},
121+
)
122+
123+
# Should have the create_tts deprecation warning
124+
tts_warnings = [
125+
x for x in w
126+
if issubclass(x.category, DeprecationWarning) and "create_tts" in str(x.message)
127+
]
128+
assert len(tts_warnings) == 1
129+
130+
mock_cls.assert_called_once_with(
131+
model_name="tts-1",
132+
api_key="sk-test",
133+
)

0 commit comments

Comments
 (0)