Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .fern/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"cliVersion": "5.95.1",
"generatorName": "fernapi/fern-python-sdk",
"generatorVersion": "5.27.1",
"generatorVersion": "5.39.3",
"generatorConfig": {
"client": {
"class_name": "BaseClient",
Expand All @@ -21,7 +21,7 @@
"skip_validation": true
}
},
"originGitCommit": "596e7bdfa377f40f3301b1d394c9f0215af5f6d4",
"originGitCommit": "330b1dab41a5004329de2c1ea6396dc4b1ea930c",
"originGitCommitIsDirty": true,
"invokedBy": "manual",
"sdkVersion": "7.17.0"
Expand Down
9 changes: 6 additions & 3 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ src/deepgram/agent/v1/requests/agent_v1conversation_text.py
# - agent/v1 send_raw: public raw-control sender for protocol-transparent bridges
# - listen/v2 send_configure: runtime tolerance for a raw dict alongside the
# generated ListenV2Configure model
# - response unions include typing.Dict[str, typing.Any] for unknown server frames;
# tracked upstream at https://github.com/fern-api/fern/issues/18824
# - listen response unions include bytes because their iterators and recv methods
# already return binary frames
# [temporarily frozen — manual patches listed above]
src/deepgram/agent/v1/socket_client.py
src/deepgram/listen/v1/socket_client.py
Expand Down Expand Up @@ -218,9 +222,7 @@ src/deepgram/listen/v2/types/__init__.py
src/deepgram/core/api_error.py
src/deepgram/core/parse_error.py

# Pydantic 2.14+ requires explicit generic annotations for these adapters or
# mypy rejects the generated module with var-annotated errors.
src/deepgram/core/pydantic_utilities.py
# Pydantic 2.14+ adapter annotations are now generated natively.

src/deepgram/core/query_encoder.py

Expand Down Expand Up @@ -279,6 +281,7 @@ tests/custom/test_query_encoder.py
tests/custom/test_regen_response_fields.py
tests/custom/test_secure_logging.py
tests/custom/test_socket_client_shims.py
tests/custom/test_socket_unknown_fallback.py
tests/custom/test_speak_v2_connect_wire.py
tests/custom/test_speak_v2_interrupt_configure.py
tests/custom/test_speak_v2_coverage.py
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Current permanently frozen files:
- `tests/custom/test_query_encoder.py` — hand-written regression test that `core/query_encoder.py` coerces Python bools to lowercase `"true"`/`"false"` before `urlencode` so websocket query strings stay wire-correct
- `tests/custom/test_regen_response_fields.py` — hand-written regression test guarding response fields added by recent regenerations: `ListenV1Response/ResultsMetadata.diarize_info` (`model_uuid`, `arch`), per-word `speaker` / `speaker_confidence`, Nova-3 multilingual `languages` / `languages_hinted` / word and utterance `language`, and `ListenV2TurnInfo.trigger`. Same rationale as `test_latency_report_stt_compat.py`: `UncheckedBaseModel` + `skip_validation` makes response parsing lenient, so a future spec removal would silently turn these into `None` rather than failing. Fixtures are real captured payloads; the `trigger` cases pin the parsing contract for the values the server sends (`model` / `manual` / `timeout`) plus the absent case, which is what a deployment without ForceEndTurn enabled (self-hosted without the feature flag) returns. Live coverage of the same field lives in `tests/manual/listen/v2/force_end_turn/`
- `tests/custom/test_secure_logging.py` — hand-written regression test that the `websockets` Authorization-header DEBUG logs are redacted (API key never logged in clear text)
- `tests/custom/test_socket_unknown_fallback.py` — hand-written regression coverage that every generated socket client returns an unknown server frame as a dictionary; Speak V1 remains a strict expected failure until Fern fixes the undiscriminated-union coercion tracked above
- `tests/custom/test_speak_v2_interrupt_configure.py` — hand-written coverage for the Speak V2 barge-in / mid-stream reconfigure surface (`send_interrupt`, `send_configure`, `SpeechInterrupted`, `ConfigureSuccess`/`ConfigureFailure`, and the `speed`/`expressivity` connect params)
- `tests/custom/test_voice_agent_force_end_turn_example.py` — hand-written regression coverage that the Voice Agent ForceEndTurn example waits for a rejection queued after `AgentAudioDone`
- `tests/custom/test_text_builder.py`, `tests/custom/test_transport.py` — hand-written tests
Expand All @@ -71,11 +72,11 @@ Current temporarily frozen files:
- `src/deepgram/listen/v1/socket_client.py` — same
- `src/deepgram/listen/v2/socket_client.py` — same (broad except, optional `send_close_stream` default). As of the 2026-08-11 regen the generator properly types `send_configure(ListenV2Configure)` and puts `ListenV2ConfigureSuccess` in the response Union, so those are taken from the generator; the only `send_configure` patch retained is runtime tolerance for a raw dict (sent verbatim) for back-compat with pre-typed-model callers
- `src/deepgram/agent/v1/socket_client.py` — same + `_sanitize_numeric_types` and public `send_raw()` for protocol-transparent control-frame bridges
- All five `socket_client.py` files also add `typing.Dict[str, typing.Any]` to their response unions, matching Fern's raw-dictionary fallback for unknown server frames. The Listen V1/V2 response unions additionally include `bytes`, matching their existing iterator and `recv()` paths. Fern should emit the dictionary member natively and preserve unknown Speak V1 frames as dictionaries rather than coercing them into `SpeakV1Flushed`; track both fixes at [fern-api/fern#18824](https://github.com/fern-api/fern/issues/18824).
- `src/deepgram/types/speak_settings_v1provider.py`, `src/deepgram/types/deepgram.py` — validate Agent TTS `expressivity` as `pydantic.StrictInt` so Pydantic v1 rejects fractional values instead of truncating them before they reach the API. Regression coverage in `tests/custom/test_socket_client_shims.py`. Unfreeze when Pydantic v1 support is dropped.
- `src/deepgram/agent/v1/types/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent.py`, `src/deepgram/agent/v1/types/agent_v1settings.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent.py`, `src/deepgram/agent/v1/requests/agent_v1settings.py` — backward-compat patches for the 2026-05-05 Agent Settings schema restructure. These preserve callable `AgentV1SettingsAgent(...)`, keep `AgentV1Settings.agent` accepting both that wrapper and `agent_id` strings, restore the legacy request TypedDict shapes, remap legacy `messages=[...]` / nested `context=AgentV1SettingsAgentContext(messages=[...])` usage into the new `context={"messages": [...]}` wire shape, and keep read-side `obj.messages` access working.
- `src/deepgram/core/api_error.py`, `src/deepgram/core/parse_error.py` — credential redaction. Every websocket `connect()` path raises `ApiError(headers=dict(headers), ...)` with the full request headers, and both error types stringify that dict, so an unredacted `Authorization` reached `str(e)`, tracebacks, log aggregators and error trackers (which serialise attributes as well as the message). Both now mask credential values at construction via `_secure_logging.redact_sensitive_headers`, preserving non-sensitive headers (`dg-request-id`) for debugging. This is the same threat `_secure_logging.py` covers for the `websockets` DEBUG handshake logs, via the other path to it. Regression coverage in `tests/custom/test_api_error_redaction.py`. Unfreeze if the generator starts redacting credentials itself.
- `src/deepgram/core/query_encoder.py` — coerces Python bools to lowercase `"true"`/`"false"` before they reach `urllib.parse.urlencode` (which would otherwise produce `"True"`/`"False"` via `str()` and break websocket query strings). Only the four `*/connect()` paths call `urlencode`; HTTP raw clients hand params to httpx, which lowercases bools itself, so the patch is a no-op for the HTTP path. Once Fern's websocket codegen normalizes bools (or the spec types these as `boolean` end-to-end), this can be unfrozen.
- `src/deepgram/core/pydantic_utilities.py` — Pydantic 2.14+ makes mypy reject Fern's unannotated module-level `TypeAdapter` globals. Retain explicit `TypeAdapter[datetime]` / `TypeAdapter[date]` annotations until Fern emits them.
- `src/deepgram/listen/v2/types/listen_v2connected.py`, `src/deepgram/listen/v2/types/listen_v2turn_info.py`, `src/deepgram/listen/v2/types/listen_v2turn_info_words_item.py`, `src/deepgram/listen/v2/types/listen_v2configure_success.py`, `src/deepgram/listen/v2/types/listen_v2configure_success_thresholds.py`, `src/deepgram/listen/v2/types/listen_v2configure_failure.py`, `src/deepgram/listen/v2/types/listen_v2fatal_error.py`, `src/deepgram/listen/v2/types/listen_v2warning.py` — read-side compatibility for the SDK 7.7 Listen V2 response retype. Through 7.6, `V2SocketClientResponse` contained `typing.Any`, so every response was returned as a raw dict; fixing the union made responses typed models and broke callers using `response["field"]`. These generated response classes inherit the hand-written base above, preserving read-only wire-key subscript access alongside canonical attribute access. Restore direct `UncheckedBaseModel` inheritance and unfreeze these files in the next major release.
- `src/deepgram/types/deepgram_listen_provider_v2.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider.py` — behavioural back-compat shim for the `language_hint` -> `language_hints` rename (2026-06-15 regen). The public field was historically (incorrectly) singular and accepted a str or a list; the API field is `language_hints` (a list, and the server uses `deny_unknown_fields` so the singular key is rejected on the wire). Each carries a hand-added `model_validator(mode='before')` / `root_validator(pre=True)` that remaps a legacy `language_hint=` kwarg and drops the dead singular key. Remove and unfreeze the language alias when it is retired in a future major.
- `src/deepgram/types/shared_topics.py`, `src/deepgram/types/shared_intents.py`, `src/deepgram/requests/shared_topics.py`, `src/deepgram/requests/shared_intents.py`, and the generated root/type/request barrels — direct Topics and Intents segments plus their generated exports remain Fern-owned, but carry the v7 deprecated `results` construction facade and Params shape for the hand-written wrappers above. The models normalize legacy `results.{topics,intents}.segments` input to direct `segments` while preserving direct fields and deprecated read facades. Freeze these generated files during the parent reconciliation; retain all direct generated segments and exports alongside the compatibility surface.
Expand Down
28 changes: 14 additions & 14 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[project]
name = "deepgram-sdk"
dynamic = ["version"]
license = "MIT"
license-files = ["LICENSE"]

[tool.poetry]
name = "deepgram-sdk"
Expand All @@ -26,8 +28,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
"License :: OSI Approved :: MIT License"
"Typing :: Typed"
]
packages = [
{ include = "deepgram", from = "src"}
Expand Down
1 change: 0 additions & 1 deletion src/deepgram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,7 +1375,6 @@ def __dir__():
"voice_agent",
]


__all__.extend(
[
"SharedIntentsResults",
Expand Down
1 change: 0 additions & 1 deletion src/deepgram/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,6 @@ def __dir__():
"v1",
]


__all__.extend(
[
"AgentV1HistoryContent",
Expand Down
1 change: 0 additions & 1 deletion src/deepgram/agent/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,6 @@ def __dir__():
"settings",
]


__all__.extend(
[
"AgentV1HistoryContent",
Expand Down
1 change: 0 additions & 1 deletion src/deepgram/agent/v1/requests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ def __dir__():
"FunctionCallHistoryMessageParams",
]


__all__.extend(
[
"AgentV1HistoryContentParams",
Expand Down
10 changes: 5 additions & 5 deletions src/deepgram/agent/v1/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
except ImportError:
from websockets import WebSocketClientProtocol # type: ignore


def _sanitize_numeric_types(obj: typing.Any) -> typing.Any:
"""Convert whole-number floats to integers for wire-compatible JSON."""
if isinstance(obj, dict):
Expand Down Expand Up @@ -85,6 +84,7 @@ def _sanitize_numeric_types(obj: typing.Any) -> typing.Any:
AgentV1Warning,
AgentV1History,
bytes,
typing.Dict[str, typing.Any],
]


Expand All @@ -93,10 +93,10 @@ def __init__(self, *, websocket: WebSocketClientProtocol):
super().__init__()
self._websocket = websocket

async def __aiter__(self):
async def __aiter__(self) -> typing.AsyncIterator[V1SocketClientResponse]:
async for message in self._websocket:
if isinstance(message, bytes):
yield message
yield message # type: ignore
else:
try:
yield construct_type(type_=V1SocketClientResponse, object_=json.loads(message)) # type: ignore
Expand Down Expand Up @@ -272,10 +272,10 @@ def __init__(self, *, websocket: websockets_sync_connection.Connection):
super().__init__()
self._websocket = websocket

def __iter__(self):
def __iter__(self) -> typing.Iterator[V1SocketClientResponse]:
for message in self._websocket:
if isinstance(message, bytes):
yield message
yield message # type: ignore
else:
try:
yield construct_type(type_=V1SocketClientResponse, object_=json.loads(message)) # type: ignore
Expand Down
1 change: 0 additions & 1 deletion src/deepgram/agent/v1/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ def __dir__():
"FunctionCallHistoryMessageFunctionCallsItem",
]


__all__.extend(
[
"AgentV1HistoryContent",
Expand Down
6 changes: 4 additions & 2 deletions src/deepgram/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(
self,
*,
environment: DeepgramClientEnvironment = DeepgramClientEnvironment.PRODUCTION,
api_key: typing.Optional[str] = os.getenv("DEEPGRAM_API_KEY"),
api_key: typing.Optional[str] = None,
headers: typing.Optional[typing.Dict[str, str]] = None,
timeout: typing.Optional[float] = None,
max_retries: typing.Optional[int] = None,
Expand All @@ -87,6 +87,7 @@ def __init__(
):
_defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None
_defaulted_max_retries = max_retries if max_retries is not None else 2
api_key = api_key if api_key is not None else os.getenv("DEEPGRAM_API_KEY")
if api_key is None:
raise ApiError(
body="The client must be instantiated be either passing in api_key or setting DEEPGRAM_API_KEY"
Expand Down Expand Up @@ -251,7 +252,7 @@ def __init__(
self,
*,
environment: DeepgramClientEnvironment = DeepgramClientEnvironment.PRODUCTION,
api_key: typing.Optional[str] = os.getenv("DEEPGRAM_API_KEY"),
api_key: typing.Optional[str] = None,
headers: typing.Optional[typing.Dict[str, str]] = None,
timeout: typing.Optional[float] = None,
max_retries: typing.Optional[int] = None,
Expand All @@ -263,6 +264,7 @@ def __init__(
):
_defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None
_defaulted_max_retries = max_retries if max_retries is not None else 2
api_key = api_key if api_key is not None else os.getenv("DEEPGRAM_API_KEY")
if api_key is None:
raise ApiError(
body="The client must be instantiated be either passing in api_key or setting DEEPGRAM_API_KEY"
Expand Down
Loading
Loading