Skip to content
11 changes: 8 additions & 3 deletions .fernignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Custom client wrappers (extend generated DeepgramApiClient with Bearer auth, session ID)
# Custom client wrappers (Bearer auth, session ID, and owned HTTP-resource lifecycle)
# Flat paths (local generation strips package-prefix)
src/main/java/DeepgramClient.java
src/main/java/AsyncDeepgramClient.java
Expand All @@ -20,8 +20,8 @@ src/main/java/com/deepgram/core/ClientOptions.java
src/main/java/com/deepgram/core/transport/

# Bug fixes for maxRetries(0) semantics ("connect once, don't retry") and a
# configurable connectionTimeoutMs on ReconnectOptions (was hardcoded 4000ms).
# Pull this back out once the fixes are upstreamed into the Fern generator.
# configurable connectionTimeoutMs on ReconnectOptions (was hardcoded 4000ms), and cancellation
# of in-flight connection attempts. Pull this back out once the fixes are upstreamed into Fern.
src/main/java/com/deepgram/core/ReconnectingWebSocketListener.java

# Forward-compat patch: Fern's generated dispatcher routes any unrecognized message
Expand Down Expand Up @@ -50,6 +50,11 @@ src/main/java/com/deepgram/resources/listen/v2/websocket/V2WebSocketClient.java
src/main/java/com/deepgram/resources/listen/v1/websocket/V1WebSocketClient.java
src/main/java/com/deepgram/resources/speak/v1/websocket/V1WebSocketClient.java

# All streaming WebSocket clients need null guards and terminal disconnect behavior so cleanup is
# safe before or during connect(). The Listen and Speak files are frozen above for other patches;
# Agent V1 needs its own freeze entry until Fern emits the guards.
src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java

# Restores the FLUX_RENEE_EN constant that generator 4.18.0 dropped. The voice is live:
# POST /v2/speak?model=flux-renee-en returns 200 with valid audio, and the name resolves in the
# server's model registry (an invented flux-* name is rejected with INVALID_QUERY_PARAMETER), so the
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ How to identify:

Current permanently frozen files:

- `src/main/java/com/deepgram/DeepgramClient.java`, `src/main/java/com/deepgram/AsyncDeepgramClient.java`, `src/main/java/com/deepgram/DeepgramClientBuilder.java`, `src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java` - custom wrapper entrypoints that add Bearer auth, session ID support, and custom transport behavior on top of Fern's generated API client
- `src/main/java/com/deepgram/DeepgramClient.java`, `src/main/java/com/deepgram/AsyncDeepgramClient.java`, `src/main/java/com/deepgram/DeepgramClientBuilder.java`, `src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java` - custom wrapper entrypoints that add Bearer auth, session ID support, custom transport behavior, and ownership-aware HTTP-resource lifecycle management on top of Fern's generated API client
- `src/main/java/com/deepgram/core/transport/` - hand-written transport abstraction
- `build.gradle`, `settings.gradle`, `gradle/`, `gradlew`, `gradlew.bat`, `pom.xml`, `Makefile` - build and project configuration
- `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `LICENSE`, `docs/` - docs
Expand All @@ -48,13 +48,15 @@ How to identify:
Current temporarily frozen files:

- `src/main/java/com/deepgram/core/ClientOptions.java` - preserves release-please version markers and correct SDK header constants that Fern currently overwrites; use the standard `.bak` swap/restore workflow during regen review. Since generator 4.18.0 Fern emits a `getSdkVersion()` helper reading `Package.getImplementationVersion()` instead of a literal. That *does* resolve in the published artifact (CI publishes via `mvn deploy -P release`, and `pom.xml`'s maven-jar-plugin sets `addDefaultImplementationEntries=true`, so the JAR manifest carries `Implementation-Version`), but it resolves to `null` under Gradle and in tests, where it silently falls back to a hardcoded literal the generator does not keep current. We keep the explicit literals because they are correct in every context and because `.github/release-please-config.json` already lists this file in `extra-files`, so release-please bumps it alongside `pom.xml`, `build.gradle`, and `.fern/metadata.json`. Fern also emits `User-Agent` with a `com.deepgram.` prefix while leaving `X-Fern-SDK-Name` on the `com.deepgram:` Maven-coordinate form; we keep both on the colon form.
- `src/main/java/com/deepgram/core/ReconnectingWebSocketListener.java` - carries bug fixes for `maxRetries(0)` semantics ("connect once, don't retry") and a configurable `connectionTimeoutMs` field (was hardcoded 4000ms), plus an `applyOptionsOverride(...)` hook used by `TransportWebSocketFactory` to apply per-transport reconnect policy; pull this back out once the fixes are upstreamed into the Fern generator. Use the standard `.bak` swap/restore workflow during regen review.
- `src/main/java/com/deepgram/core/ReconnectingWebSocketListener.java` - carries bug fixes for `maxRetries(0)` semantics ("connect once, don't retry"), a configurable `connectionTimeoutMs` field (was hardcoded 4000ms), cancellation of in-flight connection attempts, and an `applyOptionsOverride(...)` hook used by `TransportWebSocketFactory` to apply per-transport reconnect policy; pull this back out once the fixes are upstreamed into the Fern generator. Use the standard `.bak` swap/restore workflow during regen review.
- `src/main/java/com/deepgram/resources/speak/v2/websocket/V2WebSocketClient.java` and `src/main/java/com/deepgram/resources/listen/v2/websocket/V2WebSocketClient.java` - forward-compat patch (both clients). Fern's generated `handleIncomingMessage` dispatcher routes any unrecognized message type to `onError` with "Update your SDK version...", which makes a benign new server control frame look fatal to a deployed client. Patched so the unrecognized-type branch is a no-op — the raw frame is already delivered via `onMessage(String)` earlier in the method, so consumers still see it. Mirrors the JS/Python SDKs' forward-compat behavior and is regression-guarded by `src/test/java/com/deepgram/SpeakV2ForwardCompatTest.java` and `src/test/java/com/deepgram/ListenV2ForwardCompatTest.java`. These two clients also carry the streaming query-param patches described in the next entry. Use the standard `.bak` swap/restore workflow during regen review; re-apply the no-op to both after regen, and unfreeze once the generator stops treating unknown frames as errors.
- `src/main/java/com/deepgram/resources/listen/v1/websocket/V1WebSocketClient.java` and `src/main/java/com/deepgram/resources/speak/v1/websocket/V1WebSocketClient.java` (and the v2 clients above) - streaming query-param patches on the generated `connect()` builders. Two fixes: (1) multi-value serialization — array-valued params (listen: `keyterm`, `keywords`, `replace`, `search`, `tag`, `extra`, `language_hint`; speak: `tag`) were serialized with `String.valueOf(union.get())`, collapsing a `List` into one param (`keyterm=[a, b]`) instead of repeats (`keyterm=a&keyterm=b`); (2) an `additionalProperties` escape hatch — the builder exposes `additionalProperty(key, value)` for unmodeled params (e.g. `no_delay`) but `connect()` never emitted them to the URL. Both patched to route through `QueryStringMapper(arraysAsRepeats=true)`, matching the REST path. Use the standard `.bak` swap/restore workflow during regen review; re-apply after regen and unfreeze once the generator emits array params as repeats and serializes `additionalProperties` on the WS `connect()` path (tracked as an upstream Fern request).
- Fields-less message types carrying a manual `hashCode()` patch (Fern generates `equals()` but no `hashCode()` for these, violating the Object contract): `src/main/java/com/deepgram/resources/listen/v2/types/ListenV2CloseStream.java`, `src/main/java/com/deepgram/resources/listen/v2/types/ListenV2ForceEndTurn.java`, `src/main/java/com/deepgram/resources/speak/v2/types/SpeakV2Close.java`, `src/main/java/com/deepgram/resources/speak/v2/types/SpeakV2Flush.java`, and the `AgentV1*` event types `src/main/java/com/deepgram/resources/agent/v1/types/{AgentV1ListenUpdated,AgentV1SpeakUpdated,AgentV1AgentAudioDone,AgentV1SettingsApplied,AgentV1UserStartedSpeaking,AgentV1KeepAlive,AgentV1ThinkUpdated,AgentV1PromptUpdated,AgentV1ForceEndTurn}.java`. Use the standard `.bak` swap/restore workflow during regen review; drop the patches and unfreeze all of them once the generator emits a matching equals/hashCode pair for fields-less types (tracked as an upstream Fern request).
- `src/main/java/com/deepgram/types/DeepgramModel.java` - restores the `FLUX_RENEE_EN` constant that generator 4.18.0 dropped. The voice is live: `POST /v2/speak?model=flux-renee-en` returns 200 with valid audio, and the name resolves in the server's model registry (an invented `flux-*` name is rejected with `INVALID_QUERY_PARAMETER`), so the removal is a spec regression rather than a retirement, and dropping the constant would break 0.8.0 callers for nothing. Five touchpoints: the constant, the `Value` enum entry, the `visit()` case, the `valueOf()` case, and the `Visitor` method. **This file is unlike the other temporarily frozen ones — it receives frequent additive spec changes (4.18.0 alone added 25 constants), so on the next regen do NOT restore the `.bak` wholesale.** Diff the `.bak` against the newly generated file, carry forward every new voice, and re-apply only the `FLUX_RENEE_EN` touchpoints. Drop the patch and unfreeze once the spec lists the voice again (tracked as an upstream spec request).
- Union default-variant fix on the agent listen-provider unions: `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1UpdateListenListenProvider.java`, `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1SettingsAgentListenProvider.java`, `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1SettingsAgentContextListenProvider.java`. `version` is an optional discriminator, so a provider payload without it is valid (and is what 0.7.x emits), but Fern points `@JsonTypeInfo` `defaultImpl` at the empty-bodied `_UnknownValue`, so such a payload deserializes to an unknown variant carrying `null` — `getProvider()` returns `null` and re-serialization emits `{"provider":null}`, silently dropping the provider on the wire. Patched to `defaultImpl = V2Value` on each; guarded by `src/test/java/com/deepgram/AgentSettingsProviderDefaultTest.java`. Use the standard `.bak` swap/restore workflow during regen review; drop the patches and unfreeze once the generator stops defaulting unions to the empty `_UnknownValue` (tracked as an upstream Fern request).

- `src/main/java/com/deepgram/resources/listen/v1/websocket/V1WebSocketClient.java`, `src/main/java/com/deepgram/resources/listen/v2/websocket/V2WebSocketClient.java`, `src/main/java/com/deepgram/resources/speak/v1/websocket/V1WebSocketClient.java`, `src/main/java/com/deepgram/resources/speak/v2/websocket/V2WebSocketClient.java`, and `src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java` - carry null-guarded, terminal `disconnect()` behavior so error-path cleanup is safe before or during `connect()`. The first four are already frozen for their respective patches; Agent V1 is frozen for this guard. Use the standard `.bak` swap/restore workflow during regen review; unfreeze once Fern emits the guard.

### Prepare repo for regeneration

1. Create a new branch off `main` named `<YOUR_INITIALS>/sdk-gen-<YYYY-MM-DD>` (e.g. `gh/sdk-gen-2026-07-09`). Use your own initials as the prefix.
Expand Down
86 changes: 54 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ DeepgramClient client = DeepgramClient.builder()
.build();
```

### Resource lifecycle

Close an SDK-created client when your application is finished with it. This releases the OkHttp
dispatcher and connection pool, which is especially important for short-lived command-line programs
that use WebSockets.

```java
try (DeepgramClient client = DeepgramClient.builder().build()) {
// Use the client.
}
```

If you provide an `OkHttpClient` through `.httpClient(...)`, you retain ownership and must close its
resources yourself.

WebSocket clients are terminal after `close()` or `disconnect()`. Create a new WebSocket client to
connect again.

## Features

### Speech-to-Text (Listen)
Expand Down Expand Up @@ -240,11 +258,10 @@ import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import okio.ByteString;

DeepgramClient client = DeepgramClient.builder().build();
try (DeepgramClient client = DeepgramClient.builder().build();
V1WebSocketClient ws = client.listen().v1().v1WebSocket()) {
byte[] audioBytes = Files.readAllBytes(Path.of("audio.wav"));

V1WebSocketClient ws = client.listen().v1().v1WebSocket();

// Register event handlers
ws.onResults(results -> {
String transcript = results.getChannel()
Expand Down Expand Up @@ -273,8 +290,7 @@ ws.sendCloseStream(ListenV1CloseStream.builder()
.build())
.get(5, TimeUnit.SECONDS);

// Close when done
ws.close();
}
```

### Text-to-Speech Streaming (Speak WebSocket)
Expand All @@ -294,11 +310,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

DeepgramClient client = DeepgramClient.builder().build();
try (DeepgramClient client = DeepgramClient.builder().build();
V1WebSocketClient ttsWs = client.speak().v1().v1WebSocket()) {
ByteArrayOutputStream audioBuffer = new ByteArrayOutputStream();

V1WebSocketClient ttsWs = client.speak().v1().v1WebSocket();

// Register event handlers
ttsWs.onSpeakV1Audio(audioData -> {
audioBuffer.writeBytes(audioData.toByteArray());
Expand Down Expand Up @@ -331,8 +346,7 @@ ttsWs.sendClose(SpeakV1Close.builder()
.build())
.get(5, TimeUnit.SECONDS);

// Close when done
ttsWs.close();
}
```

### Flux TTS Barge-in (Speak V2 WebSocket)
Expand All @@ -343,14 +357,17 @@ The Speak V2 WebSocket adds Flux TTS barge-in and mid-stream controls. Open the
- **`sendInterrupt(...)`** stops playback (barge-in). Pass a `SpeakV2InterruptPlaybackOffset` with the audio milliseconds played so the `onSpeechInterrupted` event can report `getTextSpoken()` / `getTextRemaining()`. The offset is cumulative from session start, and each interrupt must advance past the previous one.

```java
import com.deepgram.DeepgramClient;
import com.deepgram.resources.speak.v2.types.SpeakV2Configure;
import com.deepgram.resources.speak.v2.types.SpeakV2Interrupt;
import com.deepgram.resources.speak.v2.types.SpeakV2InterruptPlaybackOffset;
import com.deepgram.resources.speak.v2.types.SpeakV2Speak;
import com.deepgram.resources.speak.v2.websocket.V2ConnectOptions;
import com.deepgram.resources.speak.v2.websocket.V2WebSocketClient;
import java.util.concurrent.TimeUnit;

V2WebSocketClient ttsWs = client.speak().v2().v2WebSocket();
try (DeepgramClient client = DeepgramClient.builder().build();
V2WebSocketClient ttsWs = client.speak().v2().v2WebSocket()) {

// Mid-stream configure acknowledgements
ttsWs.onConfigureSuccess(success -> System.out.println("configured: " + success.getApplied()));
Expand All @@ -373,7 +390,7 @@ ttsWs.sendInterrupt(SpeakV2Interrupt.builder()
.playbackOffset(SpeakV2InterruptPlaybackOffset.builder().value(1200).build())
.build());

ttsWs.close();
}
```

See [`examples/speak/StreamingTtsV2.java`](examples/speak/StreamingTtsV2.java) for a complete, runnable barge-in example.
Expand All @@ -397,9 +414,8 @@ import com.deepgram.types.ThinkSettingsV1;
import com.deepgram.types.ThinkSettingsV1Provider;
import java.util.concurrent.TimeUnit;

DeepgramClient client = DeepgramClient.builder().build();

V1WebSocketClient agentWs = client.agent().v1().v1WebSocket();
try (DeepgramClient client = DeepgramClient.builder().build();
V1WebSocketClient agentWs = client.agent().v1().v1WebSocket()) {

// Register event handlers
agentWs.onWelcome(welcome -> {
Expand Down Expand Up @@ -440,8 +456,7 @@ agentWs.onError(error -> {
agentWs.connect().get(10, TimeUnit.SECONDS);
Thread.sleep(5000);

// Close when done
agentWs.close();
}
```

## Custom Transports
Expand All @@ -466,6 +481,7 @@ import com.deepgram.DeepgramClient;
import com.deepgram.sagemaker.SageMakerConfig;
import com.deepgram.sagemaker.SageMakerTransportFactory;
import com.deepgram.resources.listen.v1.websocket.V1ConnectOptions;
import com.deepgram.resources.listen.v1.websocket.V1WebSocketClient;
import com.deepgram.types.ListenV1Model;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -481,17 +497,21 @@ var factory = new SageMakerTransportFactory(
.build()
);

DeepgramClient client = DeepgramClient.builder()
.apiKey("unused") // SageMaker uses AWS credentials, not Deepgram API keys
.transportFactory(factory)
.build();

// Use the SDK exactly as normal — the transport is transparent
var ws = client.listen().v1().v1WebSocket();
ws.onResults(results -> { /* ... */ });
ws.connect(V1ConnectOptions.builder().model(ListenV1Model.NOVA3).build())
.get(10, TimeUnit.SECONDS);
ws.sendMedia(ByteString.of(audioBytes));
try {
try (DeepgramClient client = DeepgramClient.builder()
.apiKey("unused") // SageMaker uses AWS credentials, not Deepgram API keys
.transportFactory(factory)
.build();
V1WebSocketClient ws = client.listen().v1().v1WebSocket()) {
// Use the SDK exactly as normal — the transport is transparent
ws.onResults(results -> { /* ... */ });
ws.connect(V1ConnectOptions.builder().model(ListenV1Model.NOVA3).build())
.get(10, TimeUnit.SECONDS);
ws.sendMedia(ByteString.of(audioBytes));
}
} finally {
factory.shutdown();
}
```

See the [SageMaker example](examples/sagemaker/LiveStreamingSageMaker.java) for a complete walkthrough.
Expand All @@ -509,10 +529,12 @@ DeepgramTransportFactory myFactory = (url, headers) -> {
return new MyCustomTransport(url, headers);
};

DeepgramClient client = DeepgramClient.builder()
.apiKey("your-key")
.transportFactory(myFactory)
.build();
try (DeepgramClient client = DeepgramClient.builder()
.apiKey("your-key")
.transportFactory(myFactory)
.build()) {
// Use the client for streaming. Close each WebSocket before this scope exits.
}
```

The `DeepgramTransport` interface provides bidirectional messaging: `sendText()`, `sendBinary()`, and callback registration for incoming messages, errors, and close events.
Expand Down
6 changes: 5 additions & 1 deletion examples/agent/CustomProviders.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ public static void main(String[] args) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
wsClient.disconnect();
try {
wsClient.disconnect();
} finally {
client.close();
}
}
}
}
6 changes: 5 additions & 1 deletion examples/agent/InjectMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ public static void main(String[] args) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
wsClient.disconnect();
try {
wsClient.disconnect();
} finally {
client.close();
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion examples/agent/VoiceAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ public static void main(String[] args) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
wsClient.disconnect();
try {
wsClient.disconnect();
} finally {
client.close();
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion examples/listen/ForceEndTurn.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ public static void main(String[] args) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
wsClient.disconnect();
try {
wsClient.disconnect();
} finally {
client.close();
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion examples/listen/LiveStreaming.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ public static void main(String[] args) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
wsClient.disconnect();
try {
wsClient.disconnect();
} finally {
client.close();
}
}
}
}
Loading
Loading