Skip to content

Commit 7deb302

Browse files
authored
fix(websockets): tolerate unknown message types from API (#671)
## Summary - Backport of the v6.0.1 `construct_type` fix to the v5 maintenance branch - Adds `unchecked_base_model.py` (from v6) with `construct_type` which does best-effort union coercion instead of strict Pydantic validation - Switches all 4 socket clients (listen v1, listen v2, speak v1, agent v1) from `parse_obj_as` to `construct_type` - Unknown WebSocket message types (e.g. `ConfigureSuccess`) now pass through without crashing the listener ## Test plan - [x] 853 unit/integration tests pass - [x] Verified against live Deepgram API with Listen V2 (Flux) — `ConfigureSuccess` unknown message type coerced cleanly as `ListenV2ConnectedEvent` - [x] Verify no regressions in existing v5 consumers
1 parent 7c0290d commit 7deb302

7 files changed

Lines changed: 471 additions & 12 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Example: Live Transcription with WebSocket V2 (Listen V2)
3+
4+
This example shows how to use Listen V2 for advanced conversational speech recognition
5+
with contextual turn detection.
6+
"""
7+
8+
import os
9+
import threading
10+
import time
11+
from pathlib import Path
12+
from typing import Union
13+
14+
from dotenv import load_dotenv
15+
16+
load_dotenv()
17+
18+
from deepgram import DeepgramClient
19+
from deepgram.core.events import EventType
20+
from deepgram.extensions.types.sockets import (
21+
ListenV2ConnectedEvent,
22+
ListenV2ControlMessage,
23+
ListenV2FatalErrorEvent,
24+
ListenV2TurnInfoEvent,
25+
)
26+
27+
ListenV2SocketClientResponse = Union[ListenV2ConnectedEvent, ListenV2TurnInfoEvent, ListenV2FatalErrorEvent]
28+
29+
client = DeepgramClient(api_key=os.environ.get("DEEPGRAM_API_KEY"))
30+
31+
try:
32+
with client.listen.v2.connect(
33+
model="flux-general-en",
34+
encoding="linear16",
35+
sample_rate="16000",
36+
) as connection:
37+
38+
def on_message(message: ListenV2SocketClientResponse) -> None:
39+
msg_type = getattr(message, "type", type(message).__name__)
40+
print(f"Received {msg_type} event ({type(message).__name__})")
41+
42+
# Extract transcription from TurnInfo events
43+
if isinstance(message, ListenV2TurnInfoEvent):
44+
print(f" transcript: {message.transcript}")
45+
print(f" event: {message.event}")
46+
print(f" turn_index: {message.turn_index}")
47+
48+
connection.on(EventType.OPEN, lambda _: print("Connection opened"))
49+
connection.on(EventType.MESSAGE, on_message)
50+
connection.on(EventType.CLOSE, lambda _: print("Connection closed"))
51+
connection.on(EventType.ERROR, lambda error: print(f"Error: {type(error).__name__}: {error}"))
52+
53+
# Send audio in a background thread so start_listening can process responses
54+
def send_audio():
55+
audio_path = Path(__file__).parent.parent / "fixtures" / "audio.wav"
56+
with open(audio_path, "rb") as f:
57+
audio = f.read()
58+
59+
# Send in chunks
60+
chunk_size = 4096
61+
for i in range(0, len(audio), chunk_size):
62+
connection.send_media(audio[i : i + chunk_size])
63+
time.sleep(0.01) # pace the sending
64+
65+
# Signal end of audio
66+
time.sleep(2)
67+
connection.send_control(ListenV2ControlMessage(type="CloseStream"))
68+
69+
sender = threading.Thread(target=send_audio, daemon=True)
70+
sender.start()
71+
72+
# This blocks until the connection closes
73+
connection.start_listening()
74+
75+
except Exception as e:
76+
print(f"Error: {type(e).__name__}: {e}")

src/deepgram/agent/v1/socket_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import websockets
99
import websockets.sync.connection as websockets_sync_connection
1010
from ...core.events import EventEmitterMixin, EventType
11-
from ...core.pydantic_utilities import parse_obj_as
11+
from ...core.unchecked_base_model import construct_type
1212

1313
try:
1414
from websockets.legacy.client import WebSocketClientProtocol # type: ignore
@@ -84,7 +84,7 @@ def _handle_binary_message(self, message: bytes) -> typing.Any:
8484
def _handle_json_message(self, message: str) -> typing.Any:
8585
"""Handle a JSON message by parsing it."""
8686
json_data = json.loads(message)
87-
return parse_obj_as(V1SocketClientResponse, json_data) # type: ignore
87+
return construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore
8888

8989
def _process_message(self, raw_message: typing.Any) -> typing.Tuple[typing.Any, bool]:
9090
"""Process a raw message, detecting if it's binary or JSON."""
@@ -199,7 +199,7 @@ def _handle_binary_message(self, message: bytes) -> typing.Any:
199199
def _handle_json_message(self, message: str) -> typing.Any:
200200
"""Handle a JSON message by parsing it."""
201201
json_data = json.loads(message)
202-
return parse_obj_as(V1SocketClientResponse, json_data) # type: ignore
202+
return construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore
203203

204204
def _process_message(self, raw_message: typing.Any) -> typing.Tuple[typing.Any, bool]:
205205
"""Process a raw message, detecting if it's binary or JSON."""

src/deepgram/core/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .remove_none_from_dict import remove_none_from_dict
2828
from .request_options import RequestOptions
2929
from .serialization import FieldMetadata, convert_and_respect_annotation_metadata
30+
from .unchecked_base_model import UncheckedBaseModel, UnionMetadata, construct_type
3031
_dynamic_imports: typing.Dict[str, str] = {
3132
"ApiError": ".api_error",
3233
"AsyncClientWrapper": ".client_wrapper",
@@ -44,6 +45,9 @@
4445
"SyncClientWrapper": ".client_wrapper",
4546
"UniversalBaseModel": ".pydantic_utilities",
4647
"UniversalRootModel": ".pydantic_utilities",
48+
"UncheckedBaseModel": ".unchecked_base_model",
49+
"UnionMetadata": ".unchecked_base_model",
50+
"construct_type": ".unchecked_base_model",
4751
"convert_and_respect_annotation_metadata": ".serialization",
4852
"convert_file_dict_to_httpx_tuples": ".file",
4953
"encode_query": ".query_encoder",
@@ -94,8 +98,11 @@ def __dir__():
9498
"IS_PYDANTIC_V2",
9599
"RequestOptions",
96100
"SyncClientWrapper",
101+
"UncheckedBaseModel",
102+
"UnionMetadata",
97103
"UniversalBaseModel",
98104
"UniversalRootModel",
105+
"construct_type",
99106
"convert_and_respect_annotation_metadata",
100107
"convert_file_dict_to_httpx_tuples",
101108
"encode_query",

0 commit comments

Comments
 (0)