Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
6f303a3
add realtime text to speech client, WIP
vladimir-a-sap Jul 7, 2026
ecd953e
polish minimap text-to-speech realtime poc
vladimir-a-sap Jul 7, 2026
71d4a4c
add incomplete interim packets buffering and glueing
vladimir-a-sap Jul 7, 2026
232725d
remove obsolete debug constants
vladimir-a-sap Jul 7, 2026
6b872d4
Formatting
bot-sdk-js Jul 7, 2026
54b06a5
add speech to speech realtime support and text to speech example
vladimir-a-sap Jul 10, 2026
df2098a
merge changes from upstream
vladimir-a-sap Jul 10, 2026
80a5d3e
add speech to speech demo scenario
vladimir-a-sap Jul 10, 2026
d6ad031
add smoke test for realtime api and configurability
vladimir-a-sap Jul 13, 2026
2ed77e0
rework sample application so it no longer use deprecated browser micr…
vladimir-a-sap Jul 13, 2026
c212c1e
make realtime implementation package local
vladimir-a-sap Jul 13, 2026
d144a3a
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 13, 2026
03d1e70
Formatting
bot-sdk-js Jul 13, 2026
5415795
address pr feedback, add tests
vladimir-a-sap Jul 17, 2026
a0cd474
Formatting
bot-sdk-js Jul 17, 2026
25d673b
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 17, 2026
8dc8ae6
remove duplicated dependency
vladimir-a-sap Jul 17, 2026
4240910
improve code style
vladimir-a-sap Jul 17, 2026
0603218
improve code style
vladimir-a-sap Jul 17, 2026
fcebe1e
Formatting
bot-sdk-js Jul 17, 2026
9a6d251
extract common realtime api functionality to a superclass
vladimir-a-sap Jul 17, 2026
de69739
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 17, 2026
0f2cd62
Formatting
bot-sdk-js Jul 17, 2026
6082105
fix misleading javadocs
vladimir-a-sap Jul 17, 2026
0c2038d
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 17, 2026
537203d
Formatting
bot-sdk-js Jul 17, 2026
7a5220e
replace debug output in test with actual assertions
vladimir-a-sap Jul 17, 2026
e7d844d
fix annotations from invalid package
vladimir-a-sap Jul 20, 2026
7c6dced
Formatting
bot-sdk-js Jul 20, 2026
eb8a451
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 20, 2026
671f960
add missing javadoc comments
vladimir-a-sap Jul 20, 2026
625f72a
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 20, 2026
b4ba471
Formatting
bot-sdk-js Jul 20, 2026
85120c1
add missing javadocs
vladimir-a-sap Jul 23, 2026
fa4bb64
Formatting
bot-sdk-js Jul 23, 2026
e4e04b3
fix style issues
vladimir-a-sap Jul 23, 2026
473a3e2
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 23, 2026
94a0eef
fix dependency issue
vladimir-a-sap Jul 23, 2026
5fbe0da
fix dependency issue
vladimir-a-sap Jul 23, 2026
50392d6
fix dependency issue
vladimir-a-sap Jul 23, 2026
86cd068
fix dependency issue
vladimir-a-sap Jul 23, 2026
e95a5df
fix dependency issue
vladimir-a-sap Jul 23, 2026
ac27bb7
fix dependency issue
vladimir-a-sap Jul 23, 2026
e38a42a
add sample app docs and hints
vladimir-a-sap Jul 23, 2026
4b789d3
improve dependency declarations
vladimir-a-sap Jul 23, 2026
c4f597b
improve code and docs style
vladimir-a-sap Jul 23, 2026
611c0eb
Formatting
bot-sdk-js Jul 23, 2026
b90ab43
use open-ai-core sub dependency as nothing from open-ai dep is explic…
vladimir-a-sap Jul 24, 2026
4ad44ff
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
dc53160
address code style pmd issues
vladimir-a-sap Jul 24, 2026
c6a0e94
Formatting
bot-sdk-js Jul 24, 2026
1400efc
address code style pmd issues
vladimir-a-sap Jul 24, 2026
86ab775
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
51b35a0
Formatting
bot-sdk-js Jul 24, 2026
f92d327
reduce default response buffer size
vladimir-a-sap Jul 24, 2026
a0e5eef
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
ff3007b
move realtime api classed to a dedicated package
vladimir-a-sap Jul 24, 2026
2060668
Formatting
bot-sdk-js Jul 24, 2026
1e74df5
fix pmd issues
vladimir-a-sap Jul 24, 2026
fc6bd75
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
0fb2646
address spotbugs plugin issues
vladimir-a-sap Jul 24, 2026
54b151d
Formatting
bot-sdk-js Jul 24, 2026
f6698c5
address dependency issue
vladimir-a-sap Jul 24, 2026
f36c8c7
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
40971d4
fix checkstyle issues
vladimir-a-sap Jul 24, 2026
7f656aa
Formatting
bot-sdk-js Jul 24, 2026
7eed889
fix checkstyle issues
vladimir-a-sap Jul 24, 2026
f7def36
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
a3d4641
Formatting
bot-sdk-js Jul 24, 2026
bd8cb90
fix checkstyle issues
vladimir-a-sap Jul 24, 2026
4d7cf2c
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
7e008bb
Formatting
bot-sdk-js Jul 24, 2026
b36ed95
fix checkstyle issues
vladimir-a-sap Jul 24, 2026
143c01f
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 24, 2026
0d54fe7
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 27, 2026
7f23870
fix test coverage issues
vladimir-a-sap Jul 27, 2026
07ea836
address pr feedback, fix linter issues
vladimir-a-sap Jul 27, 2026
69bbd72
remove unit test which is alredy duplicated by an integration test
vladimir-a-sap Jul 27, 2026
6f93d30
revert version change to workaround ci caching issue
vladimir-a-sap Jul 27, 2026
c81910b
add unit tests, fix test coverage issue
vladimir-a-sap Jul 28, 2026
c3f5799
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 28, 2026
d9d482d
Apply suggestions from code review
vladimir-a-sap Jul 31, 2026
7044a10
Formatting
bot-sdk-js Jul 31, 2026
4e4f357
polish code style
vladimir-a-sap Jul 31, 2026
04641d5
Merge branch 'feat/poc-openai-realtime-api' of github.com:SAP/ai-sdk-…
vladimir-a-sap Jul 31, 2026
87bba81
polish code style
vladimir-a-sap Jul 31, 2026
1069caf
Merge branch 'main' into feat/poc-openai-realtime-api
vladimir-a-sap Jul 31, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.sap.ai.sdk.foundationmodels.openai;
Comment thread
vladimir-a-sap marked this conversation as resolved.

import com.google.common.annotations.Beta;

/**
* Functional interface representing audio input channel (audio data consumer)
*
* <p>Should be closed by application (try-with-resources) when not needed anymore
*/
@Beta
public interface AudioInputChannel extends AutoCloseable {

/**
* This method is sequentially invoked by audio data provider to supply implementer (consumer)
* with the audio data. Exact audio format (encoding, sampling rate, etc.) depends on the usage
* context
*
* @param rawBytesChunk binary data in the depending on the use case format
*/
@Beta
void inputAudio(byte[] rawBytesChunk);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;

/** Functional interface representing audio output channel (audio data consumer) */
@Beta
public interface AudioOutputChannel {

/**
* This method is sequentially invoked by audio data provider to supply implementer (consumer)
* with the audio data. Exact audio format (encoding, sampling rate, etc.) depends on the usage
* context
*
* @param rawBytesChunk binary data in the depending on the use case format
* @param isLast true if this call logically concludes previous and this passed bytes data into a
* single logical entity (e.g. gets called at the end when all byte parts of a single message
* get passed)
*/
@Beta
void outputAudio(byte[] rawBytesChunk, boolean isLast);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage.OpenAiChatUserMessage;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters;
import com.sap.ai.sdk.foundationmodels.openai.realtime.OpenAiRealtimeClient;
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
Expand Down Expand Up @@ -76,6 +77,18 @@ public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel)
return client.withApiVersion(DEFAULT_API_VERSION);
}

/**
* Creates and configures OpenAI Realtime API client
*
* @return created client
*/
@Beta
@Nonnull
public static OpenAiRealtimeClient realtimeClient() {
final var withResolvedDestination = OpenAiClient.forModel(OpenAiModel.GPT_REALTIME);
return new OpenAiRealtimeClient(withResolvedDestination.destination);
}

/**
* Create a new OpenAI client targeting the specified API version.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;
import javax.annotation.Nonnull;

/**
* Allows to input (send) text to the open channel, must be closed when not needed anymore (e.g.
* try-with-resources)
*/
@Beta
public interface TextInputChannel extends AutoCloseable {

/**
* Sends input text
*
* @param text text to send
*/
@Beta
void sendText(@Nonnull final String text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.sap.ai.sdk.foundationmodels.openai.realtime;

import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;

@Slf4j
class BufferedWebSocketListener implements WebSocket.Listener {

private final Consumer<WebSocket> onOpen;
private final BiConsumer<WebSocket, CharSequence> onText;
private final StringBuilder buffer;

BufferedWebSocketListener(
@Nonnull final Consumer<WebSocket> onOpen,
@Nonnull final BiConsumer<WebSocket, CharSequence> onText) {
this.onOpen = onOpen;
this.onText = onText;
this.buffer = new StringBuilder(256 * 1024);
}

@Override
public void onOpen(@Nonnull final WebSocket webSocket) {
this.onOpen.accept(webSocket);
webSocket.request(1);
}

@Override
@Nonnull
public CompletionStage<?> onText(
final @Nonnull WebSocket webSocket, final @Nonnull CharSequence data, final boolean isLast) {
buffer.append(data);
webSocket.request(1);
if (isLast) {
final var completeMessage = buffer.toString();
buffer.setLength(0);
this.onText.accept(webSocket, completeMessage);
}

return CompletableFuture.completedStage(null);
}

@Override
public void onError(@Nonnull final WebSocket webSocket, @Nonnull final Throwable error) {
log.error("Websocket error occurred during realtime communication", error);
}

@Override
@Nonnull
public CompletionStage<?> onBinary(
@Nonnull final WebSocket webSocket, @Nonnull final ByteBuffer data, final boolean isLast) {
log.warn("Received unexpected binary bytes for WebSocket connection");
return CompletableFuture.completedStage(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.sap.ai.sdk.foundationmodels.openai.realtime;

import com.google.common.annotations.Beta;
import com.sap.ai.sdk.foundationmodels.openai.AudioInputChannel;
import com.sap.ai.sdk.foundationmodels.openai.AudioOutputChannel;
import com.sap.ai.sdk.foundationmodels.openai.TextInputChannel;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.Header;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;

/**
* OpenAI client implementation of Realtime API. Abstracts technical implementation, transport and
* threading and exposes business-level operations (high level interface)
*/
@Beta
public class OpenAiRealtimeClient {
Comment thread
vladimir-a-sap marked this conversation as resolved.

static final int PATH_BUFFER_SIZE =
400; // existing URLs are ~120 symbols long, 400 has reasonable margin

final Destination destination;

/**
* Created OpenAI Realtime client for a specific destination
*
* @param destination - destination to use
*/
@Beta
public OpenAiRealtimeClient(@Nonnull final Destination destination) {
this.destination = destination;
}

/**
* Creates a realtime channel allowing to input text and voice it (receive audio output)
*
* <p>The input channel should be used with a try-with-resources block to ensure that the
* underlying connection is closed.
*
* <p>Example:
*
* <pre>{@code
* try (var textInputChannel = client.textToSpeech(audioOutputConsumer)) {
* textInputChannel.sendText("...");
* ....
* }
* }</pre>
*
* This API implements full duplex (input + output) communication channels. Application should
* logically synchronize their state and close the input channel when it is appropriate (e.g. the
* last part of the response has been received via the output channel and the application does not
* need to send any other input). When the input channel is closed, the output channel will be
* closed automatically and the output consumer will not be called anymore.
*
* @param audioOutputConsumer - audio consumer of raw PCM mono 24000 Hz little endian output, 16
* bit depth
* @param params - allows for various additional features (e.g. voice configuration or
* conversation turn recognition options)
* @return input channel, allowing for text input
*/
@Nonnull
@Beta
public TextInputChannel textToSpeech(
@Nonnull final AudioOutputChannel audioOutputConsumer,
@Nonnull final RealtimeParam... params) {
return new TextToSpeechRealtimeClient(
getRealtimeEndpoint(), buildRealtimeHeaders(), audioOutputConsumer, params);
}

/**
* Creates a realtime channel allowing for audio conversation with a model.
*
* <p>The input channel should be used with a try-with-resources block to ensure that the
* underlying connection is closed.
*
* <p>Example:
*
* <pre>{@code
* try (var audioInputChannel = client.speechToSpeech(audioOutputConsumer)) {
* audioInputChannel.inputAudio(audioBytesData);
* ....
* }
* }</pre>
*
* This API implements full duplex (input + output) communication channels. An application should
* logically synchronize their state and close the input channel when it is appropriate (e.g. the
* last part of the response has been received via the output channel and the application does not
* need to send any other input). When the input channel is closed, the output channel will be
* closed automatically and the output consumer will not be called anymore.
*
* @param audioOutputConsumer - audio consumer of raw PCM mono 24000 Hz little endian output, 16
* bit depth
* @param params - optional configuration params
* @return input channel, allowing for audio data input (bytes, PCM mono 24000 Hz little endian 16
* bit)
*/
@Nonnull
@Beta
public AudioInputChannel speechToSpeech(
@Nonnull final AudioOutputChannel audioOutputConsumer,
@Nonnull final RealtimeParam... params) {
return new SpeechToSpeechRealtimeClient(
getRealtimeEndpoint(), buildRealtimeHeaders(), audioOutputConsumer, params);
}

Map<String, String> buildRealtimeHeaders() {
final var extraHeaders = destination.asHttp().getHeaders();
final var headers = new HashMap<String, String>(extraHeaders.size() + 1);
for (final Header header : extraHeaders) {
headers.put(header.getName(), header.getValue());
}
return headers;
}

String getRealtimeEndpoint() {
final var sb = new StringBuilder(PATH_BUFFER_SIZE);
sb.append("wss://");
final 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.sap.ai.sdk.foundationmodels.openai.realtime;

import com.google.common.annotations.Beta;
import javax.annotation.Nonnull;

/** Represents possible configuration params of realtime client */
@Beta
public interface RealtimeParam {
/** Represents configurable options */
enum ParamName {
/** Voice name to use to produce sound */
OUTPUT_VOICE,
/**
* How model will recognize that it is its turn to respond (e.g. explicitly asked, automatically
* detected)
*/
TURN_DETECTION,
/** Override or specify system prompt given to a model */
SYSTEM_PROMPT,
}

/**
* Returns param name
*
* @return name
*/
@Nonnull
ParamName getParamName();

/**
* Returns string value representation of the param
*
* @return string value
*/
@Nonnull
String getValueAsString();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.sap.ai.sdk.foundationmodels.openai.realtime;

import com.google.common.annotations.Beta;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/** Allows to configure model system prompt */
@Beta
public final class RealtimeParamSystemPrompt implements RealtimeParam {

private final String systemPrompt;

/**
* Constructs RealtimeParamSystemPrompt object
*
* @param systemPrompt system prompt to use
*/
@Beta
public RealtimeParamSystemPrompt(@Nonnull final String systemPrompt) {
this.systemPrompt = systemPrompt;
}

@Override
@Beta
public @Nonnull ParamName getParamName() {
return ParamName.SYSTEM_PROMPT;
}

@Override
@Beta
public @Nonnull String getValueAsString() {
return systemPrompt;
}

@Override
@Beta
public boolean equals(@Nullable final Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
final RealtimeParamSystemPrompt that = (RealtimeParamSystemPrompt) o;
return Objects.equals(systemPrompt, that.systemPrompt);
}

@Override
@Beta
public int hashCode() {
return Objects.hashCode(systemPrompt);
}
}
Loading