Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
10 changes: 10 additions & 0 deletions sdk/ai/azure-ai-voicelive/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

### Features Added

- Added custom query parameter support for WebSocket connections:
- `VoiceLiveClientBuilder.customQueryParameters(Map<String, String>)` to set custom query parameters
- Custom parameters are merged with endpoint URL parameters and SDK-managed parameters
- Parameter precedence: Endpoint URL params → Custom params → api-version (SDK managed) → model (method parameter)
- Enables scenarios like deployment-id, region, or other service-specific parameters
- Enhanced session creation flexibility:
- Added `VoiceLiveAsyncClient.startSession()` overload without model parameter
- Model can now be provided via custom query parameters or endpoint URL if required
- Original `startSession(String model)` method preserved for backward compatibility

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@

package com.azure.ai.voicelive;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import com.azure.core.annotation.ServiceClient;
import com.azure.core.credential.KeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.core.http.HttpHeaders;
import com.azure.core.util.logging.ClientLogger;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import reactor.core.publisher.Mono;

/**
* The VoiceLiveAsyncClient provides methods to create and manage real-time voice communication sessions
Expand All @@ -27,6 +30,7 @@ public final class VoiceLiveAsyncClient {
private final TokenCredential tokenCredential;
private final String apiVersion;
private final HttpHeaders additionalHeaders;
private final Map<String, String> customQueryParameters;

/**
* Creates a VoiceLiveAsyncClient with API key authentication.
Expand All @@ -35,13 +39,16 @@ public final class VoiceLiveAsyncClient {
* @param keyCredential The API key credential.
* @param apiVersion The API version.
* @param additionalHeaders Additional headers to include in requests.
* @param customQueryParameters Custom query parameters to include in WebSocket connection.
*/
VoiceLiveAsyncClient(URI endpoint, KeyCredential keyCredential, String apiVersion, HttpHeaders additionalHeaders) {
VoiceLiveAsyncClient(URI endpoint, KeyCredential keyCredential, String apiVersion, HttpHeaders additionalHeaders,
Map<String, String> customQueryParameters) {
this.endpoint = Objects.requireNonNull(endpoint, "'endpoint' cannot be null");
this.keyCredential = Objects.requireNonNull(keyCredential, "'keyCredential' cannot be null");
this.tokenCredential = null;
this.apiVersion = Objects.requireNonNull(apiVersion, "'apiVersion' cannot be null");
this.additionalHeaders = additionalHeaders != null ? additionalHeaders : new HttpHeaders();
this.customQueryParameters = customQueryParameters;
}

/**
Expand All @@ -51,21 +58,24 @@ public final class VoiceLiveAsyncClient {
* @param tokenCredential The token credential.
* @param apiVersion The API version.
* @param additionalHeaders Additional headers to include in requests.
* @param customQueryParameters Custom query parameters to include in WebSocket connection.
*/
VoiceLiveAsyncClient(URI endpoint, TokenCredential tokenCredential, String apiVersion,
HttpHeaders additionalHeaders) {
HttpHeaders additionalHeaders, Map<String, String> customQueryParameters) {
this.endpoint = Objects.requireNonNull(endpoint, "'endpoint' cannot be null");
this.keyCredential = null;
this.tokenCredential = Objects.requireNonNull(tokenCredential, "'tokenCredential' cannot be null");
this.apiVersion = Objects.requireNonNull(apiVersion, "'apiVersion' cannot be null");
this.additionalHeaders = additionalHeaders != null ? additionalHeaders : new HttpHeaders();
this.customQueryParameters = customQueryParameters;
}

/**
* Starts a new VoiceLiveSessionAsyncClient for real-time voice communication.
*
* @param model The model to use for the session.
* @return A Mono containing the connected VoiceLiveSessionAsyncClient.
* @throws NullPointerException if {@code model} is null.
*/
public Mono<VoiceLiveSessionAsyncClient> startSession(String model) {
Objects.requireNonNull(model, "'model' cannot be null");
Expand All @@ -81,6 +91,24 @@ public Mono<VoiceLiveSessionAsyncClient> startSession(String model) {
});
}

/**
* Starts a new VoiceLiveSessionAsyncClient for real-time voice communication without specifying a model.
* The model can be provided via custom query parameters or through the endpoint URL if required by the service.
*
* @return A Mono containing the connected VoiceLiveSessionAsyncClient.
*/
public Mono<VoiceLiveSessionAsyncClient> startSession() {
return Mono.fromCallable(() -> convertToWebSocketEndpoint(endpoint, null)).flatMap(wsEndpoint -> {
VoiceLiveSessionAsyncClient session;
if (keyCredential != null) {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, keyCredential);
} else {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, tokenCredential);
}
return session.connect(additionalHeaders).thenReturn(session);
});
}

/**
* Gets the API version.
*
Expand Down Expand Up @@ -124,26 +152,42 @@ private URI convertToWebSocketEndpoint(URI httpEndpoint, String model) {
path = path.replaceAll("/$", "") + "/voice-live/realtime";
}

// Build query string
StringBuilder queryBuilder = new StringBuilder();
// Build query parameter map to avoid duplicates
Map<String, String> queryParams = new LinkedHashMap<>();

// Start with existing query parameters from the endpoint URL
if (httpEndpoint.getQuery() != null && !httpEndpoint.getQuery().isEmpty()) {
queryBuilder.append(httpEndpoint.getQuery());
String[] pairs = httpEndpoint.getQuery().split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = pair.substring(0, idx);
String value = pair.substring(idx + 1);
queryParams.put(key, value);
}
}
}

// Add api-version if not present
if (!queryBuilder.toString().contains("api-version=")) {
if (queryBuilder.length() > 0) {
queryBuilder.append("&");
}
queryBuilder.append("api-version=").append(apiVersion);
// Add/override with custom query parameters
if (customQueryParameters != null && !customQueryParameters.isEmpty()) {
queryParams.putAll(customQueryParameters);
}

// Add model if not present
if (!queryBuilder.toString().contains("model=")) {
// Ensure api-version is set (SDK's version takes precedence)
queryParams.put("api-version", apiVersion);

// Add model if provided (function parameter takes precedence)
if (model != null && !model.isEmpty()) {
queryParams.put("model", model);
}

// Build final query string
StringBuilder queryBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : queryParams.entrySet()) {
if (queryBuilder.length() > 0) {
queryBuilder.append("&");
}
queryBuilder.append("model=").append(model);
queryBuilder.append(entry.getKey()).append("=").append(entry.getValue());
}

return new URI(scheme, httpEndpoint.getUserInfo(), httpEndpoint.getHost(), httpEndpoint.getPort(), path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

package com.azure.ai.voicelive;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import com.azure.core.annotation.ServiceClientBuilder;
import com.azure.core.client.traits.EndpointTrait;
import com.azure.core.client.traits.KeyCredentialTrait;
Expand All @@ -14,10 +20,6 @@
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;

/**
* Builder for creating instances of {@link VoiceLiveAsyncClient}.
*/
Expand All @@ -31,6 +33,7 @@ public final class VoiceLiveClientBuilder implements TokenCredentialTrait<VoiceL
private TokenCredential tokenCredential;
private VoiceLiveServiceVersion serviceVersion;
private ClientOptions clientOptions;
private Map<String, String> customQueryParameters;

/**
* Creates a new instance of VoiceLiveClientBuilder.
Expand Down Expand Up @@ -107,6 +110,23 @@ public VoiceLiveClientBuilder clientOptions(ClientOptions clientOptions) {
return this;
}

/**
* Sets custom query parameters to be included in the WebSocket connection URL.
* These parameters will be appended to the query string when establishing the WebSocket connection.
* This will replace any previously set custom query parameters.
*
* @param customQueryParameters A map of query parameter names to values.
* @return The updated VoiceLiveClientBuilder instance.
*/
public VoiceLiveClientBuilder customQueryParameters(Map<String, String> customQueryParameters) {
if (customQueryParameters != null) {
this.customQueryParameters = new HashMap<>(customQueryParameters);
} else {
this.customQueryParameters = null;
}
return this;
}

/**
* Builds a {@link VoiceLiveAsyncClient} instance with the configured options.
*
Expand All @@ -126,9 +146,11 @@ public VoiceLiveAsyncClient buildAsyncClient() {
HttpHeaders additionalHeaders = CoreUtils.createHttpHeadersFromClientOptions(clientOptions);

if (keyCredential != null) {
return new VoiceLiveAsyncClient(endpoint, keyCredential, version.getVersion(), additionalHeaders);
return new VoiceLiveAsyncClient(endpoint, keyCredential, version.getVersion(), additionalHeaders,
customQueryParameters);
} else {
return new VoiceLiveAsyncClient(endpoint, tokenCredential, version.getVersion(), additionalHeaders);
return new VoiceLiveAsyncClient(endpoint, tokenCredential, version.getVersion(), additionalHeaders,
customQueryParameters);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class VoiceLiveAsyncClientTest {
@BeforeEach
void setUp() throws Exception {
testEndpoint = new URI("https://test.cognitiveservices.azure.com");
client = new VoiceLiveAsyncClient(testEndpoint, mockKeyCredential, "2024-10-01-preview", mockHeaders);
client = new VoiceLiveAsyncClient(testEndpoint, mockKeyCredential, "2024-10-01-preview", mockHeaders, null);
}

@Test
Expand All @@ -51,15 +51,15 @@ void testConstructorWithValidParameters() {
void testConstructorWithNullEndpoint() {
// Act & Assert
assertThrows(NullPointerException.class, () -> {
new VoiceLiveAsyncClient(null, mockKeyCredential, "2024-10-01-preview", mockHeaders);
new VoiceLiveAsyncClient(null, mockKeyCredential, "2024-10-01-preview", mockHeaders, null);
});
}

@Test
void testConstructorWithNullCredential() {
// Act & Assert
assertThrows(NullPointerException.class, () -> {
new VoiceLiveAsyncClient(testEndpoint, (KeyCredential) null, "2024-10-01-preview", mockHeaders);
new VoiceLiveAsyncClient(testEndpoint, (KeyCredential) null, "2024-10-01-preview", mockHeaders, null);
});
}

Expand Down Expand Up @@ -167,4 +167,51 @@ void testReturnTypeOptimization() {
// The returned Mono should contain a VoiceLiveSessionAsyncClient when subscribed
});
}

@Test
void testStartSessionWithoutModel() {
// Test that startSession() without parameters works
assertDoesNotThrow(() -> {
Mono<VoiceLiveSessionAsyncClient> sessionMono = client.startSession();
assertNotNull(sessionMono);
});
}

@Test
void testConstructorWithCustomQueryParameters() throws Exception {
// Arrange
java.util.Map<String, String> customParams = new java.util.HashMap<>();
customParams.put("deployment-id", "test-deployment");
customParams.put("custom-param", "custom-value");

// Act
VoiceLiveAsyncClient clientWithParams = new VoiceLiveAsyncClient(testEndpoint, mockKeyCredential,
"2024-10-01-preview", mockHeaders, customParams);

// Assert
assertNotNull(clientWithParams);
}

@Test
void testConstructorWithNullCustomQueryParameters() throws Exception {
// Act & Assert
assertDoesNotThrow(() -> {
VoiceLiveAsyncClient clientWithNullParams
= new VoiceLiveAsyncClient(testEndpoint, mockKeyCredential, "2024-10-01-preview", mockHeaders, null);
assertNotNull(clientWithNullParams);
});
}

@Test
void testConstructorWithEmptyCustomQueryParameters() throws Exception {
// Arrange
java.util.Map<String, String> emptyParams = new java.util.HashMap<>();

// Act & Assert
assertDoesNotThrow(() -> {
VoiceLiveAsyncClient clientWithEmptyParams = new VoiceLiveAsyncClient(testEndpoint, mockKeyCredential,
"2024-10-01-preview", mockHeaders, emptyParams);
assertNotNull(clientWithEmptyParams);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,66 @@ void testBuilderReturnsBuilder() {
assertSame(clientBuilder, clientBuilder.credential(mockKeyCredential));
assertSame(clientBuilder, clientBuilder.serviceVersion(VoiceLiveServiceVersion.V2025_10_01));
}

@Test
void testBuilderWithCustomQueryParameters() {
// Arrange
String endpoint = "https://test.cognitiveservices.azure.com";
java.util.Map<String, String> customParams = new java.util.HashMap<>();
customParams.put("deployment-id", "test-deployment");
customParams.put("region", "eastus");

// Act & Assert
assertDoesNotThrow(() -> {
VoiceLiveAsyncClient client = clientBuilder.endpoint(endpoint)
.credential(mockKeyCredential)
.customQueryParameters(customParams)
.buildAsyncClient();

assertNotNull(client);
});
}

@Test
void testBuilderWithNullCustomQueryParameters() {
// Arrange
String endpoint = "https://test.cognitiveservices.azure.com";

// Act & Assert
assertDoesNotThrow(() -> {
VoiceLiveAsyncClient client = clientBuilder.endpoint(endpoint)
.credential(mockKeyCredential)
.customQueryParameters(null)
.buildAsyncClient();

assertNotNull(client);
});
}

@Test
void testBuilderWithEmptyCustomQueryParameters() {
// Arrange
String endpoint = "https://test.cognitiveservices.azure.com";
java.util.Map<String, String> emptyParams = new java.util.HashMap<>();

// Act & Assert
assertDoesNotThrow(() -> {
VoiceLiveAsyncClient client = clientBuilder.endpoint(endpoint)
.credential(mockKeyCredential)
.customQueryParameters(emptyParams)
.buildAsyncClient();

assertNotNull(client);
});
}

@Test
void testCustomQueryParametersReturnsBuilder() {
// Arrange
java.util.Map<String, String> customParams = new java.util.HashMap<>();
customParams.put("test", "value");

// Act & Assert
assertSame(clientBuilder, clientBuilder.customQueryParameters(customParams));
}
}
Loading
Loading