streamChatCompletion(@Nonnull final String prompt)
.map(OpenAiChatCompletionDelta::getDeltaContent);
}
+ /**
+ * Creates realtime channel allowing to input text and voice it (receive audio output)
+ *
+ * The input channel should be used with a try-with-resources block to ensure that the underlying
+ * connection is closed.
+ *
+ *
Example:
+ *
+ *
{@code
+ * try (var textInputChannel = client.textToSpeech(audioOutputConsumer)) {
+ * textInputChannel.sendText("...");
+ * ....
+ * }
+ * }
+ *
+ * This API implements full duplex (input + output) communication channels. Application should logically
+ * synchronize their state and close input channel when it is appropriate (e.g. last part of the response
+ * has been received via output channel and application does not need to send any other input). When input
+ * channel is closed, output channel will be closed automatically and output consumer will not be called
+ * anymore.
+ *
+ * @param audioOutputConsumer - audio consumer of raw PCM mono 24000 Hz little endian output
+ * @return input channel, allowing for text input
+ */
+ @Nonnull
+ public TextInputChannel textToSpeech(@Nonnull final AudioOutputChannel audioOutputConsumer) {
+ var extraHeaders = destination.asHttp().getHeaders();
+ var headers = new HashMap(extraHeaders.size() + 1);
+ for (Header header : extraHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ var endpoint = getRealtimeEndpoint();
+
+ return new TextToSpeechRealtimeClient(endpoint, headers, audioOutputConsumer);
+ }
+
+ public AudioInputChannel speechToText(@Nonnull final TextOutputChannel textOutputConsumer) {
+ var extraHeaders = destination.asHttp().getHeaders();
+ var headers = new HashMap(extraHeaders.size() + 1);
+ for (Header header : extraHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ var endpoint = getRealtimeEndpoint() + "?intent=transcription";
+
+ return new SpeechToTextRealtimeClient(endpoint, headers, textOutputConsumer);
+ }
+
+ public AudioInputChannel speechToSpeech(@Nonnull final AudioOutputChannel audioOutputChannel) {
+ var extraHeaders = destination.asHttp().getHeaders();
+ var headers = new HashMap(extraHeaders.size() + 1);
+ for (Header header : extraHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ var endpoint = getRealtimeEndpoint();
+
+ return new SpeechToSpeechRealtimeClient(endpoint, headers, audioOutputChannel);
+ }
+
+ private String getRealtimeEndpoint() {
+ var sb = new StringBuilder(PATH_BUFFER_SIZE);
+ sb.append("wss://");
+ var pathParts = destination.asHttp().getUri().toString().split("//");
+ if (pathParts.length != 2) {
+ throw new IllegalArgumentException("Invalid destination URI: " + destination.asHttp().getUri());
+ }
+ sb.append(pathParts[1].replaceFirst("^api\\.", "realtime."));
+ sb.append("v1/realtime");
+ return sb.toString();
+ }
+
private static void throwOnContentFilter(@Nonnull final OpenAiChatCompletionDelta delta) {
final String finishReason = delta.getFinishReason();
if (finishReason != null && finishReason.equals("content_filter")) {
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiModel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiModel.java
index 1926f56ce..62c435bcc 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiModel.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiModel.java
@@ -121,7 +121,7 @@ public record OpenAiModel(@Nonnull String name, @Nullable String version) implem
/** Azure OpenAI GPT-5-nano model */
public static final OpenAiModel GPT_5_NANO = new OpenAiModel("gpt-5-nano", null);
- /** Azure OpenAI GPT-5-nano model */
+ /** Azure OpenAI GPT-realtime model */
public static final OpenAiModel GPT_REALTIME = new OpenAiModel("gpt-realtime", null);
/** Azure OpenAI GPT-5.2 model */
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToSpeechRealtimeClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToSpeechRealtimeClient.java
new file mode 100644
index 000000000..afbefea13
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToSpeechRealtimeClient.java
@@ -0,0 +1,109 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.openai.models.realtime.*;
+import com.openai.models.realtime.clientsecrets.ClientSecretCreateParams;
+import java.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SpeechToSpeechRealtimeClient extends WSSOpenAIRealtimeClient implements AudioInputChannel {
+
+ private static final int MAX_DATA_CHUNK_SIZE_BYTES = 8192;
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static final Set HANDLED_RESPONSE_TYPES = Set.of(
+ "response.output_audio.delta", "response.output_audio.done"
+ );
+
+ private static final List OUTPUT_MODALITIES = List.of(
+ RealtimeSessionCreateRequest.OutputModality.AUDIO
+ );
+
+ private static final String TASK = "";
+
+
+ private final AudioOutputChannel outputConsumer;
+
+ public SpeechToSpeechRealtimeClient(String url, Map httpHeaders, AudioOutputChannel outputConsumer) {
+ super(url, httpHeaders, HANDLED_RESPONSE_TYPES);
+ this.outputConsumer = outputConsumer;
+ }
+
+ public void inputAudio(byte[] rawAudioChunk) {
+ if (rawAudioChunk.length == 0) {
+ return;
+ }
+ var cursorLeft = 0;
+ while (cursorLeft < rawAudioChunk.length) {
+ var cursorRight = Math.min(cursorLeft + MAX_DATA_CHUNK_SIZE_BYTES, rawAudioChunk.length);
+ var part = Arrays.copyOfRange(rawAudioChunk, cursorLeft, cursorRight);
+ var audioInputMessage = InputAudioBufferAppendEvent.builder()
+ .audio(Base64.getEncoder().encodeToString(part)).build();
+ super.sendMessage(audioInputMessage);
+ cursorLeft += MAX_DATA_CHUNK_SIZE_BYTES;
+ }
+
+ // TODO: make turn detection configurable and uncomment for explicit turn control
+ // var commitAudioMessage = InputAudioBufferCommitEvent.builder().build();
+ // super.sendMessage(commitAudioMessage);
+ // askForResponse();
+ }
+
+ @Override
+ protected String getSystemPrompt() {
+ return TASK;
+ }
+
+ @Override
+ protected void onResponse(String eventType, JsonNode event) {
+ if ("response.output_audio.delta".equals(eventType)) {
+ var base64Audio = event.get("delta").asText();
+ byte[] audio = Base64.getDecoder().decode(base64Audio);
+ this.outputConsumer.outputAudio(audio, Boolean.FALSE);
+ } else if ("response.output_audio.done".equals(eventType)) {
+ this.outputConsumer.outputAudio(EMPTY_BYTE_ARRAY, Boolean.TRUE);
+ }
+ }
+
+ @Override
+ protected SessionUpdateEvent sessionConfiguration() {
+ SessionUpdateEvent sessionUpdateEvent =
+ SessionUpdateEvent.builder()
+ .session(
+ ClientSecretCreateParams.Session.ofRealtime(
+ RealtimeSessionCreateRequest.builder()
+ .outputModalities(OUTPUT_MODALITIES)
+ .audio(
+ RealtimeAudioConfig.builder()
+ .input(
+ RealtimeAudioConfigInput.builder()
+ .turnDetection(
+ RealtimeAudioInputTurnDetection.ofSemanticVad(RealtimeAudioInputTurnDetection.SemanticVad.builder().build()))
+ .format(
+ RealtimeAudioFormats.AudioPcm.builder()
+ .type(
+ RealtimeAudioFormats.AudioPcm.Type
+ .AUDIO_PCM)
+ .rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
+ .build())
+ .build())
+ .output(
+ RealtimeAudioConfigOutput.builder()
+ .format(
+ RealtimeAudioFormats.AudioPcm.builder()
+ .type(
+ RealtimeAudioFormats.AudioPcm.Type
+ .AUDIO_PCM)
+ .rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
+ .build())
+ .voice(
+ RealtimeAudioConfigOutput.Voice.UnionMember1.ALLOY)
+ .build())
+ .build())
+ .build())
+ .asRealtime())
+ .build();
+ return sessionUpdateEvent;
+ }
+}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToTextRealtimeClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToTextRealtimeClient.java
new file mode 100644
index 000000000..6e56866fb
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/SpeechToTextRealtimeClient.java
@@ -0,0 +1,93 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.openai.models.realtime.*;
+import java.util.*;
+
+import com.openai.models.realtime.clientsecrets.ClientSecretCreateParams;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SpeechToTextRealtimeClient extends WSSOpenAIRealtimeClient implements AudioInputChannel {
+
+ private static final int MAX_DATA_CHUNK_SIZE_BYTES = 8192;
+ private static final Set HANDLED_RESPONSE_TYPES = Set.of(
+ "response.output_audio_transcript.done"
+ );
+
+ private static final String TASK =
+ "you are a transcript writer and your role is to convert input speech to text as precise as possible (word to word exactly, if possible)";
+
+
+ private final TextOutputChannel outputConsumer;
+
+ public SpeechToTextRealtimeClient(String url, Map httpHeaders, TextOutputChannel outputConsumer) {
+ super(url, httpHeaders, HANDLED_RESPONSE_TYPES);
+ this.outputConsumer = outputConsumer;
+ }
+
+ public void inputAudio(byte[] rawAudioChunk){
+ if (rawAudioChunk.length == 0) {
+ return;
+ }
+ var cursorLeft = 0;
+ while (cursorLeft < rawAudioChunk.length) {
+ var cursorRight = Math.min(cursorLeft + MAX_DATA_CHUNK_SIZE_BYTES, rawAudioChunk.length);
+ var part = Arrays.copyOfRange(rawAudioChunk, cursorLeft, cursorRight);
+ var audioInputMessage = InputAudioBufferAppendEvent.builder()
+ .audio(Base64.getEncoder().encodeToString(part)).build();
+ super.sendMessage(audioInputMessage);
+ cursorLeft += MAX_DATA_CHUNK_SIZE_BYTES;
+ }
+
+ var commitAudioMessage = InputAudioBufferCommitEvent.builder().build();
+ super.sendMessage(commitAudioMessage);
+ askForResponse();
+ }
+
+ @Override
+ protected String getSystemPrompt() {
+ return TASK;
+ }
+
+ @Override
+ protected void onResponse(String eventType, JsonNode event) {
+ if ("response.output_audio_transcript.done".equals(eventType)) {
+ this.outputConsumer
+ .outputText(event.has("transcript") ? event.get("transcript").asText() : "", Boolean.TRUE);
+ } else {
+ log.trace("skipping message type: {}", eventType);
+ }
+ }
+
+ @Override
+ protected SessionUpdateEvent sessionConfiguration() {
+ SessionUpdateEvent sessionUpdateEvent =
+ SessionUpdateEvent.builder()
+ .session(
+ ClientSecretCreateParams.Session.ofTranscription(
+ RealtimeTranscriptionSessionCreateRequest.builder()
+ .audio(
+ RealtimeTranscriptionSessionAudio.builder()
+ .input(
+ RealtimeTranscriptionSessionAudioInput.builder()
+ .turnDetection(Optional.empty())
+ .format(
+ RealtimeAudioFormats.AudioPcm.builder()
+ .type(
+ RealtimeAudioFormats.AudioPcm.Type
+ .AUDIO_PCM)
+ .rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
+ .build())
+ .transcription(
+ AudioTranscription.builder()
+ .model(AudioTranscription.Model.GPT_4O_TRANSCRIBE)
+ .build())
+ .build())
+ .build())
+ .build())
+ .asTranscription())
+ .build();
+ return sessionUpdateEvent;
+ }
+}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextInputChannel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextInputChannel.java
new file mode 100644
index 000000000..e1cfe32db
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextInputChannel.java
@@ -0,0 +1,5 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+public interface TextInputChannel extends AutoCloseable {
+ void sendText(String text);
+}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextOutputChannel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextOutputChannel.java
new file mode 100644
index 000000000..1c0425bb4
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextOutputChannel.java
@@ -0,0 +1,20 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+/**
+ * Functional interface representing test output channel (text data consumer)
+ *
+ * Should be closed by application (try-with-resources) when not needed anymore
+ */
+public interface TextOutputChannel {
+
+ /**
+ * This method is sequentially invoked by test data provider to supply implementer (consumer)
+ * with the test data.
+ *
+ * @param textChunk chunk of the text (possibly partial content)
+ * @param isLast true if this call logically concludes previous and this passed text data into a
+ * single logical entity (e.g. gets called at the end when all parts of a single
+ * message get passed)
+ */
+ void outputText(CharSequence textChunk, boolean isLast);
+}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextToSpeechRealtimeClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextToSpeechRealtimeClient.java
new file mode 100644
index 000000000..5e42e5ac4
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/TextToSpeechRealtimeClient.java
@@ -0,0 +1,100 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.openai.models.realtime.*;
+import com.openai.models.realtime.clientsecrets.ClientSecretCreateParams;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.*;
+
+@Slf4j
+public class TextToSpeechRealtimeClient extends WSSOpenAIRealtimeClient implements TextInputChannel {
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static final Set HANDLED_RESPONSE_TYPES = Set.of(
+ "response.output_audio.delta", "response.output_audio.done"
+ );
+
+ private static final List OUTPUT_MODALITIES = List.of(
+ RealtimeSessionCreateRequest.OutputModality.AUDIO
+ );
+
+ private static final String TASK =
+ "you are a speaker and your role is to read (produce audio) of the user input speech. voice user text input";
+
+
+ private final AudioOutputChannel outputConsumer;
+
+ public TextToSpeechRealtimeClient(String url, Map httpHeaders, AudioOutputChannel outputConsumer) {
+ super(url, httpHeaders, HANDLED_RESPONSE_TYPES);
+ this.outputConsumer = outputConsumer;
+ }
+
+ public void sendText(String text) {
+ var message = ConversationItemCreateEvent.builder()
+ .item(ConversationItem.ofRealtimeConversationItemUserMessage(
+ RealtimeConversationItemUserMessage.builder()
+ .addContent(RealtimeConversationItemUserMessage.Content.builder()
+ .text(text)
+ .type(RealtimeConversationItemUserMessage.Content.Type.INPUT_TEXT).build()).build()
+ )).build();
+
+ super.sendMessage(message);
+ askForResponse();
+ }
+
+ @Override
+ protected String getSystemPrompt() {
+ return TASK;
+ }
+
+ @Override
+ protected void onResponse(String eventType, JsonNode event) {
+ if ("response.output_audio.delta".equals(eventType)) {
+ var base64Audio = event.get("delta").asText();
+ byte[] audio = Base64.getDecoder().decode(base64Audio);
+ this.outputConsumer.outputAudio(audio, Boolean.FALSE);
+ } else if ("response.output_audio.done".equals(eventType)) {
+ this.outputConsumer.outputAudio(EMPTY_BYTE_ARRAY, Boolean.TRUE);
+ } else {
+ log.warn("skipping message type: {}", eventType);
+ }
+ }
+
+ @Override
+ protected SessionUpdateEvent sessionConfiguration() {
+ SessionUpdateEvent sessionUpdateEvent = SessionUpdateEvent.builder()
+ .session(
+ ClientSecretCreateParams.Session.ofRealtime(
+ RealtimeSessionCreateRequest.builder()
+ .outputModalities(OUTPUT_MODALITIES)
+ .audio(
+ RealtimeAudioConfig.builder()
+ .input(
+ RealtimeAudioConfigInput.builder()
+ .turnDetection(Optional.empty())
+ .format(
+ RealtimeAudioFormats.AudioPcm
+ .builder()
+ .type(RealtimeAudioFormats.AudioPcm.Type.AUDIO_PCM)
+ .rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
+ .build())
+ .build())
+ .output(
+ RealtimeAudioConfigOutput.builder()
+ .format(RealtimeAudioFormats.AudioPcm
+ .builder()
+ .type(RealtimeAudioFormats.AudioPcm.Type.AUDIO_PCM)
+ .rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
+ .build())
+ .voice(
+ RealtimeAudioConfigOutput.Voice.UnionMember1.ALLOY)
+ .build())
+ .build())
+ .build())
+ .asRealtime())
+ .build();
+ return sessionUpdateEvent;
+ }
+}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/WSSOpenAIRealtimeClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/WSSOpenAIRealtimeClient.java
new file mode 100644
index 000000000..77e2d881b
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/WSSOpenAIRealtimeClient.java
@@ -0,0 +1,179 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.models.realtime.*;
+import com.openai.models.realtime.clientsecrets.ClientSecretCreateParams;
+import com.sap.ai.sdk.core.common.ClientException;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.WebSocket;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+public abstract class WSSOpenAIRealtimeClient implements AutoCloseable {
+
+ private static final int SUCCESS_FINISH_WSS_CODE = 1000;
+ private static final ObjectMapper JACKSON = new ObjectMapper()
+ .setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);
+
+ /**
+ * WebSocket keeps TCP connection to the server. If connection idles, depending on the provider
+ * in cloud environments, cloud provider sends hard TCP RST (reset) after 60-300 seconds of inactivity,
+ * this is normally not configurable and would leave realtime conversation unusable on a pause.
+ *
+ * In mobile networks this period is even shorter and typical CGNAT session can only idle for 10-15 seconds
+ * before it gets terminated
+ *
+ * In realtime client we rely on explicit lifetime management (create/close), heartbeat mechanism is required
+ * to prevent unintended closing of connections while conversation is still expected to continue
+ */
+ private static final int HEARTBEAT_INTERVAL_MILLIS = 4500;
+ private static final String HEARTBEAT_TIMER_NAME = "wss_realtime_heartbeat";
+
+ private final HttpClient client;
+ private final CompletableFuture ws;
+ private final Timer heartbeatTimer;
+ private final Set handleMessageTypes;
+
+ public WSSOpenAIRealtimeClient(String url, Map httpHeaders, Set handleMessageTypes) {
+ this.client = HttpClient.newHttpClient();
+ var wsBuilder = this.client.newWebSocketBuilder();
+ for (Map.Entry entry : httpHeaders.entrySet()) {
+ wsBuilder = wsBuilder.header(entry.getKey(), entry.getValue());
+ }
+ this.ws = wsBuilder.buildAsync(URI.create(url), new BufferedWebSocketListener(this::onSocketOpen, this::onText));
+ this.handleMessageTypes = handleMessageTypes;
+ this.heartbeatTimer = new Timer(HEARTBEAT_TIMER_NAME,true);
+ }
+
+ public void askForResponse() {
+ WebSocket ws = this.ws.join();
+ synchronized (this) {
+ try {
+ ws.sendText(JACKSON.writeValueAsString(ResponseCreateEvent.builder().build()), true);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to serialize ask for response", e);
+ }
+ ws.request(1);
+ }
+ }
+
+ @Override
+ public void close() {
+ WebSocket ws = this.ws.join();
+ synchronized (this) {
+ heartbeatTimer.cancel();
+ try {
+ ws.sendClose(SUCCESS_FINISH_WSS_CODE, "done").join();
+ } catch (Exception e) {
+ log.error("Error while closing WebSocket", e);
+ }
+ // this.client.close(); // exists only since java 21
+ }
+ }
+
+ protected abstract String getSystemPrompt();
+
+ protected abstract void onResponse(String eventType, JsonNode event);
+
+ protected abstract SessionUpdateEvent sessionConfiguration();
+
+ protected void sendMessage(Object message) {
+ WebSocket ws = this.ws.join();
+ synchronized (this) {
+ try {
+ ws.sendText(JACKSON.writeValueAsString(message), true);
+ ws.request(1);
+ } catch (JsonProcessingException e) {
+ throw new ClientException("Failed to serialize message", e);
+ }
+
+ }
+ }
+
+ private synchronized void onSocketOpen(WebSocket ws) {
+ configureSession(ws);
+ configureConversation(ws);
+ scheduleHeartbeat(ws);
+ }
+
+ private void onText(WebSocket webSocket, CharSequence data) {
+ final JsonNode event;
+ try {
+ event = JACKSON.readTree(data.toString());
+ } catch (JsonProcessingException e) {
+ throw new ClientException("Error parsing JSON response from speech API", e);
+ }
+ var eventType = event.get("type").asText();
+ if (handleMessageTypes.contains(eventType)) {
+ onResponse(eventType, event);
+ } else {
+ log.trace("Unhandled event type: {}", eventType);
+ }
+
+ webSocket.request(1);
+ }
+
+ private synchronized void sendPing(WebSocket ws) {
+ if (ws.isInputClosed()) {
+ return;
+ }
+ ws.sendPing(ByteBuffer.wrap("ping".getBytes())).join();
+ ws.request(1);
+ }
+
+ private void configureSession(WebSocket ws) {
+ SessionUpdateEvent sue = sessionConfiguration();
+ try {
+ ws.sendText(JACKSON.writeValueAsString(sue), true);
+ } catch (JsonProcessingException e) {
+ throw new ClientException("Failed to serialize session request", e);
+ }
+
+ ws.request(1);
+ }
+
+ private void configureConversation(WebSocket ws) {
+ var systemPrompt = getSystemPrompt();
+ if (systemPrompt == null || systemPrompt.isEmpty()) {
+ return;
+ }
+ var systemConversationItem = ConversationItemCreateEvent.builder()
+ .item(ConversationItem.ofRealtimeConversationItemSystemMessage(
+ RealtimeConversationItemSystemMessage.builder()
+ .addContent(RealtimeConversationItemSystemMessage.Content.builder()
+ .text(systemPrompt)
+ .type(RealtimeConversationItemSystemMessage.Content.Type.INPUT_TEXT).build()).build()
+ )).build();
+
+ try {
+ var json = JACKSON.writeValueAsString(systemConversationItem);
+ ws.sendText(json, true);
+ } catch (JsonProcessingException e) {
+ throw new ClientException("Failed to serialize message", e);
+ }
+ ws.request(1);
+ }
+
+ private void scheduleHeartbeat(WebSocket ws) {
+ TimerTask task = new TimerTask(){
+ @Override
+ public void run() {
+ sendPing(ws);
+ }
+
+ };
+ heartbeatTimer.scheduleAtFixedRate(task, 0, HEARTBEAT_INTERVAL_MILLIS);
+ }
+}
diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
index 06b6beb08..1c6cdf7b3 100644
--- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
+++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
@@ -330,6 +330,9 @@ public class OrchestrationAiModel {
/** Azure OpenAI GPT-5-mini model */
public static final OrchestrationAiModel GPT_5_MINI = new OrchestrationAiModel("gpt-5-mini");
+ /** Azure OpenAI GPT-realtime model */
+ public static final OrchestrationAiModel GPT_REALTIME = new OrchestrationAiModel("gpt-realtime");
+
/** Azure OpenAI GPT-5-nano model */
public static final OrchestrationAiModel GPT_5_NANO = new OrchestrationAiModel("gpt-5-nano");
diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java
index 73c7161e0..78f347b56 100644
--- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java
+++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java
@@ -18,9 +18,8 @@
import com.sap.cloud.sdk.cloudplatform.connectivity.Header;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import io.vavr.control.Try;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+
+import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
diff --git a/pom.xml b/pom.xml
index 6cf8692d1..03781bc31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,7 @@
2.1.3
3.5.6
1.1.8
+ 4.41.0
3.8.6
3.2.0
5.23.0
@@ -117,6 +118,11 @@
pom
import