Skip to content

Commit 38cc1e5

Browse files
authored
fix(websockets): support array parameters in Listen v1 and v2 clients (#650)
* fix(websockets): support array parameters in Listen v1 and v2 clients Update WebSocket clients to properly handle parameters that accept both single strings and arrays of strings. Arrays are now correctly expanded into multiple query parameters (e.g., keyterm=term1&keyterm=term2) instead of being URL-encoded as a single stringified array. Fixed parameters in Listen V2: - keyterm: now accepts string | string[] - tag: now accepts string | string[] Fixed parameters in Listen V1: - keyterm: now accepts string | string[] - keywords: now accepts string | string[] - extra: now accepts string | string[] - redact: now accepts string | string[] - replace: now accepts string | string[] - search: now accepts string | string[] - tag: now accepts string | string[] Changes: - Updated client method signatures to accept Union[str, Sequence[str]] - Added runtime type checking to detect and iterate over arrays - Updated type definitions to reflect proper union types - Added documentation for array parameter support This fix ensures consistency with the AsyncAPI/OpenAPI specs and matches the behavior of the REST API clients. Fixes #648 Fixes #629 Related to #616 * fix(types): correct ListenV1Redact type definition - Wrap Union in Optional to match function signature - Add str as standalone option in Union to allow any string value - Maintains Literal types for common values (autocomplete support) Addresses Copilot AI review comments * test: update ListenV1Tag type assertion test Update test to reflect new type definition: - Changed from Optional[Any] to Optional[Union[str, Sequence[str]]] - Updated test name and docstring for clarity - Usage tests remain unchanged and already cover the correct behavior
1 parent 8ec9978 commit 38cc1e5

11 files changed

Lines changed: 132 additions & 52 deletions

src/deepgram/listen/v1/client.py

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,23 @@ def connect(
5151
dictation: typing.Optional[str] = None,
5252
encoding: typing.Optional[str] = None,
5353
endpointing: typing.Optional[str] = None,
54-
extra: typing.Optional[str] = None,
54+
extra: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
5555
interim_results: typing.Optional[str] = None,
56-
keyterm: typing.Optional[str] = None,
57-
keywords: typing.Optional[str] = None,
56+
keyterm: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
57+
keywords: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
5858
language: typing.Optional[str] = None,
5959
mip_opt_out: typing.Optional[str] = None,
6060
model: str,
6161
multichannel: typing.Optional[str] = None,
6262
numerals: typing.Optional[str] = None,
6363
profanity_filter: typing.Optional[str] = None,
6464
punctuate: typing.Optional[str] = None,
65-
redact: typing.Optional[str] = None,
66-
replace: typing.Optional[str] = None,
65+
redact: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
66+
replace: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
6767
sample_rate: typing.Optional[str] = None,
68-
search: typing.Optional[str] = None,
68+
search: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
6969
smart_format: typing.Optional[str] = None,
70-
tag: typing.Optional[str] = None,
70+
tag: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
7171
utterance_end_ms: typing.Optional[str] = None,
7272
vad_events: typing.Optional[str] = None,
7373
version: typing.Optional[str] = None,
@@ -163,13 +163,25 @@ def connect(
163163
if endpointing is not None:
164164
query_params = query_params.add("endpointing", endpointing)
165165
if extra is not None:
166-
query_params = query_params.add("extra", extra)
166+
if isinstance(extra, (list, tuple)):
167+
for item in extra:
168+
query_params = query_params.add("extra", str(item))
169+
else:
170+
query_params = query_params.add("extra", extra)
167171
if interim_results is not None:
168172
query_params = query_params.add("interim_results", interim_results)
169173
if keyterm is not None:
170-
query_params = query_params.add("keyterm", keyterm)
174+
if isinstance(keyterm, (list, tuple)):
175+
for term in keyterm:
176+
query_params = query_params.add("keyterm", str(term))
177+
else:
178+
query_params = query_params.add("keyterm", keyterm)
171179
if keywords is not None:
172-
query_params = query_params.add("keywords", keywords)
180+
if isinstance(keywords, (list, tuple)):
181+
for keyword in keywords:
182+
query_params = query_params.add("keywords", str(keyword))
183+
else:
184+
query_params = query_params.add("keywords", keywords)
173185
if language is not None:
174186
query_params = query_params.add("language", language)
175187
if mip_opt_out is not None:
@@ -185,17 +197,33 @@ def connect(
185197
if punctuate is not None:
186198
query_params = query_params.add("punctuate", punctuate)
187199
if redact is not None:
188-
query_params = query_params.add("redact", redact)
200+
if isinstance(redact, (list, tuple)):
201+
for item in redact:
202+
query_params = query_params.add("redact", str(item))
203+
else:
204+
query_params = query_params.add("redact", redact)
189205
if replace is not None:
190-
query_params = query_params.add("replace", replace)
206+
if isinstance(replace, (list, tuple)):
207+
for item in replace:
208+
query_params = query_params.add("replace", str(item))
209+
else:
210+
query_params = query_params.add("replace", replace)
191211
if sample_rate is not None:
192212
query_params = query_params.add("sample_rate", sample_rate)
193213
if search is not None:
194-
query_params = query_params.add("search", search)
214+
if isinstance(search, (list, tuple)):
215+
for item in search:
216+
query_params = query_params.add("search", str(item))
217+
else:
218+
query_params = query_params.add("search", search)
195219
if smart_format is not None:
196220
query_params = query_params.add("smart_format", smart_format)
197221
if tag is not None:
198-
query_params = query_params.add("tag", tag)
222+
if isinstance(tag, (list, tuple)):
223+
for item in tag:
224+
query_params = query_params.add("tag", str(item))
225+
else:
226+
query_params = query_params.add("tag", tag)
199227
if utterance_end_ms is not None:
200228
query_params = query_params.add("utterance_end_ms", utterance_end_ms)
201229
if vad_events is not None:
@@ -262,23 +290,23 @@ async def connect(
262290
dictation: typing.Optional[str] = None,
263291
encoding: typing.Optional[str] = None,
264292
endpointing: typing.Optional[str] = None,
265-
extra: typing.Optional[str] = None,
293+
extra: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
266294
interim_results: typing.Optional[str] = None,
267-
keyterm: typing.Optional[str] = None,
268-
keywords: typing.Optional[str] = None,
295+
keyterm: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
296+
keywords: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
269297
language: typing.Optional[str] = None,
270298
mip_opt_out: typing.Optional[str] = None,
271299
model: str,
272300
multichannel: typing.Optional[str] = None,
273301
numerals: typing.Optional[str] = None,
274302
profanity_filter: typing.Optional[str] = None,
275303
punctuate: typing.Optional[str] = None,
276-
redact: typing.Optional[str] = None,
277-
replace: typing.Optional[str] = None,
304+
redact: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
305+
replace: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
278306
sample_rate: typing.Optional[str] = None,
279-
search: typing.Optional[str] = None,
307+
search: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
280308
smart_format: typing.Optional[str] = None,
281-
tag: typing.Optional[str] = None,
309+
tag: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
282310
utterance_end_ms: typing.Optional[str] = None,
283311
vad_events: typing.Optional[str] = None,
284312
version: typing.Optional[str] = None,
@@ -374,13 +402,25 @@ async def connect(
374402
if endpointing is not None:
375403
query_params = query_params.add("endpointing", endpointing)
376404
if extra is not None:
377-
query_params = query_params.add("extra", extra)
405+
if isinstance(extra, (list, tuple)):
406+
for item in extra:
407+
query_params = query_params.add("extra", str(item))
408+
else:
409+
query_params = query_params.add("extra", extra)
378410
if interim_results is not None:
379411
query_params = query_params.add("interim_results", interim_results)
380412
if keyterm is not None:
381-
query_params = query_params.add("keyterm", keyterm)
413+
if isinstance(keyterm, (list, tuple)):
414+
for term in keyterm:
415+
query_params = query_params.add("keyterm", str(term))
416+
else:
417+
query_params = query_params.add("keyterm", keyterm)
382418
if keywords is not None:
383-
query_params = query_params.add("keywords", keywords)
419+
if isinstance(keywords, (list, tuple)):
420+
for keyword in keywords:
421+
query_params = query_params.add("keywords", str(keyword))
422+
else:
423+
query_params = query_params.add("keywords", keywords)
384424
if language is not None:
385425
query_params = query_params.add("language", language)
386426
if mip_opt_out is not None:
@@ -396,17 +436,33 @@ async def connect(
396436
if punctuate is not None:
397437
query_params = query_params.add("punctuate", punctuate)
398438
if redact is not None:
399-
query_params = query_params.add("redact", redact)
439+
if isinstance(redact, (list, tuple)):
440+
for item in redact:
441+
query_params = query_params.add("redact", str(item))
442+
else:
443+
query_params = query_params.add("redact", redact)
400444
if replace is not None:
401-
query_params = query_params.add("replace", replace)
445+
if isinstance(replace, (list, tuple)):
446+
for item in replace:
447+
query_params = query_params.add("replace", str(item))
448+
else:
449+
query_params = query_params.add("replace", replace)
402450
if sample_rate is not None:
403451
query_params = query_params.add("sample_rate", sample_rate)
404452
if search is not None:
405-
query_params = query_params.add("search", search)
453+
if isinstance(search, (list, tuple)):
454+
for item in search:
455+
query_params = query_params.add("search", str(item))
456+
else:
457+
query_params = query_params.add("search", search)
406458
if smart_format is not None:
407459
query_params = query_params.add("smart_format", smart_format)
408460
if tag is not None:
409-
query_params = query_params.add("tag", tag)
461+
if isinstance(tag, (list, tuple)):
462+
for item in tag:
463+
query_params = query_params.add("tag", str(item))
464+
else:
465+
query_params = query_params.add("tag", tag)
410466
if utterance_end_ms is not None:
411467
query_params = query_params.add("utterance_end_ms", utterance_end_ms)
412468
if vad_events is not None:

src/deepgram/listen/v2/client.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ def connect(
4343
eager_eot_threshold: typing.Optional[str] = None,
4444
eot_threshold: typing.Optional[str] = None,
4545
eot_timeout_ms: typing.Optional[str] = None,
46-
keyterm: typing.Optional[str] = None,
46+
keyterm: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
4747
mip_opt_out: typing.Optional[str] = None,
48-
tag: typing.Optional[str] = None,
48+
tag: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
4949
authorization: typing.Optional[str] = None,
5050
request_options: typing.Optional[RequestOptions] = None,
5151
) -> typing.Iterator[V2SocketClient]:
@@ -67,11 +67,13 @@ def connect(
6767
6868
eot_timeout_ms : typing.Optional[str]
6969
70-
keyterm : typing.Optional[str]
70+
keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]]
71+
Keyterm prompting can improve recognition of specialized terminology. Pass a single string or a list of strings.
7172
7273
mip_opt_out : typing.Optional[str]
7374
74-
tag : typing.Optional[str]
75+
tag : typing.Optional[typing.Union[str, typing.Sequence[str]]]
76+
Label your requests for the purpose of identification during usage reporting. Pass a single string or a list of strings.
7577
7678
authorization : typing.Optional[str]
7779
Use your API key for authentication, or alternatively generate a [temporary token](/guides/fundamentals/token-based-authentication) and pass it via the `token` query parameter.
@@ -100,11 +102,19 @@ def connect(
100102
if eot_timeout_ms is not None:
101103
query_params = query_params.add("eot_timeout_ms", eot_timeout_ms)
102104
if keyterm is not None:
103-
query_params = query_params.add("keyterm", keyterm)
105+
if isinstance(keyterm, (list, tuple)):
106+
for term in keyterm:
107+
query_params = query_params.add("keyterm", str(term))
108+
else:
109+
query_params = query_params.add("keyterm", keyterm)
104110
if mip_opt_out is not None:
105111
query_params = query_params.add("mip_opt_out", mip_opt_out)
106112
if tag is not None:
107-
query_params = query_params.add("tag", tag)
113+
if isinstance(tag, (list, tuple)):
114+
for t in tag:
115+
query_params = query_params.add("tag", str(t))
116+
else:
117+
query_params = query_params.add("tag", tag)
108118
ws_url = ws_url + f"?{query_params}"
109119
headers = self._raw_client._client_wrapper.get_headers()
110120
if authorization is not None:
@@ -154,9 +164,9 @@ async def connect(
154164
eager_eot_threshold: typing.Optional[str] = None,
155165
eot_threshold: typing.Optional[str] = None,
156166
eot_timeout_ms: typing.Optional[str] = None,
157-
keyterm: typing.Optional[str] = None,
167+
keyterm: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
158168
mip_opt_out: typing.Optional[str] = None,
159-
tag: typing.Optional[str] = None,
169+
tag: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
160170
authorization: typing.Optional[str] = None,
161171
request_options: typing.Optional[RequestOptions] = None,
162172
) -> typing.AsyncIterator[AsyncV2SocketClient]:
@@ -178,11 +188,13 @@ async def connect(
178188
179189
eot_timeout_ms : typing.Optional[str]
180190
181-
keyterm : typing.Optional[str]
191+
keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]]
192+
Keyterm prompting can improve recognition of specialized terminology. Pass a single string or a list of strings.
182193
183194
mip_opt_out : typing.Optional[str]
184195
185-
tag : typing.Optional[str]
196+
tag : typing.Optional[typing.Union[str, typing.Sequence[str]]]
197+
Label your requests for the purpose of identification during usage reporting. Pass a single string or a list of strings.
186198
187199
authorization : typing.Optional[str]
188200
Use your API key for authentication, or alternatively generate a [temporary token](/guides/fundamentals/token-based-authentication) and pass it via the `token` query parameter.
@@ -211,11 +223,19 @@ async def connect(
211223
if eot_timeout_ms is not None:
212224
query_params = query_params.add("eot_timeout_ms", eot_timeout_ms)
213225
if keyterm is not None:
214-
query_params = query_params.add("keyterm", keyterm)
226+
if isinstance(keyterm, (list, tuple)):
227+
for term in keyterm:
228+
query_params = query_params.add("keyterm", str(term))
229+
else:
230+
query_params = query_params.add("keyterm", keyterm)
215231
if mip_opt_out is not None:
216232
query_params = query_params.add("mip_opt_out", mip_opt_out)
217233
if tag is not None:
218-
query_params = query_params.add("tag", tag)
234+
if isinstance(tag, (list, tuple)):
235+
for t in tag:
236+
query_params = query_params.add("tag", str(t))
237+
else:
238+
query_params = query_params.add("tag", tag)
219239
ws_url = ws_url + f"?{query_params}"
220240
headers = self._raw_client._client_wrapper.get_headers()
221241
if authorization is not None:

src/deepgram/types/listen_v1extra.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Extra = typing.Optional[typing.Any]
5+
ListenV1Extra = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v1keyterm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Keyterm = typing.Optional[typing.Any]
5+
ListenV1Keyterm = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v1keywords.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Keywords = typing.Optional[typing.Any]
5+
ListenV1Keywords = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v1redact.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import typing
44

5-
ListenV1Redact = typing.Union[
6-
typing.Literal["true", "false", "pci", "numbers", "aggressive_numbers", "ssn"], typing.Any
5+
ListenV1Redact = typing.Optional[
6+
typing.Union[
7+
typing.Literal["true", "false", "pci", "numbers", "aggressive_numbers", "ssn"],
8+
str,
9+
typing.Sequence[str],
10+
]
711
]

src/deepgram/types/listen_v1replace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Replace = typing.Optional[typing.Any]
5+
ListenV1Replace = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v1search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Search = typing.Optional[typing.Any]
5+
ListenV1Search = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v1tag.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV1Tag = typing.Optional[typing.Any]
5+
ListenV1Tag = typing.Optional[typing.Union[str, typing.Sequence[str]]]

src/deepgram/types/listen_v2tag.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import typing
44

5-
ListenV2Tag = typing.Optional[typing.Any]
5+
ListenV2Tag = typing.Optional[typing.Union[str, typing.Sequence[str]]]

0 commit comments

Comments
 (0)