Skip to content

Commit 203ee04

Browse files
authored
fix: ensure user agent is consistent across http/websockets (#65)
* refactor: rename _get_headers to get_headers and update usage across the codebase * feat: add User-Agent header to HTTP clients in io.py and websocket.py * test: increase delay to 1 second to avoid SSL errors in WebSocket connections * test: increase flaky test reruns for websocket streaming tests
1 parent a56b90c commit 203ee04

File tree

7 files changed

+42
-21
lines changed

7 files changed

+42
-21
lines changed

src/fish_audio_sdk/io.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ def __init__(self, apikey: str, *, base_url: str = "https://api.fish.audio"):
3434
def init_async_client(self):
3535
self._async_client = httpx.AsyncClient(
3636
base_url=self._base_url,
37-
headers={"Authorization": f"Bearer {self._apikey}"},
37+
headers={
38+
"Authorization": f"Bearer {self._apikey}",
39+
"User-Agent": "fish-audio/python/legacy",
40+
},
3841
timeout=None,
3942
)
4043

4144
def init_sync_client(self):
4245
self._sync_client = httpx.Client(
4346
base_url=self._base_url,
44-
headers={"Authorization": f"Bearer {self._apikey}"},
47+
headers={
48+
"Authorization": f"Bearer {self._apikey}",
49+
"User-Agent": "fish-audio/python/legacy",
50+
},
4551
timeout=None,
4652
)
4753

src/fish_audio_sdk/websocket.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ def __init__(
2424
self._executor = ThreadPoolExecutor(max_workers=max_workers)
2525
self._client = httpx.Client(
2626
base_url=self._base_url,
27-
headers={"Authorization": f"Bearer {self._apikey}"},
27+
headers={
28+
"Authorization": f"Bearer {self._apikey}",
29+
"User-Agent": "fish-audio/python/legacy",
30+
},
2831
)
2932

3033
def __enter__(self):
@@ -97,7 +100,10 @@ def __init__(
97100
self._base_url = base_url
98101
self._client = httpx.AsyncClient(
99102
base_url=self._base_url,
100-
headers={"Authorization": f"Bearer {self._apikey}"},
103+
headers={
104+
"Authorization": f"Bearer {self._apikey}",
105+
"User-Agent": "fish-audio/python/legacy",
106+
},
101107
)
102108

103109
async def __aenter__(self):

src/fishaudio/core/client_wrapper.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ def __init__(
6060
)
6161
self.base_url = base_url
6262

63-
def _get_headers(
63+
def get_headers(
6464
self, additional_headers: Optional[Dict[str, str]] = None
6565
) -> Dict[str, str]:
66-
"""Build headers including authentication."""
66+
"""Build headers including authentication and user agent."""
6767
headers = {
6868
"Authorization": f"Bearer {self.api_key}",
6969
"User-Agent": f"fish-audio/python/{__version__}",
@@ -77,7 +77,7 @@ def _prepare_request_kwargs(
7777
) -> None:
7878
"""Prepare request kwargs by merging headers, timeout, and query params."""
7979
# Merge headers
80-
headers = self._get_headers()
80+
headers = self.get_headers()
8181
if request_options and request_options.additional_headers:
8282
headers.update(request_options.additional_headers)
8383
kwargs["headers"] = {**headers, **kwargs.get("headers", {})}
@@ -113,7 +113,7 @@ def __init__(
113113
self._client = httpx.Client(
114114
base_url=base_url,
115115
timeout=httpx.Timeout(timeout),
116-
headers=self._get_headers(),
116+
headers=self.get_headers(),
117117
)
118118

119119
def request(
@@ -185,7 +185,7 @@ def __init__(
185185
self._client = httpx.AsyncClient(
186186
base_url=base_url,
187187
timeout=httpx.Timeout(timeout),
188-
headers=self._get_headers(),
188+
headers=self.get_headers(),
189189
)
190190

191191
async def request(

src/fishaudio/resources/tts.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,7 @@ def text_generator():
329329
with connect_ws(
330330
"/v1/tts/live",
331331
client=self._client.client,
332-
headers={
333-
"model": model,
334-
"Authorization": f"Bearer {self._client.api_key}",
335-
},
332+
headers=self._client.get_headers({"model": model}),
336333
**ws_kwargs,
337334
) as ws:
338335

@@ -630,7 +627,7 @@ async def text_generator():
630627
async with aconnect_ws(
631628
"/v1/tts/live",
632629
client=self._client.client,
633-
headers={"model": model, "Authorization": f"Bearer {self._client.api_key}"},
630+
headers=self._client.get_headers({"model": model}),
634631
**ws_kwargs,
635632
) as ws:
636633

tests/integration/test_tts_websocket_integration.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def text_stream():
3434
# Save the audio
3535
save_audio(audio_chunks, "test_websocket_streaming.mp3")
3636

37-
@pytest.mark.flaky(reruns=2, reruns_delay=1)
37+
@pytest.mark.flaky(reruns=9, reruns_delay=1)
3838
def test_websocket_streaming_with_different_models(self, client, save_audio):
3939
"""Test WebSocket streaming with different models."""
4040
import time
@@ -53,7 +53,7 @@ def text_stream():
5353
save_audio(audio_chunks, f"test_websocket_model_{model}.mp3")
5454

5555
# Brief delay to avoid SSL errors when opening next WebSocket connection
56-
time.sleep(0.3)
56+
time.sleep(1.0)
5757

5858
def test_websocket_streaming_with_wav_format(self, client, save_audio):
5959
"""Test WebSocket streaming with WAV format."""
@@ -220,7 +220,7 @@ async def text_stream():
220220
save_audio(audio_chunks, "test_async_websocket_streaming.mp3")
221221

222222
@pytest.mark.asyncio
223-
@pytest.mark.flaky(reruns=2, reruns_delay=1)
223+
@pytest.mark.flaky(reruns=9, reruns_delay=1)
224224
async def test_async_websocket_streaming_with_different_models(
225225
self, async_client, save_audio
226226
):
@@ -246,7 +246,7 @@ async def text_stream():
246246
save_audio(audio_chunks, f"test_async_websocket_model_{model}.mp3")
247247

248248
# Brief delay to avoid SSL errors when opening next WebSocket connection
249-
await asyncio.sleep(0.3)
249+
await asyncio.sleep(1.0)
250250

251251
@pytest.mark.asyncio
252252
async def test_async_websocket_streaming_with_format(

tests/unit/test_core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ def test_init_with_env_var(self, mock_api_key):
111111

112112
def test_get_headers(self, mock_api_key):
113113
wrapper = ClientWrapper(api_key=mock_api_key)
114-
headers = wrapper._get_headers()
114+
headers = wrapper.get_headers()
115115
assert headers["Authorization"] == f"Bearer {mock_api_key}"
116116
assert "User-Agent" in headers
117117

118118
def test_get_headers_with_additional(self, mock_api_key):
119119
wrapper = ClientWrapper(api_key=mock_api_key)
120-
headers = wrapper._get_headers({"X-Custom": "value"})
120+
headers = wrapper.get_headers({"X-Custom": "value"})
121121
assert headers["X-Custom"] == "value"
122122
assert headers["Authorization"] == f"Bearer {mock_api_key}"
123123

@@ -139,6 +139,6 @@ def test_init_without_api_key_raises(self):
139139

140140
def test_get_headers(self, mock_api_key):
141141
wrapper = AsyncClientWrapper(api_key=mock_api_key)
142-
headers = wrapper._get_headers()
142+
headers = wrapper.get_headers()
143143
assert headers["Authorization"] == f"Bearer {mock_api_key}"
144144
assert "User-Agent" in headers

tests/unit/test_tts_realtime.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def mock_client_wrapper(mock_api_key):
1616
wrapper.api_key = mock_api_key
1717
# Mock the underlying httpx.Client
1818
wrapper._client = Mock()
19+
# Mock get_headers to return a dict with the additional headers merged
20+
wrapper.get_headers = lambda additional=None: {
21+
"Authorization": f"Bearer {mock_api_key}",
22+
"User-Agent": "fish-audio/python/test",
23+
**(additional or {}),
24+
}
1925
return wrapper
2026

2127

@@ -26,6 +32,12 @@ def async_mock_client_wrapper(mock_api_key):
2632
wrapper.api_key = mock_api_key
2733
# Mock the underlying httpx.AsyncClient
2834
wrapper._client = Mock()
35+
# Mock get_headers to return a dict with the additional headers merged
36+
wrapper.get_headers = lambda additional=None: {
37+
"Authorization": f"Bearer {mock_api_key}",
38+
"User-Agent": "fish-audio/python/test",
39+
**(additional or {}),
40+
}
2941
return wrapper
3042

3143

0 commit comments

Comments
 (0)