Skip to content

Commit 0018fc4

Browse files
authored
fix(websockets): restore optional message param on control send_ methods (#680)
## Summary - Control `send_` methods (`send_keep_alive`, `send_close_stream`, `send_finalize`, `send_flush`, `send_clear`, `send_close`) lost their optional `message` parameter in the last regen - This shipped as a breaking change in a minor patch — callers using `send_keep_alive()` etc. without args started getting `TypeError` - Restores `message: Optional[T] = None` with a typed default on all control methods across all 4 WebSocket clients - Payload-carrying methods (`send_text`, `send_media`, `send_settings`, etc.) remain required ## Affected files - `src/deepgram/speak/v1/socket_client.py` — `send_flush`, `send_clear`, `send_close` - `src/deepgram/listen/v1/socket_client.py` — `send_finalize`, `send_close_stream`, `send_keep_alive` - `src/deepgram/listen/v2/socket_client.py` — `send_close_stream` - `src/deepgram/agent/v1/socket_client.py` — `send_keep_alive` - `.fernignore` — updated comment to document this patch ## Test plan - [x] Verify `send_keep_alive()`, `send_close_stream()`, `send_finalize()`, `send_flush()`, `send_clear()`, `send_close()` can be called with no arguments - [x] Verify passing an explicit message object still works - [x] `pytest` passes
1 parent 18568bb commit 0018fc4

File tree

6 files changed

+220
-32
lines changed

6 files changed

+220
-32
lines changed

.fernignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ src/deepgram/client.py
88
# - construct_type keyword args fix (generator uses positional, function requires keyword-only)
99
# - except Exception broad catch (supports custom transports, generator narrows to WebSocketException)
1010
# - _sanitize_numeric_types in agent socket client (float→int for API)
11+
# - optional message param on control send_ methods (send_keep_alive, send_close_stream, etc.)
12+
# so users don't need to instantiate the type themselves for no-payload control messages
1113
# [temporarily frozen — generator bugs in construct_type call convention and exception handling]
1214
src/deepgram/agent/v1/socket_client.py
1315
src/deepgram/listen/v1/socket_client.py

src/deepgram/agent/v1/socket_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ async def send_function_call_response(self, message: AgentV1SendFunctionCallResp
159159
"""
160160
await self._send_model(message)
161161

162-
async def send_keep_alive(self, message: AgentV1KeepAlive) -> None:
162+
async def send_keep_alive(self, message: typing.Optional[AgentV1KeepAlive] = None) -> None:
163163
"""
164164
Send a message to the websocket connection.
165165
The message will be sent as a AgentV1KeepAlive.
166166
"""
167-
await self._send_model(message)
167+
await self._send_model(message or AgentV1KeepAlive(type="KeepAlive"))
168168

169169
async def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None:
170170
"""
@@ -292,12 +292,12 @@ def send_function_call_response(self, message: AgentV1SendFunctionCallResponse)
292292
"""
293293
self._send_model(message)
294294

295-
def send_keep_alive(self, message: AgentV1KeepAlive) -> None:
295+
def send_keep_alive(self, message: typing.Optional[AgentV1KeepAlive] = None) -> None:
296296
"""
297297
Send a message to the websocket connection.
298298
The message will be sent as a AgentV1KeepAlive.
299299
"""
300-
self._send_model(message)
300+
self._send_model(message or AgentV1KeepAlive(type="KeepAlive"))
301301

302302
def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None:
303303
"""

src/deepgram/listen/v1/socket_client.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,26 +81,26 @@ async def send_media(self, message: bytes) -> None:
8181
"""
8282
await self._send(message)
8383

84-
async def send_finalize(self, message: ListenV1Finalize) -> None:
84+
async def send_finalize(self, message: typing.Optional[ListenV1Finalize] = None) -> None:
8585
"""
8686
Send a message to the websocket connection.
8787
The message will be sent as a ListenV1Finalize.
8888
"""
89-
await self._send_model(message)
89+
await self._send_model(message or ListenV1Finalize(type="Finalize"))
9090

91-
async def send_close_stream(self, message: ListenV1CloseStream) -> None:
91+
async def send_close_stream(self, message: typing.Optional[ListenV1CloseStream] = None) -> None:
9292
"""
9393
Send a message to the websocket connection.
9494
The message will be sent as a ListenV1CloseStream.
9595
"""
96-
await self._send_model(message)
96+
await self._send_model(message or ListenV1CloseStream(type="CloseStream"))
9797

98-
async def send_keep_alive(self, message: ListenV1KeepAlive) -> None:
98+
async def send_keep_alive(self, message: typing.Optional[ListenV1KeepAlive] = None) -> None:
9999
"""
100100
Send a message to the websocket connection.
101101
The message will be sent as a ListenV1KeepAlive.
102102
"""
103-
await self._send_model(message)
103+
await self._send_model(message or ListenV1KeepAlive(type="KeepAlive"))
104104

105105
async def recv(self) -> V1SocketClientResponse:
106106
"""
@@ -186,26 +186,26 @@ def send_media(self, message: bytes) -> None:
186186
"""
187187
self._send(message)
188188

189-
def send_finalize(self, message: ListenV1Finalize) -> None:
189+
def send_finalize(self, message: typing.Optional[ListenV1Finalize] = None) -> None:
190190
"""
191191
Send a message to the websocket connection.
192192
The message will be sent as a ListenV1Finalize.
193193
"""
194-
self._send_model(message)
194+
self._send_model(message or ListenV1Finalize(type="Finalize"))
195195

196-
def send_close_stream(self, message: ListenV1CloseStream) -> None:
196+
def send_close_stream(self, message: typing.Optional[ListenV1CloseStream] = None) -> None:
197197
"""
198198
Send a message to the websocket connection.
199199
The message will be sent as a ListenV1CloseStream.
200200
"""
201-
self._send_model(message)
201+
self._send_model(message or ListenV1CloseStream(type="CloseStream"))
202202

203-
def send_keep_alive(self, message: ListenV1KeepAlive) -> None:
203+
def send_keep_alive(self, message: typing.Optional[ListenV1KeepAlive] = None) -> None:
204204
"""
205205
Send a message to the websocket connection.
206206
The message will be sent as a ListenV1KeepAlive.
207207
"""
208-
self._send_model(message)
208+
self._send_model(message or ListenV1KeepAlive(type="KeepAlive"))
209209

210210
def recv(self) -> V1SocketClientResponse:
211211
"""

src/deepgram/listen/v2/socket_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,12 @@ async def send_media(self, message: bytes) -> None:
7878
"""
7979
await self._send(message)
8080

81-
async def send_close_stream(self, message: ListenV2CloseStream) -> None:
81+
async def send_close_stream(self, message: typing.Optional[ListenV2CloseStream] = None) -> None:
8282
"""
8383
Send a message to the websocket connection.
8484
The message will be sent as a ListenV2CloseStream.
8585
"""
86-
await self._send_model(message)
86+
await self._send_model(message or ListenV2CloseStream(type="CloseStream"))
8787

8888
async def recv(self) -> V2SocketClientResponse:
8989
"""
@@ -169,12 +169,12 @@ def send_media(self, message: bytes) -> None:
169169
"""
170170
self._send(message)
171171

172-
def send_close_stream(self, message: ListenV2CloseStream) -> None:
172+
def send_close_stream(self, message: typing.Optional[ListenV2CloseStream] = None) -> None:
173173
"""
174174
Send a message to the websocket connection.
175175
The message will be sent as a ListenV2CloseStream.
176176
"""
177-
self._send_model(message)
177+
self._send_model(message or ListenV2CloseStream(type="CloseStream"))
178178

179179
def recv(self) -> V2SocketClientResponse:
180180
"""

src/deepgram/speak/v1/socket_client.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,26 @@ async def send_text(self, message: SpeakV1Text) -> None:
8282
"""
8383
await self._send_model(message)
8484

85-
async def send_flush(self, message: SpeakV1Flush) -> None:
85+
async def send_flush(self, message: typing.Optional[SpeakV1Flush] = None) -> None:
8686
"""
8787
Send a message to the websocket connection.
8888
The message will be sent as a SpeakV1Flush.
8989
"""
90-
await self._send_model(message)
90+
await self._send_model(message or SpeakV1Flush(type="Flush"))
9191

92-
async def send_clear(self, message: SpeakV1Clear) -> None:
92+
async def send_clear(self, message: typing.Optional[SpeakV1Clear] = None) -> None:
9393
"""
9494
Send a message to the websocket connection.
9595
The message will be sent as a SpeakV1Clear.
9696
"""
97-
await self._send_model(message)
97+
await self._send_model(message or SpeakV1Clear(type="Clear"))
9898

99-
async def send_close(self, message: SpeakV1Close) -> None:
99+
async def send_close(self, message: typing.Optional[SpeakV1Close] = None) -> None:
100100
"""
101101
Send a message to the websocket connection.
102102
The message will be sent as a SpeakV1Close.
103103
"""
104-
await self._send_model(message)
104+
await self._send_model(message or SpeakV1Close(type="Close"))
105105

106106
async def recv(self) -> V1SocketClientResponse:
107107
"""
@@ -187,26 +187,26 @@ def send_text(self, message: SpeakV1Text) -> None:
187187
"""
188188
self._send_model(message)
189189

190-
def send_flush(self, message: SpeakV1Flush) -> None:
190+
def send_flush(self, message: typing.Optional[SpeakV1Flush] = None) -> None:
191191
"""
192192
Send a message to the websocket connection.
193193
The message will be sent as a SpeakV1Flush.
194194
"""
195-
self._send_model(message)
195+
self._send_model(message or SpeakV1Flush(type="Flush"))
196196

197-
def send_clear(self, message: SpeakV1Clear) -> None:
197+
def send_clear(self, message: typing.Optional[SpeakV1Clear] = None) -> None:
198198
"""
199199
Send a message to the websocket connection.
200200
The message will be sent as a SpeakV1Clear.
201201
"""
202-
self._send_model(message)
202+
self._send_model(message or SpeakV1Clear(type="Clear"))
203203

204-
def send_close(self, message: SpeakV1Close) -> None:
204+
def send_close(self, message: typing.Optional[SpeakV1Close] = None) -> None:
205205
"""
206206
Send a message to the websocket connection.
207207
The message will be sent as a SpeakV1Close.
208208
"""
209-
self._send_model(message)
209+
self._send_model(message or SpeakV1Close(type="Close"))
210210

211211
def recv(self) -> V1SocketClientResponse:
212212
"""
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Tests that control send_ methods work without requiring a message argument.
2+
3+
Regression test for the breaking change where optional message params were lost
4+
during a Fern regen, causing TypeError for callers using no-arg control calls.
5+
"""
6+
7+
import json
8+
from unittest.mock import AsyncMock, MagicMock
9+
10+
import pytest
11+
12+
from deepgram.agent.v1.socket_client import AsyncV1SocketClient as AsyncAgentV1SocketClient
13+
from deepgram.agent.v1.socket_client import V1SocketClient as AgentV1SocketClient
14+
from deepgram.listen.v1.socket_client import AsyncV1SocketClient as AsyncListenV1SocketClient
15+
from deepgram.listen.v1.socket_client import V1SocketClient as ListenV1SocketClient
16+
from deepgram.listen.v2.socket_client import AsyncV2SocketClient as AsyncListenV2SocketClient
17+
from deepgram.listen.v2.socket_client import V2SocketClient as ListenV2SocketClient
18+
from deepgram.speak.v1.socket_client import AsyncV1SocketClient as AsyncSpeakV1SocketClient
19+
from deepgram.speak.v1.socket_client import V1SocketClient as SpeakV1SocketClient
20+
21+
22+
# ---------------------------------------------------------------------------
23+
# Helpers
24+
# ---------------------------------------------------------------------------
25+
26+
def _make_async_ws():
27+
ws = AsyncMock()
28+
ws.send = AsyncMock()
29+
return ws
30+
31+
32+
def _make_sync_ws():
33+
ws = MagicMock()
34+
ws.send = MagicMock()
35+
return ws
36+
37+
38+
def _sent_json(ws):
39+
"""Return the parsed JSON from the first send() call."""
40+
call_args = ws.send.call_args
41+
data = call_args[0][0]
42+
return json.loads(data)
43+
44+
45+
# ---------------------------------------------------------------------------
46+
# speak/v1 — async
47+
# ---------------------------------------------------------------------------
48+
49+
class TestAsyncSpeakV1ControlMessages:
50+
async def test_send_flush_no_args(self):
51+
ws = _make_async_ws()
52+
client = AsyncSpeakV1SocketClient(websocket=ws)
53+
await client.send_flush()
54+
assert _sent_json(ws)["type"] == "Flush"
55+
56+
async def test_send_clear_no_args(self):
57+
ws = _make_async_ws()
58+
client = AsyncSpeakV1SocketClient(websocket=ws)
59+
await client.send_clear()
60+
assert _sent_json(ws)["type"] == "Clear"
61+
62+
async def test_send_close_no_args(self):
63+
ws = _make_async_ws()
64+
client = AsyncSpeakV1SocketClient(websocket=ws)
65+
await client.send_close()
66+
assert _sent_json(ws)["type"] == "Close"
67+
68+
69+
# ---------------------------------------------------------------------------
70+
# speak/v1 — sync
71+
# ---------------------------------------------------------------------------
72+
73+
class TestSyncSpeakV1ControlMessages:
74+
def test_send_flush_no_args(self):
75+
ws = _make_sync_ws()
76+
client = SpeakV1SocketClient(websocket=ws)
77+
client.send_flush()
78+
assert _sent_json(ws)["type"] == "Flush"
79+
80+
def test_send_clear_no_args(self):
81+
ws = _make_sync_ws()
82+
client = SpeakV1SocketClient(websocket=ws)
83+
client.send_clear()
84+
assert _sent_json(ws)["type"] == "Clear"
85+
86+
def test_send_close_no_args(self):
87+
ws = _make_sync_ws()
88+
client = SpeakV1SocketClient(websocket=ws)
89+
client.send_close()
90+
assert _sent_json(ws)["type"] == "Close"
91+
92+
93+
# ---------------------------------------------------------------------------
94+
# listen/v1 — async
95+
# ---------------------------------------------------------------------------
96+
97+
class TestAsyncListenV1ControlMessages:
98+
async def test_send_finalize_no_args(self):
99+
ws = _make_async_ws()
100+
client = AsyncListenV1SocketClient(websocket=ws)
101+
await client.send_finalize()
102+
assert _sent_json(ws)["type"] == "Finalize"
103+
104+
async def test_send_close_stream_no_args(self):
105+
ws = _make_async_ws()
106+
client = AsyncListenV1SocketClient(websocket=ws)
107+
await client.send_close_stream()
108+
assert _sent_json(ws)["type"] == "CloseStream"
109+
110+
async def test_send_keep_alive_no_args(self):
111+
ws = _make_async_ws()
112+
client = AsyncListenV1SocketClient(websocket=ws)
113+
await client.send_keep_alive()
114+
assert _sent_json(ws)["type"] == "KeepAlive"
115+
116+
117+
# ---------------------------------------------------------------------------
118+
# listen/v1 — sync
119+
# ---------------------------------------------------------------------------
120+
121+
class TestSyncListenV1ControlMessages:
122+
def test_send_finalize_no_args(self):
123+
ws = _make_sync_ws()
124+
client = ListenV1SocketClient(websocket=ws)
125+
client.send_finalize()
126+
assert _sent_json(ws)["type"] == "Finalize"
127+
128+
def test_send_close_stream_no_args(self):
129+
ws = _make_sync_ws()
130+
client = ListenV1SocketClient(websocket=ws)
131+
client.send_close_stream()
132+
assert _sent_json(ws)["type"] == "CloseStream"
133+
134+
def test_send_keep_alive_no_args(self):
135+
ws = _make_sync_ws()
136+
client = ListenV1SocketClient(websocket=ws)
137+
client.send_keep_alive()
138+
assert _sent_json(ws)["type"] == "KeepAlive"
139+
140+
141+
# ---------------------------------------------------------------------------
142+
# listen/v2 — async
143+
# ---------------------------------------------------------------------------
144+
145+
class TestAsyncListenV2ControlMessages:
146+
async def test_send_close_stream_no_args(self):
147+
ws = _make_async_ws()
148+
client = AsyncListenV2SocketClient(websocket=ws)
149+
await client.send_close_stream()
150+
assert _sent_json(ws)["type"] == "CloseStream"
151+
152+
153+
# ---------------------------------------------------------------------------
154+
# listen/v2 — sync
155+
# ---------------------------------------------------------------------------
156+
157+
class TestSyncListenV2ControlMessages:
158+
def test_send_close_stream_no_args(self):
159+
ws = _make_sync_ws()
160+
client = ListenV2SocketClient(websocket=ws)
161+
client.send_close_stream()
162+
assert _sent_json(ws)["type"] == "CloseStream"
163+
164+
165+
# ---------------------------------------------------------------------------
166+
# agent/v1 — async
167+
# ---------------------------------------------------------------------------
168+
169+
class TestAsyncAgentV1ControlMessages:
170+
async def test_send_keep_alive_no_args(self):
171+
ws = _make_async_ws()
172+
client = AsyncAgentV1SocketClient(websocket=ws)
173+
await client.send_keep_alive()
174+
assert _sent_json(ws)["type"] == "KeepAlive"
175+
176+
177+
# ---------------------------------------------------------------------------
178+
# agent/v1 — sync
179+
# ---------------------------------------------------------------------------
180+
181+
class TestSyncAgentV1ControlMessages:
182+
def test_send_keep_alive_no_args(self):
183+
ws = _make_sync_ws()
184+
client = AgentV1SocketClient(websocket=ws)
185+
client.send_keep_alive()
186+
assert _sent_json(ws)["type"] == "KeepAlive"

0 commit comments

Comments
 (0)