Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 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,15 @@

### Features Added

- Added `VoiceLiveRequestOptions` class for per-request customization:
- Supports custom query parameters via `addCustomQueryParameter(String key, String value)` method
- Supports custom headers via `addCustomHeader(String name, String value)` and `setCustomHeaders(HttpHeaders)` methods
- Custom parameters and headers can be passed to session creation methods
- Enhanced session creation with new overloads:
- Added `startSession(String model, VoiceLiveRequestOptions requestOptions)` for model with custom options
- Added `startSession(VoiceLiveRequestOptions requestOptions)` for custom options without explicit model parameter
- Original `startSession(String model)` and `startSession()` methods 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,20 @@

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.ai.voicelive.models.VoiceLiveRequestOptions;
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 Down Expand Up @@ -66,6 +70,7 @@ public final class VoiceLiveAsyncClient {
*
* @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 +86,77 @@ 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);
});
}

/**
* Starts a new VoiceLiveSessionAsyncClient for real-time voice communication with custom request options.
*
* @param model The model to use for the session.
* @param requestOptions Custom query parameters and headers for the request.
* @return A Mono containing the connected VoiceLiveSessionAsyncClient.
* @throws NullPointerException if {@code model} or {@code requestOptions} is null.
*/
public Mono<VoiceLiveSessionAsyncClient> startSession(String model, VoiceLiveRequestOptions requestOptions) {
Objects.requireNonNull(model, "'model' cannot be null");
Objects.requireNonNull(requestOptions, "'requestOptions' cannot be null");

return Mono
.fromCallable(() -> convertToWebSocketEndpoint(endpoint, model, requestOptions.getCustomQueryParameters()))
.flatMap(wsEndpoint -> {
VoiceLiveSessionAsyncClient session;
if (keyCredential != null) {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, keyCredential);
} else {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, tokenCredential);
}
// Merge additional headers with custom headers from requestOptions
HttpHeaders mergedHeaders = mergeHeaders(additionalHeaders, requestOptions.getCustomHeaders());
return session.connect(mergedHeaders).thenReturn(session);
});
}

/**
* Starts a new VoiceLiveSessionAsyncClient for real-time voice communication with custom request options.
* The model can be provided via custom query parameters.
*
* @param requestOptions Custom query parameters and headers for the request.
* @return A Mono containing the connected VoiceLiveSessionAsyncClient.
* @throws NullPointerException if {@code requestOptions} is null.
*/
public Mono<VoiceLiveSessionAsyncClient> startSession(VoiceLiveRequestOptions requestOptions) {
Objects.requireNonNull(requestOptions, "'requestOptions' cannot be null");

return Mono
.fromCallable(() -> convertToWebSocketEndpoint(endpoint, null, requestOptions.getCustomQueryParameters()))
.flatMap(wsEndpoint -> {
VoiceLiveSessionAsyncClient session;
if (keyCredential != null) {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, keyCredential);
} else {
session = new VoiceLiveSessionAsyncClient(wsEndpoint, tokenCredential);
}
// Merge additional headers with custom headers from requestOptions
HttpHeaders mergedHeaders = mergeHeaders(additionalHeaders, requestOptions.getCustomHeaders());
return session.connect(mergedHeaders).thenReturn(session);
});
}

/**
* Gets the API version.
*
Expand All @@ -90,6 +166,24 @@ String getApiVersion() {
return apiVersion;
}

/**
* Merges two HttpHeaders objects, with custom headers taking precedence.
*
* @param baseHeaders The base headers.
* @param customHeaders The custom headers to merge.
* @return The merged HttpHeaders.
*/
private HttpHeaders mergeHeaders(HttpHeaders baseHeaders, HttpHeaders customHeaders) {
HttpHeaders merged = new HttpHeaders();
if (baseHeaders != null) {
baseHeaders.forEach(header -> merged.set(header.getName(), header.getValue()));
}
if (customHeaders != null) {
customHeaders.forEach(header -> merged.set(header.getName(), header.getValue()));
}
return merged;
}

/**
* Converts an HTTP endpoint to a WebSocket endpoint.
*
Expand Down Expand Up @@ -124,26 +218,118 @@ 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);
}
}
}

// 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);
}

// Add api-version if not present
if (!queryBuilder.toString().contains("api-version=")) {
// Build final query string
StringBuilder queryBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : queryParams.entrySet()) {
if (queryBuilder.length() > 0) {
queryBuilder.append("&");
}
queryBuilder.append("api-version=").append(apiVersion);
queryBuilder.append(entry.getKey()).append("=").append(entry.getValue());
}

// Add model if not present
if (!queryBuilder.toString().contains("model=")) {
return new URI(scheme, httpEndpoint.getUserInfo(), httpEndpoint.getHost(), httpEndpoint.getPort(), path,
queryBuilder.length() > 0 ? queryBuilder.toString() : null, httpEndpoint.getFragment());
} catch (URISyntaxException e) {
throw LOGGER
.logExceptionAsError(new IllegalArgumentException("Failed to convert endpoint to WebSocket URI", e));
}
}

/**
* Converts an HTTP endpoint to a WebSocket endpoint with additional custom query parameters.
*
* @param httpEndpoint The HTTP endpoint to convert.
* @param model The model name to include in the query string.
* @param additionalQueryParams Additional custom query parameters to include.
* @return The WebSocket endpoint URI.
*/
private URI convertToWebSocketEndpoint(URI httpEndpoint, String model, Map<String, String> additionalQueryParams) {
try {
String scheme;
switch (httpEndpoint.getScheme().toLowerCase()) {
case "wss":
case "ws":
scheme = httpEndpoint.getScheme();
break;

case "https":
scheme = "wss";
break;

case "http":
scheme = "ws";
break;

default:
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("Scheme " + httpEndpoint.getScheme() + " is not supported"));
}

String path = httpEndpoint.getPath();
if (!path.endsWith("/realtime")) {
path = path.replaceAll("/$", "") + "/voice-live/realtime";
}

// 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()) {
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/override with custom query parameters from request options
if (additionalQueryParams != null && !additionalQueryParams.isEmpty()) {
queryParams.putAll(additionalQueryParams);
}

// 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,10 @@

package com.azure.ai.voicelive;

import java.net.URI;
import java.net.URISyntaxException;
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 +18,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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.ai.voicelive.models;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import com.azure.core.annotation.Fluent;
import com.azure.core.http.HttpHeaders;

/**
* Options for customizing VoiceLive requests with additional query parameters and headers.
*/
@Fluent
public final class VoiceLiveRequestOptions {

private Map<String, String> customQueryParameters;
private HttpHeaders customHeaders;

/**
* Creates a new instance of VoiceLiveRequestOptions.
*/
public VoiceLiveRequestOptions() {
this.customQueryParameters = new HashMap<>();
this.customHeaders = new HttpHeaders();
}

/**
* Gets the custom query parameters.
*
* @return The custom query parameters.
*/
public Map<String, String> getCustomQueryParameters() {
return customQueryParameters;
}

/**
* Adds a custom query parameter.
*
* @param key The query parameter key.
* @param value The query parameter value.
* @return The updated VoiceLiveRequestOptions object.
* @throws NullPointerException if {@code key} is null.
*/
public VoiceLiveRequestOptions addCustomQueryParameter(String key, String value) {
Objects.requireNonNull(key, "'key' cannot be null");
if (this.customQueryParameters == null) {
this.customQueryParameters = new HashMap<>();
}
this.customQueryParameters.put(key, value);
return this;
}

/**
* Gets the custom headers.
*
* @return The custom headers.
*/
public HttpHeaders getCustomHeaders() {
return customHeaders;
}

/**
* Sets the custom headers.
*
* @param customHeaders The custom headers to set.
* @return The updated VoiceLiveRequestOptions object.
*/
public VoiceLiveRequestOptions setCustomHeaders(HttpHeaders customHeaders) {
this.customHeaders = customHeaders != null ? customHeaders : new HttpHeaders();
return this;
}

/**
* Adds a custom header.
*
* @param name The header name.
* @param value The header value.
* @return The updated VoiceLiveRequestOptions object.
* @throws NullPointerException if {@code name} is null.
*/
public VoiceLiveRequestOptions addCustomHeader(String name, String value) {
Objects.requireNonNull(name, "'name' cannot be null");
if (this.customHeaders == null) {
this.customHeaders = new HttpHeaders();
}
this.customHeaders.set(name, value);
return this;
}
}
Loading
Loading