feat: OpenAI realtime api#935
Conversation
My current understanding is that the mentioned PR cannot be completed to a SDK realtime solution. Here is the context and outcomes of my research why I think so: There are three ways to implement realtime API with OpenAPI:
The previous PR implements SIP - it is a way for client and server to communicate about a connection in order to pass data, but not to establish connection or pass data in fact. The data layer should be implemented separately from the SIP protocol. OpenAPI SIP protocol by design works with client-side web hooks as a mean to pass data and accept incoming connections (calls). Outgoing calls (client initiated) are not seem to be possible at all using solely SIP API. We cannot expect SDK environment to have public http endpoints accessible from the internet to expose hooks. AI Core does not implement hook functionality either AFAIK. Given that WebRTC is difficult to support on the BE and, AFAIK, our backend only supports Websockets at the moment, Websockets seem to be the only option we have. |
Jonas-Isr
left a comment
There was a problem hiding this comment.
The design is very cool! Thanks a lot for the detailed investigation and work :) I am reviewing this as if it would be the beta release already because I think without too much effort we turn this into a releasable state.
The two points below (and many of my comments) concern code conventions in the team.
General points:
- Please add
@Betato all newly introduced public API. This way we have a grace period to change the API if we overlooked something in the beginning. We periodically re-evaluate and remove these annotations later on. - Please don’t put hand written classes into any of the model/ packages. These are only for generated code. If possible, put the
SpeechOutputParaminterface and the 2 classes implementing it into the open package.
addressed both these points in addition to your comments in the PR |
| <dependency> | ||
| <groupId>com.openai</groupId> | ||
| <artifactId>openai-java</artifactId> | ||
| </dependency> |
There was a problem hiding this comment.
Users of the other OpenAI clients should not have to import this dependency.
This should be optional like the existing openai-java-core that is already in this pom.
| import javax.annotation.Nullable; | ||
|
|
||
| /** Allows to configure turn detection (how model responds). */ | ||
| public final class RealtimeParamTurnDetection implements RealtimeParam { |
There was a problem hiding this comment.
Why are realtime classes in the core module.
If they cannot be used outside of the realtime client they belong in the same folder as the realtime client
| @@ -0,0 +1,18 @@ | |||
| package com.sap.ai.sdk.foundationmodels.openai; | |||
There was a problem hiding this comment.
(Minor) We had a discussion that it could be great to put every class in its own realtime folder to make it easier to maintain and not make the same mistake that we did in the orchestration package that became very crowded.
package com.sap.ai.sdk.foundationmodels.openai.realtime
There was a problem hiding this comment.
missed/forgot the part about the dedicated sub-package
thanks, now moved!
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
Jonas-Isr
left a comment
There was a problem hiding this comment.
The ability to try it out in the sample app is really cool!
Besides Charles' points and the smaller comments below my main concern is test coverage. Can we maybe add some unit tests covering WSOpenAiRealtimeClient, ToAudioRealtimeClient, and the URL construction in OpenAiRealtimeClient.getRealtimeEndpoint()?
| final var extraHeaders = destination.asHttp().getHeaders(); | ||
| final var headers = new HashMap<String, String>(extraHeaders.size() + 1); | ||
| for (Header header : extraHeaders) { | ||
| headers.put(header.getName(), header.getValue()); | ||
| } |
There was a problem hiding this comment.
(Minor)
Would maybe be a bit cleaner to put this in a private method as it is used also in speechToSpeech().
| try { | ||
| ws = this.ws.join(); | ||
| } catch (final CompletionException e) { | ||
| throw new ClientException("failed to establish web socket connection", e); |
There was a problem hiding this comment.
(Very Minor)
Also for other exceptions.
| throw new ClientException("failed to establish web socket connection", e); | |
| throw new ClientException("Failed to establish web socket connection", e); |
| try { | ||
| ws.sendText(JACKSON.writeValueAsString(ResponseCreateEvent.builder().build()), true); | ||
| } catch (final JsonProcessingException e) { | ||
| throw new RuntimeException("Failed to serialize ask for response", e); |
There was a problem hiding this comment.
(Question)
Shouldn't this be a ClientException like the others?
|
|
||
| private static final int MAX_DATA_CHUNK_SIZE_BYTES = 8192; | ||
|
|
||
| private static final String TASK = ""; |
There was a problem hiding this comment.
(Question)
I don't understand the role of TASK here. Is it always empty? Or can it be set by a user?
| @Nonnull final BiConsumer<WebSocket, CharSequence> onText) { | ||
| this.onOpen = onOpen; | ||
| this.onText = onText; | ||
| this.buffer = new StringBuilder(1024 * 1024); |
There was a problem hiding this comment.
(Question)
How many are we expecting users to create of these? Will this size be too large?
There was a problem hiding this comment.
Context
This is now used for responses. There is at least three levels of data segmentation handled by software (not going too deep into networking):
- TCP level - packet partitioning/glueing - handled by operation system
- WebSocket level - partitioning made by the server, packet glueing is done by this buffer
- User application level data partitioning (server sends sound.delta messages in the web socket channel) - managed by the application using the realtime-client/SDK (either to wait and consume fully or play deltas/increments)
It didn't make sense to expose level 2 glueing logic to the app since partial websocket messages would be broken data-wise (not full json) and wouldn't provide any value, but would complicate the SDK exposed API
Response to the question
Users are not intended to create these buffers directly (since package local), but implicitly these buffers will be created once for every open realtime session. The total number of buffers created will depend on how many open realtime conversations a user would want at the same time. This can't normally be above ~64.5k per network interface and limited by the number of available TCP sockets.
I experimented with different requests/responses and most model audio responses were inside 1m range with short responses being typically 200-300k (20-30% of the allocated hardcoded capacity).
This might be slightly over for text responses, but two our use cases (Text-to-Speech and Speech-to-Speech) now output audio along with supplementary text.
We may later add ways to configure this if it will be needed
UPDATE:
I experimented a bit more and it seems that in practice this buffer size can be significantly reduced indeed. I set 128k instead of the previous 1m for now.
| * OpenAI client implementation of Realtime API. Abstracts technical implementation, transport and | ||
| * threading and exposes business-level operations (high level interface) | ||
| */ | ||
| public class OpenAiRealtimeClient { |
There was a problem hiding this comment.
(Major)
@Beta missing on a new public class. Same for AudioInputChannel, AudioOutputChannel, TextInputChannel, TextOutputChannel as far as I see.
| for (RealtimeParam param : params) { | ||
| if (param.getParamName() == RealtimeParam.SpeechOutputParamName.TURN_DETECTION) { | ||
| if (RealtimeParamTurnDetection.EACH_CALL_IS_A_TURN.equals(param)) { | ||
| turnDetectionEager = true; | ||
| } else if (RealtimeParamTurnDetection.BY_MODEL_AUTO.equals(param)) { | ||
| turnDetectionEager = false; | ||
| } | ||
| } | ||
| } | ||
| this.eagerTurnDetection = turnDetectionEager; |
There was a problem hiding this comment.
(Minor)
As far as I can see this is the same as is done in the constructor of the SpeechToSpeechRealtimeClient. Maybe it makes sense to put it into ToAudioRealtimeClient as utility method?
| <h2>⏰ Realtime API</h2> | ||
| </div> | ||
| Realtime API allows for various real time interactions <a | ||
| href="https://todo.todo" |
There was a problem hiding this comment.
(Minor)
Do we have "official" docs somewhere that we can link here?
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
…java into feat/poc-openai-realtime-api
Context
AI/ai-sdk-java-backlog#386.
Adds OpenAI realtime API support
Feature scope:
Definition of Done
Aligned changes with the JavaScript SDK