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
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"enable-wire-tests": true,
"runtime-version": true
},
"originGitCommit": "e252995bcdb25f3e12d46ae342a2b93d0c1085f9",
"originGitCommit": "9dcbd3d5e8d36420319e1b33988613ad2cb2be10",
"originGitCommitIsDirty": true,
"invokedBy": "manual",
"sdkVersion": "0.11.0"
Expand Down
5 changes: 5 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ src/main/java/com/deepgram/core/ClientOptions.java
# Transport abstraction (pluggable transport for SageMaker, etc.)
src/main/java/com/deepgram/core/transport/

# Hand-written exception that carries a Listen V1 server Error to the generic onError handler
# (thrown by the frozen listen v1 V1WebSocketClient below when no onErrorMessage handler is set).
# No Fern equivalent; the generator would delete it.
src/main/java/com/deepgram/resources/listen/v1/websocket/ListenV1ErrorException.java

# 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.
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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/core/transport/` - hand-written transport abstraction
- `src/main/java/com/deepgram/resources/listen/v1/websocket/ListenV1ErrorException.java` - hand-written exception carrying a Listen V1 server `Error` (`ListenV1Error`) to the generic `onError` handler when no `onErrorMessage` handler is registered; thrown by the frozen listen v1 `V1WebSocketClient`. No Fern equivalent
- `build.gradle`, `settings.gradle`, `gradle/`, `gradlew`, `gradlew.bat`, `pom.xml`, `Makefile` - build and project configuration
- `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `LICENSE`, `docs/` - docs
- `src/test/` - manually maintained tests
Expand All @@ -47,10 +48,10 @@ 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. Fern 4.22.1 tracks and closes child WebSockets before shutting down SDK-owned OkHttp resources; the permanent custom client wrappers inherit it, so do not add wrapper-local lifecycle code and retain Fern's lifecycle implementation during reconciliation.
- `src/main/java/com/deepgram/core/ClientOptions.java` - preserves release-please version markers, correct SDK header constants, and the accurate `close()` lifecycle Javadoc 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. Fern 4.22.1 tracks and closes child WebSockets before shutting down SDK-owned OkHttp resources; the permanent custom client wrappers inherit it, so do not add wrapper-local lifecycle code and retain Fern's lifecycle implementation during reconciliation.
- `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/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).
- `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. Listen V1 also forwards a parsed server `Error` to `onError` as a `ListenV1ErrorException` (carrying the typed `ListenV1Error`) when no `onErrorMessage` handler is registered, preserving the prior generic error-handler behavior. 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).
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,14 @@ Stream audio for real-time speech-to-text.
import com.deepgram.DeepgramClient;
import com.deepgram.resources.listen.v1.types.ListenV1CloseStream;
import com.deepgram.resources.listen.v1.types.ListenV1CloseStreamType;
import com.deepgram.resources.listen.v1.types.ListenV1Configure;
import com.deepgram.resources.listen.v1.websocket.V1WebSocketClient;
import com.deepgram.resources.listen.v1.websocket.V1ConnectOptions;
import com.deepgram.types.ListenV1Model;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okio.ByteString;

Expand All @@ -288,12 +291,25 @@ ws.onError(error -> {
System.err.println("Error: " + error.getMessage());
});

ws.onErrorMessage(error -> {
System.err.println("Server error: " + error.getVariant() + ": " + error.getDescription());
});

// Connect with options (model is required)
ws.connect(V1ConnectOptions.builder()
.model(ListenV1Model.NOVA3)
.build())
.get(10, TimeUnit.SECONDS);

// Update Nova-3 keyterms and numerals without reconnecting. Keep the keyterm list under the
// 500-token limit: an over-limit update currently stops transcription without an Error, and
// the server closes the stream.
ws.sendConfigure(ListenV1Configure.builder()
.keyterms(List.of("Deepgram"))
.features(Map.of("numerals", true))
.build())
.get(5, TimeUnit.SECONDS);

ws.sendMedia(ByteString.of(audioBytes));
ws.sendCloseStream(ListenV1CloseStream.builder()
.type(ListenV1CloseStreamType.CLOSE_STREAM)
Expand Down
86 changes: 86 additions & 0 deletions examples/listen/LiveReconfigure.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import com.deepgram.DeepgramClient;
import com.deepgram.resources.listen.v1.types.ListenV1CloseStream;
import com.deepgram.resources.listen.v1.types.ListenV1CloseStreamType;
import com.deepgram.resources.listen.v1.types.ListenV1Configure;
import com.deepgram.resources.listen.v1.types.ListenV1Error;
import com.deepgram.resources.listen.v1.types.ListenV1ResultsChannelAlternativesItem;
import com.deepgram.resources.listen.v1.websocket.V1ConnectOptions;
import com.deepgram.resources.listen.v1.websocket.V1WebSocketClient;
import com.deepgram.types.ListenV1Model;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
* Reconfigures an active Nova-3 Listen V1 stream without reconnecting.
*
* <p>Keep the keyterm list under the 500-token limit: an over-limit update currently stops transcription without an
* {@code Error}, and the server closes the stream.
*
* <p>Usage: {@code DEEPGRAM_API_KEY=... java LiveReconfigure}
*/
public class LiveReconfigure {
public static void main(String[] args) {
String apiKey = System.getenv("DEEPGRAM_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("DEEPGRAM_API_KEY environment variable is required");
System.exit(1);
}

DeepgramClient client = DeepgramClient.builder().apiKey(apiKey).build();
V1WebSocketClient wsClient = client.listen().v1().v1WebSocket();
CountDownLatch closeLatch = new CountDownLatch(1);

try {
wsClient.onConnected(() -> System.out.println("Connected to Deepgram"));
wsClient.onResults(result -> {
if (result.getChannel() != null
&& result.getChannel().getAlternatives() != null
&& !result.getChannel().getAlternatives().isEmpty()) {
ListenV1ResultsChannelAlternativesItem alternative =
result.getChannel().getAlternatives().get(0);
if (alternative.getTranscript() != null
&& !alternative.getTranscript().isEmpty()) {
System.out.println(alternative.getTranscript());
}
}
});
wsClient.onErrorMessage(LiveReconfigure::printListenError);
wsClient.onError(error -> System.err.println("WebSocket error occurred: " + error.getMessage()));
wsClient.onDisconnected(reason -> {
System.out.println("Connection closed.");
closeLatch.countDown();
});

wsClient.connect(V1ConnectOptions.builder()
.model(ListenV1Model.NOVA3)
.build())
.get(10, TimeUnit.SECONDS);

wsClient.sendConfigure(ListenV1Configure.builder()
.keyterms(List.of("Deepgram", "Nova-3"))
.features(Map.of("numerals", true))
.build())
.get(10, TimeUnit.SECONDS);
System.out.println("Sent live reconfiguration for keyterms and numerals.");

// Stream audio after this point, for example: wsClient.sendMedia(audioChunk);
wsClient.sendCloseStream(ListenV1CloseStream.builder()
.type(ListenV1CloseStreamType.CLOSE_STREAM)
.build())
.get(10, TimeUnit.SECONDS);
closeLatch.await(15, TimeUnit.SECONDS);
} catch (Exception e) {
System.err.println("Unable to run the live reconfiguration example: " + e.getMessage());
} finally {
wsClient.close();
client.close();
}
}

private static void printListenError(ListenV1Error error) {
System.err.println("Listen error (" + error.getVariant() + "): " + error.getDescription());
error.getCode().ifPresent(code -> System.err.println("Error code: " + code));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
* Provides production-ready resilience for WebSocket connections.
*/
public abstract class ReconnectingWebSocketListener extends WebSocketListener {
// A single volatile reference keeps an override internally consistent.
private volatile ReconnectOptions activeOptions;

private final int maxEnqueuedMessages;
Expand Down Expand Up @@ -129,7 +128,8 @@ public void connect() {
} catch (TimeoutException e) {
connectionFuture.cancel(true);
TimeoutException timeoutError =
new TimeoutException("WebSocket connection timeout after " + options.connectionTimeoutMs + " milliseconds"
new TimeoutException("WebSocket connection timeout after " + options.connectionTimeoutMs
+ " milliseconds"
+ (retryCount.get() > 0
? " (retry attempt #" + retryCount.get() + ")"
: " (initial connection attempt)"));
Expand Down
Loading
Loading