Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ src/deepgram/client.py
# - construct_type keyword args fix (generator uses positional, function requires keyword-only)
# - except Exception broad catch (supports custom transports, generator narrows to WebSocketException)
# - _sanitize_numeric_types in agent socket client (float→int for API)
# - optional message param on control send_ methods (send_keep_alive, send_close_stream, etc.)
# so users don't need to instantiate the type themselves for no-payload control messages
# [temporarily frozen — generator bugs in construct_type call convention and exception handling]
src/deepgram/agent/v1/socket_client.py
src/deepgram/listen/v1/socket_client.py
Expand Down
8 changes: 4 additions & 4 deletions src/deepgram/agent/v1/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ async def send_function_call_response(self, message: AgentV1SendFunctionCallResp
"""
await self._send_model(message)

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

async def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None:
"""
Expand Down Expand Up @@ -292,12 +292,12 @@ def send_function_call_response(self, message: AgentV1SendFunctionCallResponse)
"""
self._send_model(message)

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

def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None:
"""
Expand Down
24 changes: 12 additions & 12 deletions src/deepgram/listen/v1/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,26 +81,26 @@ async def send_media(self, message: bytes) -> None:
"""
await self._send(message)

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

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

async def send_keep_alive(self, message: ListenV1KeepAlive) -> None:
async def send_keep_alive(self, message: typing.Optional[ListenV1KeepAlive] = None) -> None:
"""
Send a message to the websocket connection.
The message will be sent as a ListenV1KeepAlive.
"""
await self._send_model(message)
await self._send_model(message or ListenV1KeepAlive(type="KeepAlive"))
Comment on lines +84 to +103
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR restores optional control-message params, but there are no tests asserting that these send_*() control methods can be called with no arguments (and that they send the expected default payload). Since the repo already has coverage for listen.v1.socket_client behavior in tests/custom/test_transport.py, please add a small unit test that instantiates the socket client with a mock transport and verifies send_keep_alive()/send_close_stream()/send_finalize() work without args.

Copilot uses AI. Check for mistakes.

async def recv(self) -> V1SocketClientResponse:
"""
Expand Down Expand Up @@ -186,26 +186,26 @@ def send_media(self, message: bytes) -> None:
"""
self._send(message)

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

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

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

def recv(self) -> V1SocketClientResponse:
"""
Expand Down
8 changes: 4 additions & 4 deletions src/deepgram/listen/v2/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ async def send_media(self, message: bytes) -> None:
"""
await self._send(message)

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

async def recv(self) -> V2SocketClientResponse:
"""
Expand Down Expand Up @@ -169,12 +169,12 @@ def send_media(self, message: bytes) -> None:
"""
self._send(message)

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

def recv(self) -> V2SocketClientResponse:
"""
Expand Down
24 changes: 12 additions & 12 deletions src/deepgram/speak/v1/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,26 @@ async def send_text(self, message: SpeakV1Text) -> None:
"""
await self._send_model(message)

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

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

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

async def recv(self) -> V1SocketClientResponse:
"""
Expand Down Expand Up @@ -187,26 +187,26 @@ def send_text(self, message: SpeakV1Text) -> None:
"""
self._send_model(message)

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

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

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

def recv(self) -> V1SocketClientResponse:
"""
Expand Down
186 changes: 186 additions & 0 deletions tests/custom/test_websocket_control_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Tests that control send_ methods work without requiring a message argument.

Regression test for the breaking change where optional message params were lost
during a Fern regen, causing TypeError for callers using no-arg control calls.
"""

import json
from unittest.mock import AsyncMock, MagicMock

import pytest

from deepgram.agent.v1.socket_client import AsyncV1SocketClient as AsyncAgentV1SocketClient
from deepgram.agent.v1.socket_client import V1SocketClient as AgentV1SocketClient
from deepgram.listen.v1.socket_client import AsyncV1SocketClient as AsyncListenV1SocketClient
from deepgram.listen.v1.socket_client import V1SocketClient as ListenV1SocketClient
from deepgram.listen.v2.socket_client import AsyncV2SocketClient as AsyncListenV2SocketClient
from deepgram.listen.v2.socket_client import V2SocketClient as ListenV2SocketClient
from deepgram.speak.v1.socket_client import AsyncV1SocketClient as AsyncSpeakV1SocketClient
from deepgram.speak.v1.socket_client import V1SocketClient as SpeakV1SocketClient


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_async_ws():
ws = AsyncMock()
ws.send = AsyncMock()
return ws


def _make_sync_ws():
ws = MagicMock()
ws.send = MagicMock()
return ws


def _sent_json(ws):
"""Return the parsed JSON from the first send() call."""
call_args = ws.send.call_args
data = call_args[0][0]
return json.loads(data)


# ---------------------------------------------------------------------------
# speak/v1 — async
# ---------------------------------------------------------------------------

class TestAsyncSpeakV1ControlMessages:
async def test_send_flush_no_args(self):
ws = _make_async_ws()
client = AsyncSpeakV1SocketClient(websocket=ws)
await client.send_flush()
assert _sent_json(ws)["type"] == "Flush"

async def test_send_clear_no_args(self):
ws = _make_async_ws()
client = AsyncSpeakV1SocketClient(websocket=ws)
await client.send_clear()
assert _sent_json(ws)["type"] == "Clear"

async def test_send_close_no_args(self):
ws = _make_async_ws()
client = AsyncSpeakV1SocketClient(websocket=ws)
await client.send_close()
assert _sent_json(ws)["type"] == "Close"


# ---------------------------------------------------------------------------
# speak/v1 — sync
# ---------------------------------------------------------------------------

class TestSyncSpeakV1ControlMessages:
def test_send_flush_no_args(self):
ws = _make_sync_ws()
client = SpeakV1SocketClient(websocket=ws)
client.send_flush()
assert _sent_json(ws)["type"] == "Flush"

def test_send_clear_no_args(self):
ws = _make_sync_ws()
client = SpeakV1SocketClient(websocket=ws)
client.send_clear()
assert _sent_json(ws)["type"] == "Clear"

def test_send_close_no_args(self):
ws = _make_sync_ws()
client = SpeakV1SocketClient(websocket=ws)
client.send_close()
assert _sent_json(ws)["type"] == "Close"


# ---------------------------------------------------------------------------
# listen/v1 — async
# ---------------------------------------------------------------------------

class TestAsyncListenV1ControlMessages:
async def test_send_finalize_no_args(self):
ws = _make_async_ws()
client = AsyncListenV1SocketClient(websocket=ws)
await client.send_finalize()
assert _sent_json(ws)["type"] == "Finalize"

async def test_send_close_stream_no_args(self):
ws = _make_async_ws()
client = AsyncListenV1SocketClient(websocket=ws)
await client.send_close_stream()
assert _sent_json(ws)["type"] == "CloseStream"

async def test_send_keep_alive_no_args(self):
ws = _make_async_ws()
client = AsyncListenV1SocketClient(websocket=ws)
await client.send_keep_alive()
assert _sent_json(ws)["type"] == "KeepAlive"


# ---------------------------------------------------------------------------
# listen/v1 — sync
# ---------------------------------------------------------------------------

class TestSyncListenV1ControlMessages:
def test_send_finalize_no_args(self):
ws = _make_sync_ws()
client = ListenV1SocketClient(websocket=ws)
client.send_finalize()
assert _sent_json(ws)["type"] == "Finalize"

def test_send_close_stream_no_args(self):
ws = _make_sync_ws()
client = ListenV1SocketClient(websocket=ws)
client.send_close_stream()
assert _sent_json(ws)["type"] == "CloseStream"

def test_send_keep_alive_no_args(self):
ws = _make_sync_ws()
client = ListenV1SocketClient(websocket=ws)
client.send_keep_alive()
assert _sent_json(ws)["type"] == "KeepAlive"


# ---------------------------------------------------------------------------
# listen/v2 — async
# ---------------------------------------------------------------------------

class TestAsyncListenV2ControlMessages:
async def test_send_close_stream_no_args(self):
ws = _make_async_ws()
client = AsyncListenV2SocketClient(websocket=ws)
await client.send_close_stream()
assert _sent_json(ws)["type"] == "CloseStream"


# ---------------------------------------------------------------------------
# listen/v2 — sync
# ---------------------------------------------------------------------------

class TestSyncListenV2ControlMessages:
def test_send_close_stream_no_args(self):
ws = _make_sync_ws()
client = ListenV2SocketClient(websocket=ws)
client.send_close_stream()
assert _sent_json(ws)["type"] == "CloseStream"


# ---------------------------------------------------------------------------
# agent/v1 — async
# ---------------------------------------------------------------------------

class TestAsyncAgentV1ControlMessages:
async def test_send_keep_alive_no_args(self):
ws = _make_async_ws()
client = AsyncAgentV1SocketClient(websocket=ws)
await client.send_keep_alive()
assert _sent_json(ws)["type"] == "KeepAlive"


# ---------------------------------------------------------------------------
# agent/v1 — sync
# ---------------------------------------------------------------------------

class TestSyncAgentV1ControlMessages:
def test_send_keep_alive_no_args(self):
ws = _make_sync_ws()
client = AgentV1SocketClient(websocket=ws)
client.send_keep_alive()
assert _sent_json(ws)["type"] == "KeepAlive"
Loading