diff --git a/.changeset/fix-deepgram-query-encoding.md b/.changeset/fix-deepgram-query-encoding.md new file mode 100644 index 000000000..13ebe4774 --- /dev/null +++ b/.changeset/fix-deepgram-query-encoding.md @@ -0,0 +1,5 @@ +--- +"@livekit/agents-plugin-deepgram": patch +--- + +fix(deepgram): avoid double-encoding STT query params diff --git a/plugins/deepgram/src/_utils.test.ts b/plugins/deepgram/src/_utils.test.ts new file mode 100644 index 000000000..fca12dc37 --- /dev/null +++ b/plugins/deepgram/src/_utils.test.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it } from 'vitest'; +import { appendQueryParams } from './_utils.js'; + +describe('appendQueryParams', () => { + it('preserves non-ASCII keyterm and keywords values in the websocket URL', () => { + const url = new URL('wss://api.deepgram.com/v1/listen'); + + appendQueryParams(url, { + keyterm: ['słucham', 'dzień dobry', 'znamię', 'dziękuję'], + keywords: ['potwierdź:3', 'żółć:1.5'], + }); + + expect(url.searchParams.getAll('keyterm')).toEqual([ + 'słucham', + 'dzień dobry', + 'znamię', + 'dziękuję', + ]); + expect(url.searchParams.getAll('keywords')).toEqual(['potwierdź:3', 'żółć:1.5']); + expect(url.toString()).not.toContain('%25'); + }); +}); diff --git a/plugins/deepgram/src/_utils.ts b/plugins/deepgram/src/_utils.ts index 2c070afc0..10d4de5ca 100644 --- a/plugins/deepgram/src/_utils.ts +++ b/plugins/deepgram/src/_utils.ts @@ -48,3 +48,22 @@ export class PeriodicCollector { this.lastFlushTime = performance.now() / 1000; } } + +type QueryParamScalar = string | number | boolean; +type QueryParamValue = QueryParamScalar | QueryParamScalar[]; + +export function appendQueryParams( + url: URL, + params: Record, +): void { + Object.entries(params).forEach(([key, value]) => { + if (value === undefined) return; + + if (Array.isArray(value)) { + value.forEach((item) => url.searchParams.append(key, String(item))); + return; + } + + url.searchParams.append(key, String(value)); + }); +} diff --git a/plugins/deepgram/src/stt.ts b/plugins/deepgram/src/stt.ts index 0f10ee331..a4f67df59 100644 --- a/plugins/deepgram/src/stt.ts +++ b/plugins/deepgram/src/stt.ts @@ -17,7 +17,7 @@ import { } from '@livekit/agents'; import type { AudioFrame } from '@livekit/rtc-node'; import { WebSocket } from 'ws'; -import { PeriodicCollector } from './_utils.js'; +import { PeriodicCollector, appendQueryParams } from './_utils.js'; import type { STTLanguages, STTModels } from './models.js'; export interface STTOptions { @@ -195,15 +195,7 @@ export class SpeechStream extends stt.SpeechStream { language: this.#opts.language, mip_opt_out: this.#opts.mipOptOut, }; - Object.entries(params).forEach(([k, v]) => { - if (v !== undefined) { - if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { - streamURL.searchParams.append(k, encodeURIComponent(v)); - } else { - v.forEach((x) => streamURL.searchParams.append(k, encodeURIComponent(x))); - } - } - }); + appendQueryParams(streamURL, params); ws = new WebSocket(streamURL, { headers: { Authorization: `Token ${this.#opts.apiKey}` },