Skip to content

Commit f299d78

Browse files
xitzhangXiting Zhang
andauthored
[VoiceLive]Add custom query parameters support and optional model par… (#47637)
* [VoiceLive]Add custom query parameters support and optional model parameter * create VoiceLiveRequestOptions --------- Co-authored-by: Xiting Zhang <xitzhang@microsoft.com>
1 parent 584f7e8 commit f299d78

6 files changed

Lines changed: 496 additions & 17 deletions

File tree

sdk/ai/azure-ai-voicelive/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
### Features Added
66

7+
- Added `VoiceLiveRequestOptions` class for per-request customization:
8+
- Supports custom query parameters via `addCustomQueryParameter(String key, String value)` method
9+
- Supports custom headers via `addCustomHeader(String name, String value)` and `setCustomHeaders(HttpHeaders)` methods
10+
- Custom parameters and headers can be passed to session creation methods
11+
- Enhanced session creation with new overloads:
12+
- Added `startSession(String model, VoiceLiveRequestOptions requestOptions)` for model with custom options
13+
- Added `startSession(VoiceLiveRequestOptions requestOptions)` for custom options without explicit model parameter
14+
- Original `startSession(String model)` and `startSession()` methods preserved for backward compatibility
15+
716
### Breaking Changes
817

918
### Bugs Fixed

sdk/ai/azure-ai-voicelive/src/main/java/com/azure/ai/voicelive/VoiceLiveAsyncClient.java

Lines changed: 199 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33

44
package 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;
613
import com.azure.core.annotation.ServiceClient;
714
import com.azure.core.credential.KeyCredential;
815
import com.azure.core.credential.TokenCredential;
916
import com.azure.core.http.HttpHeaders;
1017
import 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,

sdk/ai/azure-ai-voicelive/src/main/java/com/azure/ai/voicelive/VoiceLiveClientBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
package com.azure.ai.voicelive;
55

6+
import java.net.URI;
7+
import java.net.URISyntaxException;
8+
import java.util.Objects;
9+
610
import com.azure.core.annotation.ServiceClientBuilder;
711
import com.azure.core.client.traits.EndpointTrait;
812
import com.azure.core.client.traits.KeyCredentialTrait;
@@ -14,10 +18,6 @@
1418
import com.azure.core.util.CoreUtils;
1519
import com.azure.core.util.logging.ClientLogger;
1620

17-
import java.net.URI;
18-
import java.net.URISyntaxException;
19-
import java.util.Objects;
20-
2121
/**
2222
* Builder for creating instances of {@link VoiceLiveAsyncClient}.
2323
*/
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.ai.voicelive.models;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import java.util.Objects;
9+
10+
import com.azure.core.annotation.Fluent;
11+
import com.azure.core.http.HttpHeaders;
12+
13+
/**
14+
* Options for customizing VoiceLive requests with additional query parameters and headers.
15+
*/
16+
@Fluent
17+
public final class VoiceLiveRequestOptions {
18+
19+
private Map<String, String> customQueryParameters;
20+
private HttpHeaders customHeaders;
21+
22+
/**
23+
* Creates a new instance of VoiceLiveRequestOptions.
24+
*/
25+
public VoiceLiveRequestOptions() {
26+
this.customQueryParameters = new HashMap<>();
27+
this.customHeaders = new HttpHeaders();
28+
}
29+
30+
/**
31+
* Gets the custom query parameters.
32+
*
33+
* @return The custom query parameters.
34+
*/
35+
public Map<String, String> getCustomQueryParameters() {
36+
return customQueryParameters;
37+
}
38+
39+
/**
40+
* Adds a custom query parameter.
41+
*
42+
* @param key The query parameter key.
43+
* @param value The query parameter value.
44+
* @return The updated VoiceLiveRequestOptions object.
45+
* @throws NullPointerException if {@code key} is null.
46+
*/
47+
public VoiceLiveRequestOptions addCustomQueryParameter(String key, String value) {
48+
Objects.requireNonNull(key, "'key' cannot be null");
49+
if (this.customQueryParameters == null) {
50+
this.customQueryParameters = new HashMap<>();
51+
}
52+
this.customQueryParameters.put(key, value);
53+
return this;
54+
}
55+
56+
/**
57+
* Gets the custom headers.
58+
*
59+
* @return The custom headers.
60+
*/
61+
public HttpHeaders getCustomHeaders() {
62+
return customHeaders;
63+
}
64+
65+
/**
66+
* Sets the custom headers.
67+
*
68+
* @param customHeaders The custom headers to set.
69+
* @return The updated VoiceLiveRequestOptions object.
70+
*/
71+
public VoiceLiveRequestOptions setCustomHeaders(HttpHeaders customHeaders) {
72+
this.customHeaders = customHeaders != null ? customHeaders : new HttpHeaders();
73+
return this;
74+
}
75+
76+
/**
77+
* Adds a custom header.
78+
*
79+
* @param name The header name.
80+
* @param value The header value.
81+
* @return The updated VoiceLiveRequestOptions object.
82+
* @throws NullPointerException if {@code name} is null.
83+
*/
84+
public VoiceLiveRequestOptions addCustomHeader(String name, String value) {
85+
Objects.requireNonNull(name, "'name' cannot be null");
86+
if (this.customHeaders == null) {
87+
this.customHeaders = new HttpHeaders();
88+
}
89+
this.customHeaders.set(name, value);
90+
return this;
91+
}
92+
}

0 commit comments

Comments
 (0)