fix(deepgram): forward only the first model and language values to /listen

Authorization and pricing read the first model and language query value, but the raw query was forwarded, so Deepgram (which honours the last repeated value) could be sent a model the key was never allowed. Later duplicates of those two keys are now dropped before the upstream URL is built

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-18 21:29:29 +00:00
parent 0897b663c5
commit 0b5b69ea3a
3 changed files with 78 additions and 8 deletions

View file

@ -26,6 +26,7 @@ DEEPGRAM_LISTEN_ADDON_PRICING_PARAMS: Final = MappingProxyType(
}
)
_DISABLED_PARAM_VALUES: Final = frozenset({"", "false"})
_SINGLE_VALUED_PARAMS: Final = frozenset({"model", "language"})
class DeepgramException(BaseLLMException):
@ -36,13 +37,23 @@ def deepgram_listen_requested_model(query_string: str) -> str:
return httpx.QueryParams(query_string).get("model") or DEEPGRAM_LISTEN_DEFAULT_MODEL
def _first_occurrences(query_string: str) -> httpx.QueryParams:
"""Authorization and pricing read the first ``model`` and ``language`` value; Deepgram must not see a second one."""
items: Final = httpx.QueryParams(query_string).multi_items()
return httpx.QueryParams(
tuple(
(key, value)
for index, (key, value) in enumerate(items)
if key not in _SINGLE_VALUED_PARAMS or all(earlier != key for earlier, _ in items[:index])
)
)
def deepgram_listen_websocket_target(api_base: str | None, query_string: str) -> str:
listen_url: Final = httpx.URL(f"{(api_base or DEEPGRAM_DEFAULT_API_BASE).rstrip('/')}/listen")
websocket_url: Final = listen_url.copy_with(scheme=_WEBSOCKET_SCHEMES.get(listen_url.scheme, listen_url.scheme))
params: Final = httpx.QueryParams(query_string)
query: Final = (
query_string if params.get("model") else str(params.remove("model").add("model", DEEPGRAM_LISTEN_DEFAULT_MODEL))
)
params: Final = _first_occurrences(query_string)
query: Final = params if params.get("model") else params.remove("model").add("model", DEEPGRAM_LISTEN_DEFAULT_MODEL)
return f"{websocket_url}?{query}"
@ -64,7 +75,7 @@ def deepgram_listen_pricing_model(upstream_url: str) -> str:
the multilingual streaming entry when ``language=multi``, otherwise the model's own streaming entry. Pre-recorded
entries are never a substitute: Deepgram prices the two products differently."""
streaming: Final = f"{DEEPGRAM_LISTEN_STREAMING_PRICING_PREFIX}{deepgram_listen_model(upstream_url)}"
language: Final = parse_qs(urlparse(upstream_url).query).get("language", ("",))[-1]
language: Final = parse_qs(urlparse(upstream_url).query).get("language", ("",))[0]
if language.strip().lower() == DEEPGRAM_LISTEN_MULTILINGUAL_LANGUAGE:
return f"{streaming}{DEEPGRAM_LISTEN_MULTILINGUAL_PRICING_SUFFIX}"
return streaming

View file

@ -1,6 +1,7 @@
import math
from collections.abc import Mapping, Sequence
from typing import Final
from urllib.parse import parse_qs, urlparse
import pytest
@ -76,6 +77,24 @@ def _metadata(duration: object, channels: object = 1) -> dict[str, object]:
"wss://dg.internal/v1/listen?model=nova-3&keywords=a&keywords=b",
id="repeated keys preserved",
),
pytest.param(
None,
"model=nova-2&encoding=linear16&model=nova-3",
"wss://api.deepgram.com/v1/listen?model=nova-2&encoding=linear16",
id="only the authorized first model reaches deepgram",
),
pytest.param(
None,
"language=en&model=nova-3&language=multi",
"wss://api.deepgram.com/v1/listen?language=en&model=nova-3",
id="only the priced first language reaches deepgram",
),
pytest.param(
None,
"model=&model=nova-2",
"wss://api.deepgram.com/v1/listen?model=nova-3",
id="blank first model is the default, later models dropped",
),
],
)
def test_deepgram_listen_websocket_target(api_base: str | None, query_string: str, expected: str):
@ -210,12 +229,22 @@ def test_deepgram_listen_model_comes_from_the_upstream_query(upstream_url: str,
@pytest.mark.parametrize(
"query_string",
["model=nova-2&language=en", "language=en", "model=&language=en", "", "model=nova-3-medical"],
[
"model=nova-2&language=en",
"language=en",
"model=&language=en",
"",
"model=nova-3-medical",
"model=nova-2&model=nova-3",
"model=&model=nova-3-medical",
],
)
def test_requested_model_is_the_model_the_upstream_target_will_carry(query_string: str):
def test_requested_model_is_the_only_model_the_upstream_target_carries(query_string: str):
"""Authorization runs against ``deepgram_listen_requested_model``; the upstream URL is built separately, so the
two must always agree or a key could be authorized for one model and reach another."""
two must always agree or a key could be authorized for one model and reach another. Deepgram reads the last
repeated ``model``, so the target must carry exactly one."""
target: Final = deepgram_listen_websocket_target(None, query_string)
assert parse_qs(urlparse(target).query)["model"] == [deepgram_listen_requested_model(query_string)]
assert deepgram_listen_requested_model(query_string) == deepgram_listen_model(target)

View file

@ -421,6 +421,36 @@ def test_deepgram_listen_authorizes_the_model_it_will_actually_send_upstream(que
assert relay.calls == []
def test_deepgram_listen_strips_a_second_model_that_would_outrank_the_authorized_one(monkeypatch):
"""Deepgram honours the last repeated ``model``; auth and pricing read the first. A key allowed only ``nova-2``
must not smuggle ``nova-3`` past authorization behind an authorized first value."""
monkeypatch.delenv("DEEPGRAM_API_BASE", raising=False)
monkeypatch.setattr(litellm, "max_budget", 0.0)
_price_nova_2_streaming(monkeypatch)
cache = asyncio.run(_cache_restricted_key("sk-only-nova-2", ["nova-2"]))
relay = _FakeRelay()
client = TestClient(_app_with_relay(relay))
with (
patch(GET_CREDENTIALS, return_value="dg-provider-key"),
patch.multiple( # test-quality-ok: the real key auth path reads these proxy_server globals and has no injection seam
"litellm.proxy.proxy_server",
master_key="sk-master",
prisma_client=MagicMock(),
user_api_key_cache=cache,
llm_model_list=None,
llm_router=None,
),
):
with client.websocket_connect(
"/deepgram/v1/listen?model=nova-2&language=en&model=nova-3&language=multi",
headers={"Authorization": "Bearer sk-only-nova-2"},
):
pass
assert [call.target for call in relay.calls] == ["wss://api.deepgram.com/v1/listen?model=nova-2&language=en"]
def test_deepgram_listen_echoes_the_browser_subprotocol_that_carries_the_litellm_key(monkeypatch):
"""Browsers cannot set headers, so they send the key as a subprotocol and abort the handshake unless the
server echoes that subprotocol back; the key itself must still stay off the upstream connection."""