Skip to content

fix: route Speak V2 websocket through custom transport_factory - #766

Open
mayankbohradev wants to merge 2 commits into
deepgram:mainfrom
mayankbohradev:fix/speak-v2-transport-factory
Open

fix: route Speak V2 websocket through custom transport_factory#766
mayankbohradev wants to merge 2 commits into
deepgram:mainfrom
mayankbohradev:fix/speak-v2-transport-factory

Conversation

@mayankbohradev

@mayankbohradev mayankbohradev commented Aug 13, 2026

Copy link
Copy Markdown

Summary

A transport_factory passed to DeepgramClient or AsyncDeepgramClient was not applied to the Speak V2 WebSocket client. Speak V2 connected through the default websockets transport instead, with no error or warning.

Who this affects

transport_factory exists so callers can route WebSocket traffic through their own transport, including proxies, test doubles, and custom-hosted endpoints. Before this fix, Listen v1/v2, Speak v1, and Agent v1 used the custom transport while Speak V2 silently connected directly to api.deepgram.com.

This is especially important for callers relying on controlled network egress, auditing, or custom-hosted transports: accepting the factory but bypassing it can send traffic to an unintended endpoint without any signal.

Root cause

install_transport() patches the generated modules listed in _TARGET_MODULES in src/deepgram/transport.py. Speak V2 shipped after that hand-maintained list was created, and deepgram.speak.v2.raw_client and deepgram.speak.v2.client were never added.

The existing tests iterated _TARGET_MODULES itself, so they could verify registered modules were patched but could not detect a real WebSocket module missing from the list.

Changes

  • add deepgram.speak.v2.raw_client and deepgram.speak.v2.client to _TARGET_MODULES
  • add an independent source scan that fails when a generated WebSocket module is absent from the registry
  • add a non-vacuous guard proving discovery finds a known WebSocket module
  • document the _TARGET_MODULES regeneration invariant in AGENTS.md
  • update the stale module count and apply the configured import ordering

The guard intentionally uses a static source scan rather than pkgutil.walk_packages to avoid import side effects and still detect modules that fail to import. It asserts a subset rather than exact equality because the runtime registry may intentionally contain a module not present in a particular generated tree; the behavior-breaking case is a real WebSocket module absent from the registry.

Verification

  • confirmed the new guard fails on the unpatched tree and names both missing Speak V2 modules
  • no-network runtime probe confirmed both sync and async speak.v2.connect() invoke the supplied factory with the expected /v2/speak URL and authorization headers
  • full Docker-backed suite: 380 passed, 1 skipped
  • focused transport suite: 36 passed
  • mypy src/: clean across 882 source files
  • Ruff import-order check and src/deepgram/transport.py check: clean
  • git diff --check: clean
  • warning count unchanged: two existing websockets deprecation warnings

`_TARGET_MODULES` in transport.py lists the modules that
`install_transport()` patches so a user-supplied `transport_factory`
replaces the default `websockets` transport. The Speak V2 websocket
client shipped in 7.7.0 and binds the same patched symbols, but its two
modules were never added to the list.

Effect: a caller passing `transport_factory` gets custom-transport
routing for Listen v1/v2, Speak v1 and Agent v1, while Speak V2 opens a
direct connection to api.deepgram.com. There is no error or warning, so
traffic intended to flow through a proxied or custom-hosted transport
silently bypasses it.

The existing transport tests could not catch this. They iterate
`_TARGET_MODULES` itself, so they verify the list is internally
consistent but cannot detect a module missing from it -- the missing
entry is never iterated. Left as-is, the next regen that adds a
websocket client reintroduces the same silent bypass.

Changes:
- add deepgram.speak.v2.raw_client and deepgram.speak.v2.client to
  `_TARGET_MODULES` (and correct the now-stale count in the comment)
- add a completeness guard that discovers websocket modules from the
  package source, independently of `_TARGET_MODULES`, and asserts every
  discovered module is registered

Verified: the new guard fails on the unpatched tree naming both Speak V2
modules, and passes after the fix. 319 passed, 1 skipped across
tests/custom and tests/utils; `mypy src/` and `mypy tests/typecheck`
clean. WireMock-backed tests under tests/wire require Docker and were
not run locally.
@mayankbohradev
mayankbohradev force-pushed the fix/speak-v2-transport-factory branch from d6d879e to 67b7341 Compare August 13, 2026 15:23
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