33
44package com .azure .ai .voicelive ;
55
6+ import java .net .URI ;
7+ import java .net .URISyntaxException ;
8+ import java .util .LinkedHashMap ;
9+ import java .util .Map ;
10+ import java .util .Objects ;
11+
12+ import com .azure .ai .voicelive .models .VoiceLiveRequestOptions ;
613import com .azure .core .annotation .ServiceClient ;
714import com .azure .core .credential .KeyCredential ;
815import com .azure .core .credential .TokenCredential ;
916import com .azure .core .http .HttpHeaders ;
1017import com .azure .core .util .logging .ClientLogger ;
11- import reactor .core .publisher .Mono ;
1218
13- import java .net .URI ;
14- import java .net .URISyntaxException ;
15- import java .util .Objects ;
19+ import reactor .core .publisher .Mono ;
1620
1721/**
1822 * The VoiceLiveAsyncClient provides methods to create and manage real-time voice communication sessions
@@ -66,6 +70,7 @@ public final class VoiceLiveAsyncClient {
6670 *
6771 * @param model The model to use for the session.
6872 * @return A Mono containing the connected VoiceLiveSessionAsyncClient.
73+ * @throws NullPointerException if {@code model} is null.
6974 */
7075 public Mono <VoiceLiveSessionAsyncClient > startSession (String model ) {
7176 Objects .requireNonNull (model , "'model' cannot be null" );
@@ -81,6 +86,77 @@ public Mono<VoiceLiveSessionAsyncClient> startSession(String model) {
8186 });
8287 }
8388
89+ /**
90+ * Starts a new VoiceLiveSessionAsyncClient for real-time voice communication without specifying a model.
91+ * The model can be provided via custom query parameters or through the endpoint URL if required by the service.
92+ *
93+ * @return A Mono containing the connected VoiceLiveSessionAsyncClient.
94+ */
95+ public Mono <VoiceLiveSessionAsyncClient > startSession () {
96+ return Mono .fromCallable (() -> convertToWebSocketEndpoint (endpoint , null )).flatMap (wsEndpoint -> {
97+ VoiceLiveSessionAsyncClient session ;
98+ if (keyCredential != null ) {
99+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , keyCredential );
100+ } else {
101+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , tokenCredential );
102+ }
103+ return session .connect (additionalHeaders ).thenReturn (session );
104+ });
105+ }
106+
107+ /**
108+ * Starts a new VoiceLiveSessionAsyncClient for real-time voice communication with custom request options.
109+ *
110+ * @param model The model to use for the session.
111+ * @param requestOptions Custom query parameters and headers for the request.
112+ * @return A Mono containing the connected VoiceLiveSessionAsyncClient.
113+ * @throws NullPointerException if {@code model} or {@code requestOptions} is null.
114+ */
115+ public Mono <VoiceLiveSessionAsyncClient > startSession (String model , VoiceLiveRequestOptions requestOptions ) {
116+ Objects .requireNonNull (model , "'model' cannot be null" );
117+ Objects .requireNonNull (requestOptions , "'requestOptions' cannot be null" );
118+
119+ return Mono
120+ .fromCallable (() -> convertToWebSocketEndpoint (endpoint , model , requestOptions .getCustomQueryParameters ()))
121+ .flatMap (wsEndpoint -> {
122+ VoiceLiveSessionAsyncClient session ;
123+ if (keyCredential != null ) {
124+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , keyCredential );
125+ } else {
126+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , tokenCredential );
127+ }
128+ // Merge additional headers with custom headers from requestOptions
129+ HttpHeaders mergedHeaders = mergeHeaders (additionalHeaders , requestOptions .getCustomHeaders ());
130+ return session .connect (mergedHeaders ).thenReturn (session );
131+ });
132+ }
133+
134+ /**
135+ * Starts a new VoiceLiveSessionAsyncClient for real-time voice communication with custom request options.
136+ * The model can be provided via custom query parameters.
137+ *
138+ * @param requestOptions Custom query parameters and headers for the request.
139+ * @return A Mono containing the connected VoiceLiveSessionAsyncClient.
140+ * @throws NullPointerException if {@code requestOptions} is null.
141+ */
142+ public Mono <VoiceLiveSessionAsyncClient > startSession (VoiceLiveRequestOptions requestOptions ) {
143+ Objects .requireNonNull (requestOptions , "'requestOptions' cannot be null" );
144+
145+ return Mono
146+ .fromCallable (() -> convertToWebSocketEndpoint (endpoint , null , requestOptions .getCustomQueryParameters ()))
147+ .flatMap (wsEndpoint -> {
148+ VoiceLiveSessionAsyncClient session ;
149+ if (keyCredential != null ) {
150+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , keyCredential );
151+ } else {
152+ session = new VoiceLiveSessionAsyncClient (wsEndpoint , tokenCredential );
153+ }
154+ // Merge additional headers with custom headers from requestOptions
155+ HttpHeaders mergedHeaders = mergeHeaders (additionalHeaders , requestOptions .getCustomHeaders ());
156+ return session .connect (mergedHeaders ).thenReturn (session );
157+ });
158+ }
159+
84160 /**
85161 * Gets the API version.
86162 *
@@ -90,6 +166,24 @@ String getApiVersion() {
90166 return apiVersion ;
91167 }
92168
169+ /**
170+ * Merges two HttpHeaders objects, with custom headers taking precedence.
171+ *
172+ * @param baseHeaders The base headers.
173+ * @param customHeaders The custom headers to merge.
174+ * @return The merged HttpHeaders.
175+ */
176+ private HttpHeaders mergeHeaders (HttpHeaders baseHeaders , HttpHeaders customHeaders ) {
177+ HttpHeaders merged = new HttpHeaders ();
178+ if (baseHeaders != null ) {
179+ baseHeaders .forEach (header -> merged .set (header .getName (), header .getValue ()));
180+ }
181+ if (customHeaders != null ) {
182+ customHeaders .forEach (header -> merged .set (header .getName (), header .getValue ()));
183+ }
184+ return merged ;
185+ }
186+
93187 /**
94188 * Converts an HTTP endpoint to a WebSocket endpoint.
95189 *
@@ -124,26 +218,118 @@ private URI convertToWebSocketEndpoint(URI httpEndpoint, String model) {
124218 path = path .replaceAll ("/$" , "" ) + "/voice-live/realtime" ;
125219 }
126220
127- // Build query string
128- StringBuilder queryBuilder = new StringBuilder ();
221+ // Build query parameter map to avoid duplicates
222+ Map <String , String > queryParams = new LinkedHashMap <>();
223+
224+ // Start with existing query parameters from the endpoint URL
129225 if (httpEndpoint .getQuery () != null && !httpEndpoint .getQuery ().isEmpty ()) {
130- queryBuilder .append (httpEndpoint .getQuery ());
226+ String [] pairs = httpEndpoint .getQuery ().split ("&" );
227+ for (String pair : pairs ) {
228+ int idx = pair .indexOf ("=" );
229+ if (idx > 0 ) {
230+ String key = pair .substring (0 , idx );
231+ String value = pair .substring (idx + 1 );
232+ queryParams .put (key , value );
233+ }
234+ }
235+ }
236+
237+ // Ensure api-version is set (SDK's version takes precedence)
238+ queryParams .put ("api-version" , apiVersion );
239+
240+ // Add model if provided (function parameter takes precedence)
241+ if (model != null && !model .isEmpty ()) {
242+ queryParams .put ("model" , model );
131243 }
132244
133- // Add api-version if not present
134- if (!queryBuilder .toString ().contains ("api-version=" )) {
245+ // Build final query string
246+ StringBuilder queryBuilder = new StringBuilder ();
247+ for (Map .Entry <String , String > entry : queryParams .entrySet ()) {
135248 if (queryBuilder .length () > 0 ) {
136249 queryBuilder .append ("&" );
137250 }
138- queryBuilder .append ("api-version =" ).append (apiVersion );
251+ queryBuilder .append (entry . getKey ()). append ( " =" ).append (entry . getValue () );
139252 }
140253
141- // Add model if not present
142- if (!queryBuilder .toString ().contains ("model=" )) {
254+ return new URI (scheme , httpEndpoint .getUserInfo (), httpEndpoint .getHost (), httpEndpoint .getPort (), path ,
255+ queryBuilder .length () > 0 ? queryBuilder .toString () : null , httpEndpoint .getFragment ());
256+ } catch (URISyntaxException e ) {
257+ throw LOGGER
258+ .logExceptionAsError (new IllegalArgumentException ("Failed to convert endpoint to WebSocket URI" , e ));
259+ }
260+ }
261+
262+ /**
263+ * Converts an HTTP endpoint to a WebSocket endpoint with additional custom query parameters.
264+ *
265+ * @param httpEndpoint The HTTP endpoint to convert.
266+ * @param model The model name to include in the query string.
267+ * @param additionalQueryParams Additional custom query parameters to include.
268+ * @return The WebSocket endpoint URI.
269+ */
270+ private URI convertToWebSocketEndpoint (URI httpEndpoint , String model , Map <String , String > additionalQueryParams ) {
271+ try {
272+ String scheme ;
273+ switch (httpEndpoint .getScheme ().toLowerCase ()) {
274+ case "wss" :
275+ case "ws" :
276+ scheme = httpEndpoint .getScheme ();
277+ break ;
278+
279+ case "https" :
280+ scheme = "wss" ;
281+ break ;
282+
283+ case "http" :
284+ scheme = "ws" ;
285+ break ;
286+
287+ default :
288+ throw LOGGER .logExceptionAsError (
289+ new IllegalArgumentException ("Scheme " + httpEndpoint .getScheme () + " is not supported" ));
290+ }
291+
292+ String path = httpEndpoint .getPath ();
293+ if (!path .endsWith ("/realtime" )) {
294+ path = path .replaceAll ("/$" , "" ) + "/voice-live/realtime" ;
295+ }
296+
297+ // Build query parameter map to avoid duplicates
298+ Map <String , String > queryParams = new LinkedHashMap <>();
299+
300+ // Start with existing query parameters from the endpoint URL
301+ if (httpEndpoint .getQuery () != null && !httpEndpoint .getQuery ().isEmpty ()) {
302+ String [] pairs = httpEndpoint .getQuery ().split ("&" );
303+ for (String pair : pairs ) {
304+ int idx = pair .indexOf ("=" );
305+ if (idx > 0 ) {
306+ String key = pair .substring (0 , idx );
307+ String value = pair .substring (idx + 1 );
308+ queryParams .put (key , value );
309+ }
310+ }
311+ }
312+
313+ // Add/override with custom query parameters from request options
314+ if (additionalQueryParams != null && !additionalQueryParams .isEmpty ()) {
315+ queryParams .putAll (additionalQueryParams );
316+ }
317+
318+ // Ensure api-version is set (SDK's version takes precedence)
319+ queryParams .put ("api-version" , apiVersion );
320+
321+ // Add model if provided (function parameter takes precedence)
322+ if (model != null && !model .isEmpty ()) {
323+ queryParams .put ("model" , model );
324+ }
325+
326+ // Build final query string
327+ StringBuilder queryBuilder = new StringBuilder ();
328+ for (Map .Entry <String , String > entry : queryParams .entrySet ()) {
143329 if (queryBuilder .length () > 0 ) {
144330 queryBuilder .append ("&" );
145331 }
146- queryBuilder .append ("model =" ).append (model );
332+ queryBuilder .append (entry . getKey ()). append ( " =" ).append (entry . getValue () );
147333 }
148334
149335 return new URI (scheme , httpEndpoint .getUserInfo (), httpEndpoint .getHost (), httpEndpoint .getPort (), path ,
0 commit comments