diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 35ba63950967..2de6e9d3f4fa 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -36,7 +36,7 @@ com.microsoft.sqlserver:mssql-jdbc;10.2.3.jre8 com.microsoft.azure:azure-functions-maven-plugin;1.30.0 com.microsoft.azure.functions:azure-functions-java-library;2.2.0 com.mysql:mysql-connector-j;9.0.0 -com.openai:openai-java;4.6.1 +com.openai:openai-java;4.14.0 com.squareup.okhttp3:okhttp;4.12.0 commons-codec:commons-codec;1.15 commons-net:commons-net;3.9.0 diff --git a/sdk/ai/azure-ai-agents/CHANGELOG.md b/sdk/ai/azure-ai-agents/CHANGELOG.md index a3ca8244915d..2de616879d89 100644 --- a/sdk/ai/azure-ai-agents/CHANGELOG.md +++ b/sdk/ai/azure-ai-agents/CHANGELOG.md @@ -7,6 +7,7 @@ - New `MemorySearchAgent` sample was added demonstrating memory search functionality - Tests for `MemoryStoresClient` and `MemoryStoresAsyncClient` - Various documentation updates +- Using unified `HttpClient` setup for Azure specifics and `openai` client library wrapping methods ### Breaking Changes @@ -16,6 +17,8 @@ ### Other Changes +- Updated version of `openai` client library to `4.14.0` + ## 1.0.0-beta.1 (2025-11-12) ### Features Added diff --git a/sdk/ai/azure-ai-agents/pom.xml b/sdk/ai/azure-ai-agents/pom.xml index 378c86a00a9b..b50a8af301e6 100644 --- a/sdk/ai/azure-ai-agents/pom.xml +++ b/sdk/ai/azure-ai-agents/pom.xml @@ -52,7 +52,7 @@ com.openai openai-java - 4.6.1 + 4.14.0 com.azure @@ -97,7 +97,7 @@ - com.openai:openai-java:[4.6.1] + com.openai:openai-java:[4.14.0] diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java index e7df9ee0c91f..8052a17e5902 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java @@ -28,8 +28,11 @@ import com.openai.errors.UnauthorizedException; import com.openai.errors.UnexpectedStatusCodeException; import com.openai.errors.UnprocessableEntityException; +import reactor.core.publisher.Mono; import java.io.ByteArrayOutputStream; +import java.net.MalformedURLException; +import java.net.URI; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -82,10 +85,14 @@ public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) Objects.requireNonNull(request, "request"); Objects.requireNonNull(requestOptions, "requestOptions"); - com.azure.core.http.HttpRequest azureRequest = buildAzureRequest(request); - - return new AzureHttpResponseAdapter( - this.httpPipeline.sendSync(azureRequest, buildRequestContext(requestOptions))); + try { + com.azure.core.http.HttpRequest azureRequest = buildAzureRequest(request); + return new AzureHttpResponseAdapter( + this.httpPipeline.sendSync(azureRequest, buildRequestContext(requestOptions))); + } catch (MalformedURLException exception) { + throw new OpenAIException("Invalid URL in request: " + exception.getMessage(), + LOGGER.logThrowableAsError(exception)); + } } @Override @@ -98,9 +105,8 @@ public CompletableFuture executeAsync(HttpRequest request, Request Objects.requireNonNull(request, "request"); Objects.requireNonNull(requestOptions, "requestOptions"); - final com.azure.core.http.HttpRequest azureRequest = buildAzureRequest(request); - - return this.httpPipeline.send(azureRequest, buildRequestContext(requestOptions)) + return Mono.fromCallable(() -> buildAzureRequest(request)) + .flatMap(azureRequest -> this.httpPipeline.send(azureRequest, buildRequestContext(requestOptions))) .map(response -> (HttpResponse) new AzureHttpResponseAdapter(response)) .onErrorMap(HttpClientWrapper::mapAzureExceptionToOpenAI) .toFuture(); @@ -163,7 +169,7 @@ private static Throwable mapAzureExceptionToOpenAI(Throwable throwable) { } else if (throwable instanceof TimeoutException) { return throwable; } else { - return new OpenAIException(throwable.getMessage(), throwable.getCause()); + return new OpenAIException(throwable.getMessage(), throwable); } } @@ -179,7 +185,8 @@ private static Headers toOpenAIHeaders(HttpHeaders azureHeaders) { /** * Converts the OpenAI request metadata and body into an Azure {@link com.azure.core.http.HttpRequest}. */ - private static com.azure.core.http.HttpRequest buildAzureRequest(HttpRequest request) { + private static com.azure.core.http.HttpRequest buildAzureRequest(HttpRequest request) + throws MalformedURLException { HttpRequestBody requestBody = request.body(); String contentType = requestBody != null ? requestBody.contentType() : null; BinaryData bodyData = null; @@ -196,7 +203,7 @@ private static com.azure.core.http.HttpRequest buildAzureRequest(HttpRequest req } com.azure.core.http.HttpRequest azureRequest = new com.azure.core.http.HttpRequest( - HttpMethod.valueOf(request.method().name()), OpenAiRequestUrlBuilder.buildUrl(request), headers); + HttpMethod.valueOf(request.method().name()), URI.create(request.url()).toURL(), headers); if (bodyData != null) { azureRequest.setBody(bodyData); diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java deleted file mode 100644 index f4490f771020..000000000000 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.ai.agents.implementation.http; - -import com.azure.core.util.CoreUtils; -import com.azure.core.util.logging.ClientLogger; -import com.openai.core.http.HttpRequest; -import com.openai.core.http.QueryParams; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.List; -import java.util.StringJoiner; - -/** - * Utility methods that reconstruct the absolute {@link URL} required by the Azure pipeline from the - * OpenAI request metadata. The builder keeps the low-level path/query handling isolated so that - * {@link HttpClientHelper} can focus on the higher-level request mapping logic. - * This class will be deprecated as soon as support is added in the OpenAI SDK as described in this issue: - * - */ -public final class OpenAiRequestUrlBuilder { - - private static final ClientLogger LOGGER = new ClientLogger(OpenAiRequestUrlBuilder.class); - - private OpenAiRequestUrlBuilder() { - } - - /** - * Builds an absolute {@link URL} using the base URL, path segments, and query parameters that are stored in the - * OpenAI {@link HttpRequest} abstraction. - * This method will be deprecated as soon as support is added in the OpenAI SDK as described in this issue: - * - * - * @param request Source request provided by the OpenAI client. - * @return Absolute URL that can be consumed by Azure HTTP components. - */ - static URL buildUrl(HttpRequest request) { - try { - URI baseUri = URI.create(request.baseUrl()); - URL baseUrl = baseUri.toURL(); - String path = buildPath(baseUrl.getPath(), request.pathSegments()); - String query = buildQueryString(request.queryParams()); - URI resolved = new URI(baseUrl.getProtocol(), baseUrl.getUserInfo(), baseUrl.getHost(), baseUrl.getPort(), - path, query, null); - return resolved.toURL(); - } catch (MalformedURLException | URISyntaxException ex) { - throw LOGGER.logThrowableAsWarning(new IllegalStateException( - "Failed to build Azure HTTP request URL from base: " + request.baseUrl(), ex)); - } - } - - /** - * Creates a normalized path that merges the OpenAI base path with the additional path segments present on the - * request. - */ - private static String buildPath(String basePath, List pathSegments) { - StringBuilder builder = new StringBuilder(); - String normalizedBasePath = normalizeBasePath(basePath); - if (!CoreUtils.isNullOrEmpty(normalizedBasePath)) { - builder.append(normalizedBasePath); - } - - for (String segment : pathSegments) { - if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '/') { - builder.append('/'); - } - if (segment != null) { - builder.append(segment); - } - } - - return builder.length() == 0 ? "/" : builder.toString(); - } - - /** - * Normalizes the base path ensuring trailing slashes are removed and {@code null} inputs result in an empty path. - */ - private static String normalizeBasePath(String basePath) { - if (CoreUtils.isNullOrEmpty(basePath)) { - return ""; - } - if ("/".equals(basePath)) { - return ""; - } - return trimTrailingSlash(basePath); - } - - /** - * Removes the final {@code '/'} character when present so that subsequent concatenation does not duplicate - * separators. - */ - private static String trimTrailingSlash(String value) { - if (value == null) { - return null; - } - int length = value.length(); - if (length == 0) { - return value; - } - return value.charAt(length - 1) == '/' ? value.substring(0, length - 1) : value; - } - - /** - * Converts OpenAI {@link QueryParams} into a flattened query string. Encoding is deferred to {@link URI} so we do - * not double-encode values already escaped by upstream layers. - */ - private static String buildQueryString(QueryParams queryParams) { - if (queryParams == null || queryParams.isEmpty()) { - return null; - } - - StringJoiner joiner = new StringJoiner("&"); - queryParams.keys().forEach(name -> { - List values = queryParams.values(name); - if (values.isEmpty()) { - joiner.add(name); - } else { - values.forEach(value -> joiner.add(formatQueryComponent(name, value))); - } - }); - String query = joiner.toString(); - return query.isEmpty() ? null : query; - } - - /** - * Formats a single query component using {@code name=value} semantics, handling parameters that omit a value. - */ - private static String formatQueryComponent(String name, String value) { - if (value == null) { - return name; - } - return name + "=" + value; - } -} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java index 59ab92fb7cde..bf3b9a4bd77b 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java @@ -4,9 +4,7 @@ package com.azure.ai.agents.implementation.http; import com.azure.core.http.HttpClient; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpHeaders; -import com.azure.core.http.HttpMethod; import com.azure.core.http.HttpPipelineBuilder; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; @@ -27,7 +25,6 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -36,39 +33,6 @@ class HttpClientHelperTests { - private static final HttpHeaderName REQUEST_ID_HEADER = HttpHeaderName.fromString("x-request-id"); - private static final HttpHeaderName CUSTOM_HEADER_NAME = HttpHeaderName.fromString("custom-header"); - private static final HttpHeaderName X_TEST_HEADER = HttpHeaderName.fromString("X-Test"); - private static final HttpHeaderName X_MULTI_HEADER = HttpHeaderName.fromString("X-Multi"); - - @Test - void executeMapsRequestAndResponse() { - RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 201, - new HttpHeaders().set(REQUEST_ID_HEADER, "req-123").set(CUSTOM_HEADER_NAME, "custom-value"), "pong")); - com.openai.core.http.HttpClient openAiClient - = HttpClientHelper.mapToOpenAIHttpClient(new HttpPipelineBuilder().httpClient(recordingClient).build()); - - com.openai.core.http.HttpRequest openAiRequest = createOpenAiRequest(); - - try (com.openai.core.http.HttpResponse response = openAiClient.execute(openAiRequest)) { - HttpRequest sentRequest = recordingClient.getLastRequest(); - assertNotNull(sentRequest, "Azure HttpClient should receive a request"); - assertEquals(HttpMethod.POST, sentRequest.getHttpMethod()); - assertEquals("https://example.com/path/segment?q=a%20b", sentRequest.getUrl().toString()); - assertEquals("alpha", sentRequest.getHeaders().getValue(X_TEST_HEADER)); - assertArrayEquals(new String[] { "first", "second" }, sentRequest.getHeaders().getValues(X_MULTI_HEADER)); - assertEquals("text/plain", sentRequest.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE)); - assertEquals("payload", new String(sentRequest.getBodyAsBinaryData().toBytes(), StandardCharsets.UTF_8)); - - assertEquals(201, response.statusCode()); - assertEquals("req-123", response.requestId().orElseThrow(() -> new AssertionError("Missing request id"))); - assertEquals("custom-value", response.headers().values("custom-header").get(0)); - assertEquals("pong", new String(readAllBytes(response.body()), StandardCharsets.UTF_8)); - } catch (Exception e) { - fail("Exception thrown while reading response", e); - } - } - @Test void executeAsyncCompletesSuccessfully() { RecordingHttpClient recordingClient diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 52515ee47e88..24f3793a26a4 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -10,6 +10,8 @@ ### Other Changes +- Updated version of `openai` client library to `4.14.0` + ## 1.0.0-beta.3 (2025-11-12) ### Features Added diff --git a/sdk/ai/azure-ai-projects/pom.xml b/sdk/ai/azure-ai-projects/pom.xml index b58668f4d6b9..c7647d97cdb1 100644 --- a/sdk/ai/azure-ai-projects/pom.xml +++ b/sdk/ai/azure-ai-projects/pom.xml @@ -77,7 +77,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. com.openai openai-java - 4.6.1 + 4.14.0 @@ -106,7 +106,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. - com.openai:openai-java:[4.6.1] + com.openai:openai-java:[4.14.0] diff --git a/sdk/openai/azure-ai-openai-stainless/pom.xml b/sdk/openai/azure-ai-openai-stainless/pom.xml index c5ef78dc4142..45eb20131aab 100644 --- a/sdk/openai/azure-ai-openai-stainless/pom.xml +++ b/sdk/openai/azure-ai-openai-stainless/pom.xml @@ -57,7 +57,7 @@ com.openai openai-java - 4.6.1 + 4.14.0 @@ -132,7 +132,7 @@ - com.openai:openai-java:[4.6.1] + com.openai:openai-java:[4.14.0]