mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
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:
parent
0897b663c5
commit
0b5b69ea3a
3 changed files with 78 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue