Skip to content

Commit adac196

Browse files
deepgram-stt: report connection-lifetime remainder so usage matches billing
STTMetrics.audio_duration accumulated only the duration of audio frames pushed to the Deepgram WebSocket via the PeriodicCollector. Deepgram bills for the full WebSocket connection lifetime, so cost-tracking users saw a consistent 4–5 s per-stream undercount against Deepgram's usage reports (much worse on short calls). Track the connection open timestamp and the total duration already reported via _on_audio_duration_report, and on close emit any remainder between wall-clock connection lifetime and reported frames. The collector is also flushed before the remainder calc so pending frame duration is accounted for first. Fixes #5498
1 parent 2bb75be commit adac196

1 file changed

Lines changed: 20 additions & 0 deletions

File tree

  • livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram

livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ def __init__(
410410

411411
self._request_id = ""
412412
self._reconnect_event = asyncio.Event()
413+
# Track how much duration has already been reported so we can emit
414+
# the connection-lifetime remainder on close, matching what Deepgram
415+
# actually bills (which includes WebSocket open/teardown overhead
416+
# beyond the pushed audio frames).
417+
self._reported_duration: float = 0.0
413418

414419
def update_options(
415420
self,
@@ -562,8 +567,10 @@ async def recv_task(ws: aiohttp.ClientWebSocketResponse) -> None:
562567
ws: aiohttp.ClientWebSocketResponse | None = None
563568

564569
while True:
570+
conn_start_time = 0.0
565571
try:
566572
ws = await self._connect_ws()
573+
conn_start_time = time.perf_counter()
567574
tasks = [
568575
asyncio.create_task(send_task(ws)),
569576
asyncio.create_task(recv_task(ws)),
@@ -593,6 +600,18 @@ async def recv_task(ws: aiohttp.ClientWebSocketResponse) -> None:
593600
finally:
594601
if ws is not None:
595602
await ws.close()
603+
# Deepgram bills WebSocket lifetime, not just audio
604+
# frames pushed. Emit the remainder between the
605+
# connection's wall-clock lifetime and the frame
606+
# durations we've already reported so usage reflects
607+
# what the provider actually charges for.
608+
if conn_start_time:
609+
self._audio_duration_collector.flush()
610+
lifetime = time.perf_counter() - conn_start_time
611+
remainder = lifetime - self._reported_duration
612+
if remainder > 0:
613+
self._on_audio_duration_report(remainder)
614+
self._reported_duration = 0.0
596615

597616
async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse:
598617
live_config: dict[str, Any] = {
@@ -646,6 +665,7 @@ async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse:
646665
return ws
647666

648667
def _on_audio_duration_report(self, duration: float) -> None:
668+
self._reported_duration += duration
649669
usage_event = stt.SpeechEvent(
650670
type=stt.SpeechEventType.RECOGNITION_USAGE,
651671
request_id=self._request_id,

0 commit comments

Comments
 (0)