Skip to content

feat(core): expose response metadata and socket payload types - #826

Merged
GregHolmes merged 10 commits into
mainfrom
gh/sdk-gen-2026-11-04
Nov 10, 2026
Merged

GregHolmes merged 10 commits into
mainfrom
gh/sdk-gen-2026-11-04

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Regenerate the Python SDK with fernapi/fern-python-sdk 5.39.3.

  • Unknown undiscriminated-union values now remain raw payloads instead of becoming None. Agent V1, Listen V1/V2, and Speak V2 response unions type those fallback values as dictionaries; Listen V1/V2 also correctly type their existing binary-frame paths. This supersedes the unmerged workaround in fix(listen): preserve unknown v1 and v2 websocket responses #804.
  • Speak V1 still coerces unknown frames into SpeakV1Flushed because of a Fern undiscriminated-union defect.
  • Responses returned through with_raw_response() now expose the underlying httpx.Response as .response.
  • httpx.RemoteProtocolError no longer retries by default because the server may already have applied the request. For safely replayable operations, opt in with request_options={"retry_remote_protocol_errors": True}.
  • Added regression coverage for unmatched literal-discriminated union payloads, typed socket fallbacks, binary Listen frames, and default/opt-in protocol retry behavior.
  • Reapplied required compatibility, transport, credential-redaction, query-encoding, package-export, and wire-coverage patches.
  • Removed the pydantic_utilities.py mypy workaround because Fern now handles its TypeAdapter diagnostic.
  • Preserved explicit api_key=None behavior in the custom client after Fern moved environment lookup into BaseClient.
  • Restored .gitignore coverage-artifact rules and removed all transient .bak files.

Upstream Follow-up

  • fern-api/fern#18824 tracks native dictionary fallbacks for Python WebSocket response unions and the Speak V1 undiscriminated-union coercion defect.

Closes

Validation

  • poetry run pytest -rP . - 1147 passed, 4 skipped
  • poetry run ruff check src tests/wire tests/custom/test_http_retry_coverage.py tests/typecheck/compat_aliases.py
  • poetry run mypy src/ tests/typecheck
  • Live raw-response probe against manage.v1.projects.list() - HTTP 200

poetry run ruff check . continues to report existing violations in manual examples and tests/custom/test_transport.py, outside this regeneration.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
src.deepgram 97% 94% 0 ✔
src.deepgram.agent 100% 100% 0 ✔
src.deepgram.agent.v1 98% 100% 0 ✔
src.deepgram.agent.v1.settings 100% 100% 0 ✔
src.deepgram.agent.v1.settings.think 100% 100% 0 ✔
src.deepgram.agent.v1.settings.think.models 97% 100% 0 ✔
src.deepgram.auth 100% 100% 0 ✔
src.deepgram.auth.v1 100% 100% 0 ✔
src.deepgram.auth.v1.tokens 97% 100% 0 ✔
src.deepgram.core 89% 84% 0 ➖
src.deepgram.errors 100% 100% 0 ✔
src.deepgram.helpers 98% 92% 0 ✔
src.deepgram.listen 100% 100% 0 ✔
src.deepgram.listen.v1 98% 93% 0 ✔
src.deepgram.listen.v1.media 97% 100% 0 ✔
src.deepgram.listen.v2 98% 93% 0 ✔
src.deepgram.manage 100% 100% 0 ✔
src.deepgram.manage.v1 100% 100% 0 ✔
src.deepgram.manage.v1.models 96% 100% 0 ✔
src.deepgram.manage.v1.projects 97% 100% 0 ✔
src.deepgram.manage.v1.projects.billing 100% 100% 0 ✔
src.deepgram.manage.v1.projects.billing.balances 96% 100% 0 ✔
src.deepgram.manage.v1.projects.billing.breakdown 97% 100% 0 ✔
src.deepgram.manage.v1.projects.billing.fields 97% 100% 0 ✔
src.deepgram.manage.v1.projects.billing.purchases 97% 100% 0 ✔
src.deepgram.manage.v1.projects.keys 96% 100% 0 ✔
src.deepgram.manage.v1.projects.members 97% 100% 0 ✔
src.deepgram.manage.v1.projects.members.invites 96% 100% 0 ✔
src.deepgram.manage.v1.projects.members.scopes 96% 100% 0 ✔
src.deepgram.manage.v1.projects.models 96% 100% 0 ✔
src.deepgram.manage.v1.projects.usage 98% 100% 0 ✔
src.deepgram.manage.v1.projects.usage.breakdown 97% 100% 0 ✔
src.deepgram.manage.v1.projects.usage.fields 97% 100% 0 ✔
src.deepgram.read 100% 100% 0 ✔
src.deepgram.read.v1 100% 100% 0 ✔
src.deepgram.read.v1.text 98% 100% 0 ✔
src.deepgram.self_hosted 100% 100% 0 ✔
src.deepgram.self_hosted.v1 100% 100% 0 ✔
src.deepgram.self_hosted.v1.distribution_credentials 96% 100% 0 ✔
src.deepgram.speak 100% 100% 0 ✔
src.deepgram.speak.v1 98% 97% 0 ✔
src.deepgram.speak.v1.audio 91% 80% 0 ✔
src.deepgram.speak.v2 98% 93% 0 ✔
src.deepgram.speak.v2.audio 100% 100% 0 ✔
src.deepgram.voice_agent 100% 100% 0 ✔
src.deepgram.voice_agent.configurations 95% 100% 0 ✔
src.deepgram.voice_agent.variables 95% 100% 0 ✔
Summary 96% (6613 / 6910) 92% (1457 / 1578) 0 ✔

Scope: hand-maintained SDK logic. Fern-generated data models (types/, requests/), package __init__.py files, version.py, and the unused core/http_sse/ scaffolding are excluded — see .coveragerc. Unscoped whole-package coverage is ~70%.

@GregHolmes GregHolmes changed the title chore: SDK regeneration 2026-11-04 feat(regen): update fern-python-sdk to 5.39.3 Nov 4, 2026
@GregHolmes GregHolmes changed the title feat(regen): update fern-python-sdk to 5.39.3 fix(listen): preserve unknown websocket response payloads Nov 4, 2026
@GregHolmes GregHolmes changed the title fix(listen): preserve unknown websocket response payloads feat(core): expose raw responses, retain unknown payloads, and make protocol retries opt-in Nov 4, 2026
@GregHolmes GregHolmes changed the title feat(core): expose raw responses, retain unknown payloads, and make protocol retries opt-in feat(regen): update fern-python-sdk to 5.39.3 Nov 4, 2026

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reran this in Docker (python:3.12) at 1e8c95b. Mostly good. Requesting changes for S2, S3 and N1. S1 is a Fern issue so no fix needed here, N2/N3 are FYI.

What I checked

  • Do unknown server frames come back as raw dicts, not None? Yes for listen v1/v2, speak v2, agent v1 (sync iter, async iter, recv(), start_listening). I ran a local ws server sending {"type":"BrandNewServerEvent"}, {"foo":"bar"}, {"type":"Results"}. No None, no crash.
  • Tests actually ran? Yes. 1147 passed / 4 skipped with wiremock up (main is 1138/4). Heads up: pytest dies at startup with no docker in the container, so I ran wiremock as a sibling container and set WIREMOCK_URL. mypy src/ tests/typecheck is clean.
  • Greg's hand patch (typing.Dict[str, Any] added to the 5 socket_client.py unions): it is type-only, runtime already works from Fern's own unchecked_base_model change. It survives regens because those 5 files are already in .fernignore. Correct for the 4 sockets above.
  • Version: stays 7.17.0, matches the released tag. Good.

To fix (S2, S3, N1) / Fern issue (S1) / FYI (N2, N3)
[S1] speak.v1 doesn't give a dict. An unknown frame there comes back as SpeakV1Flushed (type="BrandNewServerEvent"), and {"foo":"bar"} too. Same on main, so not new, but the PR body says unknown handling is restored and the new Dict member on that union is never reached. This one is a Fern issue (undiscriminated-union casting in the generator), not something to fix in this PR. Just raise it with Fern alongside the dict-member ask, and don't block on it.
[S2] No runtime test for the socket fallback. The new coverage is a typecheck file plus 4 lines in test_core_utilities_coverage.py. Please add one test per socket client that feeds an unknown frame and asserts isinstance(msg, dict). That would have caught S1.
[S3] RemoteProtocolError no longer retries by default (retry_remote_protocol_errors opt-in). Reasonable, but it's a behavior change for anyone relying on the old retry, and the title is feat(regen) so release-please writes it up as a plain minor. Can we get an explicit line in the release notes?
[N1] AGENTS.md: the socket_client.py entries say "same" and don't mention the new Dict member. Add one line, and add it to the list of things to raise with Fern (generator should emit the dict member itself so the freeze can shrink).
[N2] Typed users may see new mypy errors: the sockets' iter/aiter are now annotated (were unannotated), and the union is bytes | models | dict. Anyone doing m.type in a loop needs an isinstance. Worth a release note line too.
[N3] http_client.py retry recursion no longer passes optional_body (Greg's patch was dropped, correctly, since the file isn't frozen). Retried optional-body calls fall back to the default. I couldn't find a case where it changes the wire, just flagging it's generator behavior now.

Re S3: I'd keep it a minor with a release note rather than feat!.

@GregHolmes GregHolmes changed the title feat(regen): update fern-python-sdk to 5.39.3 fix(core): preserve unknown WebSocket messages Nov 9, 2026
@GregHolmes

Copy link
Copy Markdown
Contributor Author

@dg-coreylweathers Addressed the review items:

  • Added runtime fallback coverage for every socket client, with Speak V1 retained as a strict expected failure for the upstream Fern defect.
  • Added bytes to the Listen V1/V2 response unions and sync/async binary-frame coverage so their annotations match the existing runtime paths.
  • Updated AGENTS.md and .fernignore for the dictionary and binary-union patches.
  • Filed fern-api/fern#18824 for native dictionary fallback generation and the Speak V1 coercion bug; it is now linked in the PR description.
  • Clarified the retry and socket-typing behavior in the PR summary. The explicit customer-facing release note will go in the generated Release Please PR, which is the changelog source of truth.

Focused tests, mypy, and the Python 3.10-3.13 CI matrix pass.

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on a few release-notes items. The code itself looks good: I ran the unknown-frame handling against a local WebSocket server and mypy, ruff and pytest pass in Docker (3.12).

Retry default is a behavior change shipping as a patch (S4). RemoteProtocolError no longer retries by default. The commits are now fix(...) with no breaking marker, so Release Please would propose 7.17.1. Please make it a minor (or mark it breaking) so it doesn't land in a patch.

The release note isn't in the repo yet (S3). The note for the retry change is planned for the Release Please PR, which doesn't exist yet, so nothing in the repo records it. Please add the line here, for example: "RemoteProtocolError no longer retries by default; opt in with request_options={'retry_remote_protocol_errors': True}".

The title says more than the PR delivers (S5). Speak V1 still turns unknown frames into SpeakV1Flushed (the Fern bug in fern#18824, already disclosed and pinned by the xfail test). Please word the title so it doesn't promise unknown-message preservation for Speak V1.

Typed users may see new mypy errors (N2). The new union members (including bytes on Listen v1/v2) can surface new errors in typed callers. It's in AGENTS.md but users won't see that, so please add a short user-facing release note.

Everything else I checked held up.

@GregHolmes GregHolmes changed the title fix(core): preserve unknown WebSocket messages feat(core): expose response metadata and socket payload types Nov 9, 2026
@GregHolmes

GregHolmes commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

@dg-coreylweathers Follow-up on the review items:

  • S1 is tracked upstream in Python WebSocket response unions should preserve unknown frames as dictionaries fern-api/fern#18824; Speak V1 remains a strict expected failure in the regression test until Fern preserves unknown frames as dictionaries.
  • S2 is covered by tests/custom/test_socket_unknown_fallback.py, which exercises unknown-frame fallback for each socket client and binary Listen frames.
  • N1 is reflected in AGENTS.md and .fernignore, including the dictionary-union patch and the Fern follow-up.
  • The PR is titled feat(core): expose response metadata and socket payload types, so this ships as a minor release rather than a patch. The title intentionally does not promise raw unknown-frame preservation for Speak V1.
  • We intentionally do not update CHANGELOG.md in feature/regeneration PRs. Release Please is the changelog source of truth and will generate the customer-facing release entry for the retry opt-in and expanded socket response typing.

CI remains green across Python 3.10-3.13.

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Greg's replies cover the asks. The head is the same 6ae6912 I already ran, and in Docker (3.12) the socket fallback, retry and core coverage tests pass, mypy is clean and ruff is clean. The Speak V1 case is an xfail pinned to fern#18824, as agreed.

  • S2: tests/custom/test_socket_unknown_fallback.py feeds unknown frames to each socket client and covers binary Listen frames.
  • S4 and S5: the title is feat(core), so release-please proposes a minor, and it no longer promises unknown-frame preservation for Speak V1.
  • S3 and N2: the retry opt-in line and the typing change are in the PR body, which becomes the squash body. I'm fine leaving CHANGELOG.md to Release Please as long as that body line makes it into the squashed commit.

Not tested here: the wiremock wire suite, which needs a sibling container. CI shows green on 3.10 to 3.13.

@GregHolmes
GregHolmes merged commit 3b3679c into main Nov 10, 2026
13 checks passed
@GregHolmes
GregHolmes deleted the gh/sdk-gen-2026-11-04 branch November 10, 2026 16:41
GregHolmes added a commit that referenced this pull request Nov 12, 2026
🤖 I have created a release *beep* *boop*
---


##
[7.18.0](v7.17.0...v7.18.0)
(2026-11-10)


### Features

* **core:** Preserve unknown Agent V1, Listen V1/V2, and Speak V2
WebSocket messages as raw dictionary payloads instead of dropping them;
Listen V1/V2 binary frames are now typed. Responses from
`with_raw_response()` expose the underlying `httpx.Response` as
`.response`. To avoid replaying a request the server may already have
applied, `httpx.RemoteProtocolError` is no longer retried by default;
opt in for safe requests with
`request_options={"retry_remote_protocol_errors": True}`.
([#826](#826))
([3b3679c](3b3679c))

## Highlights

- Unknown Agent V1, Listen V1/V2, and Speak V2 WebSocket frames now
reach applications as raw dictionary payloads when the SDK does not
recognize their variant.
- `with_raw_response()` wrappers now expose the underlying
`httpx.Response` through `.response`.
- `httpx.RemoteProtocolError` retries are opt-in through
`request_options={"retry_remote_protocol_errors": True}` to prevent
accidental replay of a request that the server may have already applied.

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Greg Holmes <greg.holmes@deepgram.com>
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.

AsyncV2SocketClient.__aiter__ silently yields None for Warning messages (SDK 7.8.1)

2 participants