Skip to content

fix: emit repeated keyterm query params for multi-keyterm streaming - #81

Merged
GregHolmes merged 3 commits into
mainfrom
fix/streaming-keyterm-list-serialization
Jul 23, 2026
Merged

fix: emit repeated keyterm query params for multi-keyterm streaming#81
GregHolmes merged 3 commits into
mainfrom
fix/streaming-keyterm-list-serialization

Conversation

@jkroll-deepgram

@jkroll-deepgram jkroll-deepgram commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

The listen v1 (Nova-3) and v2 (Flux) streaming WebSocket clients mis-serialize a multi-value keyterm. When keyterm is a List<String>, the client stringifies the whole list into a single query param instead of emitting repeated params:

❌ keyterm=%5Ba%2C+b%5D        // one param, value "[a, b]"
✅ keyterm=a&keyterm=b          // one param per term (correct)

Per the keyterm docs, multiple key terms must be sent as repeated keyterm= params. The server treats the stringified list as a single nonsense term, so instead of boosting the intended terms, it degrades recognition of them.

Root cause

Both streaming clients build the query with:

urlBuilder.addQueryParameter("keyterm", String.valueOf(options.getKeyterm().get()));

String.valueOf(...) on a List<String> yields Java's list toString()[a, b] — which okhttp then percent-encodes into one param. The prerecorded/REST path is unaffected (it uses QueryStringMapper with arraysAsRepeats).

Fix

Emit one keyterm param per term in both streaming clients (instanceof Iterable handles the List case; scalar string keyterms are unchanged). Also freeze V1WebSocketClient.java in .fernignoreV2WebSocketClient.java was already frozen, so otherwise the v1 fix would be lost on the next Fern regeneration.

Impact

  • Single-string keyterms: unchanged.
  • Multi-keyterm streaming: now correct. Before this fix there was no way to send more than one keyterm on a streaming connection — of(List.of(...)) mangled the value, and even a one-element list produced [term].

Tests

Added connect-handshake wire coverage (MockWebServer, asserting the captured upgrade URL):

  • ListenV2ConnectWireTest (Flux): list → repeated params; single string → one param.
  • ListenV1ConnectWireTest (Nova-3, new): list → repeated params.

Existing numerals wire tests still pass. ./gradlew spotlessJavaCheck test is green.

Manual verification

Streamed a real audio clip to Flux (flux-general-en) with a two-element keyterm list, real-time paced:

keyterm before after
List.of("a", "b") boosted term consistently mis-transcribed (6/6) correct (5/5)
"a" (single) correct correct
none correct correct

The listen v1 (Nova-3) and v2 (Flux) streaming WebSocket clients serialized a
List<String> keyterm by stringifying the whole list into a single query param
(keyterm=[a, b]) instead of repeated params (keyterm=a&keyterm=b). The server
treats the stringified list as one nonsense key term, degrading recognition of
the terms it was meant to boost.

Emit one keyterm query param per term in both streaming clients. Freeze
V1WebSocketClient in .fernignore (V2 was already frozen) so the fix survives
Fern regeneration.
Add connect-handshake wire tests asserting that a multi-value keyterm
serializes as repeated params (keyterm=a&keyterm=b) rather than a single
stringified list, for both v2 (Flux) and v1 (Nova-3) streaming clients.
Also pin the single-string keyterm case to one param.
@GregHolmes
GregHolmes merged commit 8aa4e4a into main Jul 23, 2026
8 checks passed
GregHolmes added a commit that referenced this pull request Jul 23, 2026
Generalizes #81, which fixed multi-value keyterm serialization on the
streaming connect path. The same bug affects every array-valued query
param: the generated streaming clients stringify the value, so a
multi-value list collapses into a single param instead of repeated
params. The server treats the stringified list as one nonsense value.
The prerecorded REST path is unaffected.

Route every array-valued streaming param through the shared array-aware
serializer the REST path already uses, with repeats enabled, and drop
PR #81's bespoke per-term loop. Params fixed on v1 -- keyterm, keywords,
replace, search, tag, extra. Params fixed on v2 -- keyterm, tag,
language_hint.

language_hint on v2 Flux, a string-or-list union structurally identical
to keyterm, had the identical bug and is included here. Scalar strings
are unchanged; an empty list now omits the param.

Both streaming clients are already frozen in .fernignore; comments
broadened to cover all multi-value query params. The durable fix belongs
upstream in the Fern generator WebSocket template.

Closes #77
dg-coreylweathers pushed a commit that referenced this pull request Jul 24, 2026
…#82)

## Summary

Generalizes #81. That PR fixed multi-value `keyterm` serialization on
the streaming connect path, but the same bug affects every array-valued
query param. The generated streaming clients serialize each param by
stringifying the value, so a multi-value `List<String>` collapses into a
single param instead of repeated params:

- Wrong: `keyterm=%5Ba%2C+b%5D` — one param, value "[a, b]"
- Right: `keyterm=a&keyterm=b` — one param per value

The server treats the stringified list as a single nonsense value. The
prerecorded REST path is unaffected — it already routes query params
through the shared array-aware serializer with repeats enabled.

## Root cause

Not a spec problem. The same Fern generator produces two query-building
paths from the same spec. The REST path uses the shared array-aware
serializer and is correct. The streaming/WebSocket path is hand-rolled
and stringifies lists, so it mangles them. This is a generator defect in
the WebSocket client template.

## Fix

Route every array-valued streaming param through the same shared
serializer the REST path uses, with array-as-repeats enabled, and drop
PR #81's bespoke per-term loop. Params fixed:

- v1 on `/v1/listen`: keyterm, keywords, replace, search, tag, extra
- v2 on `/v2/listen`: keyterm, tag, language_hint

`language_hint` on v2 Flux — a `String | List<String>` union
structurally identical to `keyterm` — had the identical bug and is
included here.

## Behavior

- Single-string values: unchanged.
- Multi-value lists: now correct as repeated params.
- Empty list: now omits the param instead of emitting an empty
stringified list.

## Freeze / regen

Both streaming clients are already frozen in `.fernignore`; comments
broadened from "multi-keyterm" to "multi-value query param". The durable
fix belongs upstream in the Fern generator's WebSocket template, tracked
so these entries can be unfrozen once it lands.

## Tests

Extended connect-handshake wire coverage with MockWebServer, asserting
the captured upgrade URL for every array param on both clients, plus
scalar-string guards. `./gradlew spotlessJavaCheck test` is green. Also
verified with a throwaway matrix across 9 params and 4 shapes —
multi-list, single-list, scalar, empty — plus exact raw-query and
percent-encoding assertions, all passing.

Closes #77
GregHolmes pushed a commit that referenced this pull request Jul 24, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.7.1](v0.7.0...v0.7.1)
(2026-07-24)


### Bug Fixes

* emit additionalProperties as query params on streaming connect
([#86](#86))
([28229fc](28229fc))
* emit repeated query params for all multi-value streaming options
([#82](#82))
([746be15](746be15)),
closes [#77](#77)
* emit repeated tag query params for multi-value speak v2 streaming
([#85](#85))
([e748619](e748619))

#### What's in this release

Three related fixes to streaming (WebSocket) query-parameter
serialization on `listen` and `speak`. All are bug fixes — no API
changes.

**Multi-keyterm streaming fix**
- Streaming connections mis-serialized a multi-value `keyterm` (and
other array-valued params): a `List<String>` was stringified into a
single param (`keyterm=[a, b]`) instead of repeated params
(`keyterm=a&keyterm=b`). The server treats the stringified list as one
nonsense term, so multiple key terms were not boosted — recognition of
them was actually degraded.
- Now fixed for every array-valued streaming query param — `listen`:
`keyterm`, `keywords`, `replace`, `search`, `tag`, `extra`,
`language_hint`; `speak`: `tag` — by routing them through the same
repeated-param serialization the REST path already uses (#81, #82, #85;
closes #77).
- **If you worked around this by hand-joining terms into a single
string, drop that workaround** — pass a real `List<String>` (e.g.
`ListenV1Keyterm.of(List.of("a", "b"))`) and each term is now sent as
its own `keyterm=` param.

**Unmodeled streaming query params now work (e.g. `no_delay`)**
- The streaming `connect(...)` builders dropped `additionalProperties`,
so unmodeled params set via `.additionalProperty("no_delay", true)`
never reached the URL — it was impossible to set `no_delay` on a
streaming connection.
- Now fixed on all four connect clients (`listen` v1/v2, `speak` v1/v2):
`additionalProperties` are emitted as query params, so any
not-yet-modeled param can be passed through until it gains a typed
option (#86; closes #83).

_All four streaming clients remain frozen in `.fernignore`; the durable
fix belongs in the Fern generator's WebSocket template, tracked for the
next generator upgrade._

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants