From 89496d3c3b0cf9f87491637ce168ddba783637e5 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Tue, 2 Dec 2025 12:56:15 +0100 Subject: [PATCH 01/16] Porting over changes from private repo --- .../azure/ai/agents/AgentsClientBuilder.java | 62 +++++- .../http/AzureHttpResponseAdapter.java | 73 +++++++ .../implementation/http/HttpClientHelper.java | 191 ++++++++++++++++++ .../http/OpenAiRequestUrlBuilder.java | 133 ++++++++++++ .../http/PolicyDecoratingHttpClient.java | 65 ++++++ .../http/HttpClientHelperTests.java | 176 ++++++++++++++++ .../http/PolicyDecoratingHttpClientTests.java | 130 ++++++++++++ 7 files changed, 825 insertions(+), 5 deletions(-) create mode 100644 sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java create mode 100644 sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java create mode 100644 sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java create mode 100644 sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java create mode 100644 sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java create mode 100644 sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index 3eb05608a18f..3f0fcee37d0b 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -5,6 +5,8 @@ import com.azure.ai.agents.implementation.AgentsClientImpl; import com.azure.ai.agents.implementation.TokenUtils; +import com.azure.ai.agents.implementation.http.HttpClientHelper; +import com.azure.ai.agents.implementation.http.PolicyDecoratingHttpClient; import com.azure.core.annotation.Generated; import com.azure.core.annotation.ServiceClientBuilder; import com.azure.core.client.traits.ConfigurationTrait; @@ -43,6 +45,7 @@ import com.openai.credential.BearerTokenCredential; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -328,7 +331,12 @@ private HttpPipeline createHttpPipeline() { * @return an instance of ConversationsAsyncClient. */ public ConversationsAsyncClient buildConversationsAsyncClient() { - return new ConversationsAsyncClient(getOpenAIAsyncClientBuilder().build()); + HttpClient decoratedHttpClient = getOpenAIHttpClient(); + return new ConversationsAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { + if (decoratedHttpClient != null) { + optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + } + })); } /** @@ -337,7 +345,12 @@ public ConversationsAsyncClient buildConversationsAsyncClient() { * @return an instance of ConversationsClient. */ public ConversationsClient buildConversationsClient() { - return new ConversationsClient(getOpenAIClientBuilder().build()); + HttpClient decoratedHttpClient = getOpenAIHttpClient(); + return new ConversationsClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { + if (decoratedHttpClient != null) { + optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + } + })); } /** @@ -346,8 +359,12 @@ public ConversationsClient buildConversationsClient() { * @return an instance of ResponsesClient */ public ResponsesClient buildResponsesClient() { - OpenAIOkHttpClient.Builder builder = getOpenAIClientBuilder(); - return new ResponsesClient(builder.build()); + HttpClient decoratedHttpClient = getOpenAIHttpClient(); + return new ResponsesClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { + if (decoratedHttpClient != null) { + optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + } + })); } /** @@ -356,7 +373,12 @@ public ResponsesClient buildResponsesClient() { * @return an instance of ResponsesAsyncClient */ public ResponsesAsyncClient buildResponsesAsyncClient() { - return new ResponsesAsyncClient(getOpenAIAsyncClientBuilder().build()); + HttpClient decoratedHttpClient = getOpenAIHttpClient(); + return new ResponsesAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { + if (decoratedHttpClient != null) { + optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + } + })); } private OpenAIOkHttpClient.Builder getOpenAIClientBuilder() { @@ -396,6 +418,36 @@ private String getUserAgent() { return UserAgentUtil.toUserAgentString(applicationId, sdkName, sdkVersion, configuration); } + private HttpClient getOpenAIHttpClient() { + if (this.httpClient == null) { + return null; + } + List orderedPolicies = getOrderedCustomPolicies(); + if (orderedPolicies.isEmpty()) { + return this.httpClient; + } + return new PolicyDecoratingHttpClient(this.httpClient, orderedPolicies); + } + + private List getOrderedCustomPolicies() { + if (this.pipelinePolicies.isEmpty()) { + return Collections.emptyList(); + } + List orderedPolicies = new ArrayList<>(); + this.pipelinePolicies.stream() + .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_CALL) + .forEach(orderedPolicies::add); + this.pipelinePolicies.stream() + .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_RETRY) + .forEach(orderedPolicies::add); + return orderedPolicies; + } + + private static HttpPipelinePosition pipelinePosition(HttpPipelinePolicy policy) { + HttpPipelinePosition position = policy.getPipelinePosition(); + return position == null ? HttpPipelinePosition.PER_RETRY : position; + } + private static final ClientLogger LOGGER = new ClientLogger(AgentsClientBuilder.class); /** diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java new file mode 100644 index 000000000000..7160c634e152 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.implementation.http; + +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.logging.ClientLogger; +import com.openai.core.http.Headers; +import com.openai.core.http.HttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +/** + * Adapter that exposes an Azure {@link com.azure.core.http.HttpResponse} as an OpenAI {@link HttpResponse}. This keeps + * the translation logic encapsulated so response handling elsewhere can remain framework agnostic. + */ +final class AzureHttpResponseAdapter implements HttpResponse { + + private static final ClientLogger LOGGER = new ClientLogger(AzureHttpResponseAdapter.class); + + private final com.azure.core.http.HttpResponse azureResponse; + private final Headers headers; + private final InputStream bodyStream; + + /** + * Creates a new adapter instance for the provided Azure response. + * + * @param azureResponse Response returned by the Azure pipeline. + */ + AzureHttpResponseAdapter(com.azure.core.http.HttpResponse azureResponse) { + this.azureResponse = azureResponse; + this.headers = toOpenAiHeaders(azureResponse.getHeaders()); + this.bodyStream = azureResponse.getBodyAsBinaryData().toStream(); + } + + @Override + public int statusCode() { + return azureResponse.getStatusCode(); + } + + @Override + public Headers headers() { + return headers; + } + + @Override + public InputStream body() { + return bodyStream; + } + + @Override + public void close() { + try { + bodyStream.close(); + } catch (IOException ex) { + throw LOGGER.logExceptionAsWarning(new UncheckedIOException("Failed to close response body stream", ex)); + } + } + + /** + * Copies headers from the Azure response into the immutable OpenAI {@link Headers} collection. + */ + private static Headers toOpenAiHeaders(HttpHeaders httpHeaders) { + Headers.Builder builder = Headers.builder(); + for (HttpHeader header : httpHeaders) { + builder.put(header.getName(), header.getValuesList()); + } + return builder.build(); + } +} 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 new file mode 100644 index 000000000000..e49ea086dff2 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/HttpClientHelper.java @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.implementation.http; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; +import com.openai.core.RequestOptions; +import com.openai.core.http.Headers; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Utility entry point that adapts an Azure {@link com.azure.core.http.HttpClient} so it can be consumed by + * the OpenAI SDK generated clients. The helper performs request/response translation so that existing Azure + * pipelines, diagnostics, and retry policies can be reused without exposing the Azure HTTP primitives to + * callers that only understand the OpenAI surface area. + */ +public final class HttpClientHelper { + + private static final ClientLogger LOGGER = new ClientLogger(HttpClientHelper.class); + + private HttpClientHelper() { + } + + /** + * Wraps the given Azure {@link com.azure.core.http.HttpClient} with an implementation of the OpenAI + * {@link com.openai.core.http.HttpClient} interface. All requests and responses are converted on the fly. + * + * @param azureHttpClient The Azure HTTP client that should execute requests. + * @return A bridge client that honors the OpenAI interface but delegates execution to the Azure pipeline. + */ + public static com.openai.core.http.HttpClient httpClientMapper(com.azure.core.http.HttpClient azureHttpClient) { + return new HttpClientWrapper(azureHttpClient); + } + + private static final class HttpClientWrapper implements com.openai.core.http.HttpClient { + + private static final HttpHeaderName CONTENT_TYPE = HttpHeaderName.CONTENT_TYPE; + + private final com.azure.core.http.HttpClient azureHttpClient; + + private HttpClientWrapper(com.azure.core.http.HttpClient azureHttpClient) { + this.azureHttpClient = Objects.requireNonNull(azureHttpClient, "'azureHttpClient' cannot be null."); + } + + @Override + public void close() { + // no-op + } + + @Override + public HttpResponse execute(HttpRequest request) { + return execute(request, RequestOptions.none()); + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(requestOptions, "requestOptions"); + + com.azure.core.http.HttpRequest azureRequest = buildAzureRequest(request); + com.azure.core.http.HttpResponse azureResponse = this.azureHttpClient.sendSync(azureRequest, Context.NONE); + return new AzureHttpResponseAdapter(azureResponse); + } + + @Override + public CompletableFuture executeAsync(HttpRequest request) { + return executeAsync(request, RequestOptions.none()); + } + + @Override + public CompletableFuture executeAsync(HttpRequest request, RequestOptions requestOptions) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(requestOptions, "requestOptions"); + + final com.azure.core.http.HttpRequest azureRequest; + try { + azureRequest = buildAzureRequest(request); + } catch (RuntimeException runtimeException) { + return failedFuture(runtimeException); + } + + return this.azureHttpClient.send(azureRequest) + .map(AzureHttpResponseAdapter::new) + .map(response -> (HttpResponse) response) + .toFuture(); + } + + /** + * 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) { + HttpRequestBody requestBody = request.body(); + String contentType = requestBody != null ? requestBody.contentType() : null; + BinaryData bodyData = null; + + if (requestBody != null) { + try { + bodyData = toBinaryData(requestBody); + } finally { + closeQuietly(requestBody); + } + } + + HttpHeaders headers = toAzureHeaders(request.headers()); + if (!CoreUtils.isNullOrEmpty(contentType) && headers.getValue(CONTENT_TYPE) == null) { + headers.set(CONTENT_TYPE, contentType); + } + + com.azure.core.http.HttpRequest azureRequest + = new com.azure.core.http.HttpRequest(com.azure.core.http.HttpMethod.valueOf(request.method().name()), + OpenAiRequestUrlBuilder.buildUrl(request), headers); + + if (bodyData != null) { + azureRequest.setBody(bodyData); + } + + return azureRequest; + } + + /** + * Copies OpenAI headers into an {@link HttpHeaders} instance so the Azure pipeline can process them. + */ + private static HttpHeaders toAzureHeaders(Headers sourceHeaders) { + HttpHeaders target = new HttpHeaders(); + sourceHeaders.names().forEach(name -> { + List values = sourceHeaders.values(name); + HttpHeaderName headerName = HttpHeaderName.fromString(name); + if (values.isEmpty()) { + target.set(headerName, ""); + } else { + target.set(headerName, values); + } + }); + return target; + } + + /** + * Buffers the OpenAI {@link HttpRequestBody} into {@link BinaryData} so it can be attached to the Azure + * request. The body is consumed exactly once and closed afterwards. + */ + private static BinaryData toBinaryData(HttpRequestBody requestBody) { + if (requestBody == null) { + return null; + } + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + requestBody.writeTo(outputStream); + return BinaryData.fromBytes(outputStream.toByteArray()); + } catch (IOException e) { + throw LOGGER.logExceptionAsError(new UncheckedIOException("Failed to buffer request body", e)); + } + } + + /** + * Closes the OpenAI request body while suppressing any exceptions (to avoid masking the root cause). + */ + private static void closeQuietly(HttpRequestBody body) { + if (body == null) { + return; + } + try { + body.close(); + } catch (Exception ignored) { + // no-op + } + } + + /** + * Creates a failed {@link CompletableFuture} for callers that require a future even when conversion fails. + */ + private static CompletableFuture failedFuture(Throwable throwable) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(throwable); + return future; + } + } +} 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 new file mode 100644 index 000000000000..8663565939a9 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java @@ -0,0 +1,133 @@ +// 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. + */ +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. + * + * @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", 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/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java new file mode 100644 index 000000000000..eccbd8c084c5 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.implementation.http; + +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.util.Context; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Objects; + +/** + * Lightweight {@link HttpClient} decorator that inserts the supplied {@link HttpPipelinePolicy policies} in front of + * the provided delegate client. This makes it possible to reuse the Azure pipeline policy system with HTTP clients that + * originate outside of the Azure SDK (e.g. OpenAI generated clients). + */ +public final class PolicyDecoratingHttpClient implements HttpClient { + + private final HttpClient delegate; + private final HttpPipeline pipeline; + + /** + * Creates a new decorating client. + * + * @param delegate Underlying HTTP client that performs the actual network I/O. + * @param policies Policies that should run before the request reaches the delegate. + */ + public PolicyDecoratingHttpClient(HttpClient delegate, List policies) { + this.delegate = Objects.requireNonNull(delegate, "delegate cannot be null"); + Objects.requireNonNull(policies, "policies cannot be null"); + + HttpPipelineBuilder builder = new HttpPipelineBuilder().httpClient(this.delegate); + if (!policies.isEmpty()) { + builder.policies(policies.toArray(new HttpPipelinePolicy[0])); + } + this.pipeline = builder.build(); + } + + @Override + /** + * Sends the request using the decorated pipeline without a custom {@link Context}. + */ + public Mono send(HttpRequest request) { + return pipeline.send(request); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return pipeline.send(request, context); + } + + /** + * Synchronously sends the request by blocking on the reactive pipeline. Intended for compatibility with + * libraries that lack asynchronous plumbing. + */ + public HttpResponse sendSync(HttpRequest request, Context context) { + return send(request, context).block(); + } +} 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 new file mode 100644 index 000000000000..72c7841dd285 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/HttpClientHelperTests.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Context; +import com.openai.core.http.HttpRequestBody; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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; + +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() throws Exception { + 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.httpClientMapper(recordingClient); + + 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)); + } + } + + @Test + void executeAsyncCompletesSuccessfully() throws Exception { + RecordingHttpClient recordingClient + = new RecordingHttpClient(request -> createMockResponse(request, 204, new HttpHeaders(), "")); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + + com.openai.core.http.HttpRequest openAiRequest = createOpenAiRequest(); + + CompletableFuture future = openAiClient.executeAsync(openAiRequest); + try (com.openai.core.http.HttpResponse response = future.join()) { + assertEquals(204, response.statusCode()); + } + assertEquals(1, recordingClient.getSendCount()); + } + + private static com.openai.core.http.HttpRequest createOpenAiRequest() { + return com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.POST) + .baseUrl("https://example.com") + .addPathSegment("path") + .addPathSegment("segment") + .putHeader("X-Test", "alpha") + .putHeaders("X-Multi", Arrays.asList("first", "second")) + .putQueryParam("q", "a b") + .body(new TestHttpRequestBody("payload", "text/plain")) + .build(); + } + + private static MockHttpResponse createMockResponse(HttpRequest request, int statusCode, HttpHeaders headers, + String body) { + byte[] bytes = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8); + return new MockHttpResponse(request, statusCode, headers, bytes); + } + + private static byte[] readAllBytes(InputStream stream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int read; + while ((read = stream.read(chunk)) != -1) { + buffer.write(chunk, 0, read); + } + return buffer.toByteArray(); + } + + private static final class RecordingHttpClient implements HttpClient { + private final Function responseFactory; + private HttpRequest lastRequest; + private int sendCount; + + private RecordingHttpClient(Function responseFactory) { + this.responseFactory = responseFactory; + } + + @Override + public Mono send(HttpRequest request) { + this.lastRequest = request; + this.sendCount++; + return Mono.just(responseFactory.apply(request)); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return send(request); + } + + HttpRequest getLastRequest() { + return lastRequest; + } + + int getSendCount() { + return sendCount; + } + } + + private static final class TestHttpRequestBody implements HttpRequestBody { + private final byte[] content; + private final String contentType; + + private TestHttpRequestBody(String content, String contentType) { + this.content = content.getBytes(StandardCharsets.UTF_8); + this.contentType = contentType; + } + + @Override + public void writeTo(OutputStream outputStream) { + try { + outputStream.write(content); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public long contentLength() { + return content.length; + } + + @Override + public boolean repeatable() { + return true; + } + + @Override + public void close() { + // no-op + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java new file mode 100644 index 000000000000..e014322e99e4 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelinePosition; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Context; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PolicyDecoratingHttpClientTests { + + private static final HttpHeaderName PER_CALL_HEADER = HttpHeaderName.fromString("X-Per-Call"); + private static final HttpHeaderName PER_RETRY_HEADER = HttpHeaderName.fromString("X-Per-Retry"); + + @Test + void policiesMutateRequestBeforeDelegation() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + HttpPipelinePolicy perCallPolicy + = new HeaderAppendingPolicy(PER_CALL_HEADER, "one", HttpPipelinePosition.PER_CALL); + HttpPipelinePolicy perRetryPolicy + = new HeaderAppendingPolicy(PER_RETRY_HEADER, "two", HttpPipelinePosition.PER_RETRY); + + PolicyDecoratingHttpClient client + = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + client.sendSync(request, Context.NONE); + + HttpRequest sentRequest = recordingClient.getLastRequest(); + assertNotNull(sentRequest); + HttpHeaders headers = sentRequest.getHeaders(); + assertEquals("one", headers.getValue(PER_CALL_HEADER)); + assertEquals("two", headers.getValue(PER_RETRY_HEADER)); + assertEquals(1, recordingClient.getSendCount()); + } + + @Test + void nullArgumentsAreRejected() { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + HttpPipelinePolicy noopPolicy + = new HeaderAppendingPolicy(HttpHeaderName.fromString("X-Test"), "value", HttpPipelinePosition.PER_CALL); + + assertThrows(NullPointerException.class, + () -> new PolicyDecoratingHttpClient(null, Collections.singletonList(noopPolicy))); + assertThrows(NullPointerException.class, () -> new PolicyDecoratingHttpClient(recordingClient, null)); + } + + @Test + void sendDelegatesToUnderlyingClient() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(recordingClient, Collections.emptyList()); + + HttpRequest request = new HttpRequest(HttpMethod.POST, URI.create("https://example.com").toURL()); + HttpResponse response = client.sendSync(request, Context.NONE); + + assertNotNull(response); + assertEquals(1, recordingClient.getSendCount()); + assertEquals(request, recordingClient.getLastRequest()); + } + + private static final class RecordingHttpClient implements HttpClient { + private HttpRequest lastRequest; + private final AtomicInteger sendCount = new AtomicInteger(); + + @Override + public Mono send(HttpRequest request) { + this.lastRequest = request; + this.sendCount.incrementAndGet(); + return Mono.just(new MockHttpResponse(request, 200, "ok".getBytes(StandardCharsets.UTF_8))); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return send(request); + } + + HttpRequest getLastRequest() { + return lastRequest; + } + + int getSendCount() { + return sendCount.get(); + } + } + + private static final class HeaderAppendingPolicy implements HttpPipelinePolicy { + private final HttpHeaderName headerName; + private final String headerValue; + private final HttpPipelinePosition position; + + private HeaderAppendingPolicy(HttpHeaderName headerName, String headerValue, HttpPipelinePosition position) { + this.headerName = headerName; + this.headerValue = headerValue; + this.position = position; + } + + @Override + public HttpPipelinePosition getPipelinePosition() { + return position; + } + + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + context.getHttpRequest().getHeaders().set(headerName, headerValue); + return next.process(); + } + } +} From eca457fd82be4219c0ae49320c1b3d08cffda69d Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Tue, 2 Dec 2025 18:38:22 +0100 Subject: [PATCH 02/16] formatting --- .../java/com/azure/ai/agents/AgentsClientBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index 3f0fcee37d0b..f5cd13c5ece7 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -435,11 +435,11 @@ private List getOrderedCustomPolicies() { } List orderedPolicies = new ArrayList<>(); this.pipelinePolicies.stream() - .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_CALL) - .forEach(orderedPolicies::add); + .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_CALL) + .forEach(orderedPolicies::add); this.pipelinePolicies.stream() - .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_RETRY) - .forEach(orderedPolicies::add); + .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_RETRY) + .forEach(orderedPolicies::add); return orderedPolicies; } From 954ba7d4ae497a88469b75ad4c8c0100595f5ae1 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 11:06:34 +0100 Subject: [PATCH 03/16] Update sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/OpenAiRequestUrlBuilder.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agents/implementation/http/OpenAiRequestUrlBuilder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index 8663565939a9..b649ae7334f4 100644 --- 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 @@ -44,7 +44,9 @@ static URL buildUrl(HttpRequest request) { path, query, null); return resolved.toURL(); } catch (MalformedURLException | URISyntaxException ex) { - throw LOGGER.logThrowableAsWarning(new IllegalStateException("Failed to build Azure HTTP request URL", ex)); + throw LOGGER.logThrowableAsWarning( + new IllegalStateException( + "Failed to build Azure HTTP request URL from base: " + request.baseUrl(), ex)); } } From 4df3dba2529f3ebf98d5342bd4376652f2d5d52a Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 11:07:04 +0100 Subject: [PATCH 04/16] Update sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agents/implementation/http/PolicyDecoratingHttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java index eccbd8c084c5..18b9d1b5bf85 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java @@ -42,10 +42,10 @@ public PolicyDecoratingHttpClient(HttpClient delegate, List this.pipeline = builder.build(); } - @Override /** * Sends the request using the decorated pipeline without a custom {@link Context}. */ + @Override public Mono send(HttpRequest request) { return pipeline.send(request); } From 385e9b55564ce44da827dd6bfb6a30b043444a0d Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 11:16:42 +0100 Subject: [PATCH 05/16] Formatting mismatch --- .../agents/implementation/http/OpenAiRequestUrlBuilder.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index b649ae7334f4..5ae6eee03b8b 100644 --- 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 @@ -44,9 +44,8 @@ static URL buildUrl(HttpRequest request) { 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)); + throw LOGGER.logThrowableAsWarning(new IllegalStateException( + "Failed to build Azure HTTP request URL from base: " + request.baseUrl(), ex)); } } From 0957fd2e9877be7ba9bda122e876dbb76630f1df Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:23:35 +0100 Subject: [PATCH 06/16] Add async and error propagation test coverage for PolicyDecoratingHttpClient (#47429) * Initial plan * Add async and error propagation test coverage for PolicyDecoratingHttpClient Co-authored-by: jpalvarezl <11056031+jpalvarezl@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jpalvarezl <11056031+jpalvarezl@users.noreply.github.com> Co-authored-by: Jose Alvarez --- .../http/PolicyDecoratingHttpClientTests.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java index e014322e99e4..81052b047516 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class PolicyDecoratingHttpClientTests { @@ -80,6 +81,114 @@ void sendDelegatesToUnderlyingClient() throws Exception { assertEquals(request, recordingClient.getLastRequest()); } + @Test + void asyncSendAppliesPolicies() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + HttpPipelinePolicy perCallPolicy + = new HeaderAppendingPolicy(PER_CALL_HEADER, "async-one", HttpPipelinePosition.PER_CALL); + HttpPipelinePolicy perRetryPolicy + = new HeaderAppendingPolicy(PER_RETRY_HEADER, "async-two", HttpPipelinePosition.PER_RETRY); + + PolicyDecoratingHttpClient client + = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + HttpResponse response = client.send(request).block(); + + assertNotNull(response); + HttpRequest sentRequest = recordingClient.getLastRequest(); + assertNotNull(sentRequest); + HttpHeaders headers = sentRequest.getHeaders(); + assertEquals("async-one", headers.getValue(PER_CALL_HEADER)); + assertEquals("async-two", headers.getValue(PER_RETRY_HEADER)); + assertEquals(1, recordingClient.getSendCount()); + } + + @Test + void asyncSendWithContextAppliesPolicies() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + HttpPipelinePolicy perCallPolicy + = new HeaderAppendingPolicy(PER_CALL_HEADER, "context-one", HttpPipelinePosition.PER_CALL); + HttpPipelinePolicy perRetryPolicy + = new HeaderAppendingPolicy(PER_RETRY_HEADER, "context-two", HttpPipelinePosition.PER_RETRY); + + PolicyDecoratingHttpClient client + = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + HttpResponse response = client.send(request, Context.NONE).block(); + + assertNotNull(response); + HttpRequest sentRequest = recordingClient.getLastRequest(); + assertNotNull(sentRequest); + HttpHeaders headers = sentRequest.getHeaders(); + assertEquals("context-one", headers.getValue(PER_CALL_HEADER)); + assertEquals("context-two", headers.getValue(PER_RETRY_HEADER)); + assertEquals(1, recordingClient.getSendCount()); + } + + @Test + void policyErrorPropagatesInAsyncSend() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + RuntimeException policyException = new RuntimeException("Policy error"); + HttpPipelinePolicy failingPolicy = new FailingPolicy(policyException); + + PolicyDecoratingHttpClient client + = new PolicyDecoratingHttpClient(recordingClient, Collections.singletonList(failingPolicy)); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.send(request).block()); + assertEquals("Policy error", thrown.getMessage()); + assertEquals(0, recordingClient.getSendCount()); + } + + @Test + void underlyingClientErrorPropagatesInAsyncSend() throws Exception { + RuntimeException clientException = new RuntimeException("Client error"); + FailingHttpClient failingClient = new FailingHttpClient(clientException); + + PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(failingClient, Collections.emptyList()); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.send(request).block()); + assertEquals("Client error", thrown.getMessage()); + assertTrue(failingClient.wasCalled()); + } + + @Test + void policyErrorPropagatesInSyncSend() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(); + RuntimeException policyException = new RuntimeException("Sync policy error"); + HttpPipelinePolicy failingPolicy = new FailingPolicy(policyException); + + PolicyDecoratingHttpClient client + = new PolicyDecoratingHttpClient(recordingClient, Collections.singletonList(failingPolicy)); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.sendSync(request, Context.NONE)); + assertEquals("Sync policy error", thrown.getMessage()); + assertEquals(0, recordingClient.getSendCount()); + } + + @Test + void underlyingClientErrorPropagatesInSyncSend() throws Exception { + RuntimeException clientException = new RuntimeException("Sync client error"); + FailingHttpClient failingClient = new FailingHttpClient(clientException); + + PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(failingClient, Collections.emptyList()); + + HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.sendSync(request, Context.NONE)); + assertEquals("Sync client error", thrown.getMessage()); + assertTrue(failingClient.wasCalled()); + } + private static final class RecordingHttpClient implements HttpClient { private HttpRequest lastRequest; private final AtomicInteger sendCount = new AtomicInteger(); @@ -127,4 +236,46 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN return next.process(); } } + + private static final class FailingPolicy implements HttpPipelinePolicy { + private final RuntimeException exception; + + private FailingPolicy(RuntimeException exception) { + this.exception = exception; + } + + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + return Mono.error(exception); + } + + @Override + public HttpPipelinePosition getPipelinePosition() { + return HttpPipelinePosition.PER_CALL; + } + } + + private static final class FailingHttpClient implements HttpClient { + private final RuntimeException exception; + private boolean called = false; + + private FailingHttpClient(RuntimeException exception) { + this.exception = exception; + } + + @Override + public Mono send(HttpRequest request) { + this.called = true; + return Mono.error(exception); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return send(request); + } + + boolean wasCalled() { + return called; + } + } } From d113cc27294852970b6b9c237e679562dea518e9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:36:02 +0100 Subject: [PATCH 07/16] Add error handling tests for HttpClientHelper (#47428) * Initial plan * Add error handling tests for HttpClientHelper Added comprehensive error handling test cases including: - Null request body handling - IOException during body buffering - Malformed URLs - Async execution failures Co-authored-by: jpalvarezl <11056031+jpalvarezl@users.noreply.github.com> * Address code review feedback on error handling tests - Simplify FailingHttpRequestBody to directly throw UncheckedIOException - Improve assertion clarity in body buffering test - Remove fragile message checking in malformed URL test Co-authored-by: jpalvarezl <11056031+jpalvarezl@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jpalvarezl <11056031+jpalvarezl@users.noreply.github.com> Co-authored-by: Jose Alvarez --- .../http/HttpClientHelperTests.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) 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 72c7841dd285..14e43f692337 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 @@ -28,6 +28,8 @@ 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; +import static org.junit.jupiter.api.Assertions.assertTrue; class HttpClientHelperTests { @@ -76,6 +78,108 @@ void executeAsyncCompletesSuccessfully() throws Exception { assertEquals(1, recordingClient.getSendCount()); } + @Test + void executeWithNullRequestBodySucceeds() throws Exception { + RecordingHttpClient recordingClient = new RecordingHttpClient(request -> { + // Verify the request has no body (or empty body) + com.azure.core.util.BinaryData bodyData = request.getBodyAsBinaryData(); + if (bodyData != null) { + assertEquals(0, bodyData.toBytes().length); + } + return createMockResponse(request, 200, new HttpHeaders(), "success"); + }); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + + com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.GET) + .baseUrl("https://example.com") + .addPathSegment("test") + .build(); + + try (com.openai.core.http.HttpResponse response = openAiClient.execute(openAiRequest)) { + assertEquals(200, response.statusCode()); + assertEquals("success", new String(readAllBytes(response.body()), StandardCharsets.UTF_8)); + } + } + + @Test + void executeThrowsUncheckedIOExceptionOnBodyBufferingFailure() { + RecordingHttpClient recordingClient + = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + + com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.POST) + .baseUrl("https://example.com") + .body(new FailingHttpRequestBody()) + .build(); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + openAiClient.execute(openAiRequest); + }); + // Verify the error is related to body buffering failure + boolean hasBufferMessage = exception.getMessage() != null && exception.getMessage().contains("buffer"); + boolean hasIOCause = exception.getCause() instanceof IOException; + assertTrue(hasBufferMessage || hasIOCause, "Expected error related to buffer failure, got: " + exception); + } + + @Test + void executeThrowsExceptionOnMalformedUrl() { + RecordingHttpClient recordingClient + = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + + com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.GET) + .baseUrl("not-a-valid-url") + .build(); + + // Malformed URLs should throw an exception (typically IllegalArgumentException or IllegalStateException) + assertThrows(RuntimeException.class, () -> { + openAiClient.execute(openAiRequest); + }); + } + + @Test + void executeAsyncPropagatesRequestBuildingErrors() { + RecordingHttpClient recordingClient + = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + + com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.POST) + .baseUrl("https://example.com") + .body(new FailingHttpRequestBody()) + .build(); + + CompletableFuture future = openAiClient.executeAsync(openAiRequest); + + Exception exception = assertThrows(Exception.class, future::join); + Throwable cause = exception.getCause(); + assertNotNull(cause, "Expected a cause for the exception"); + // The error should be related to request building/buffering failure + assertTrue(cause instanceof RuntimeException, "Expected RuntimeException, got: " + cause.getClass().getName()); + } + + @Test + void executeAsyncPropagatesHttpClientFailures() { + FailingHttpClient failingClient = new FailingHttpClient(new RuntimeException("Network error")); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(failingClient); + + com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() + .method(com.openai.core.http.HttpMethod.GET) + .baseUrl("https://example.com") + .build(); + + CompletableFuture future = openAiClient.executeAsync(openAiRequest); + + Exception exception = assertThrows(Exception.class, future::join); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertTrue(cause instanceof RuntimeException); + assertEquals("Network error", cause.getMessage()); + } + private static com.openai.core.http.HttpRequest createOpenAiRequest() { return com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.POST) @@ -173,4 +277,50 @@ public void close() { // no-op } } + + private static final class FailingHttpRequestBody implements HttpRequestBody { + @Override + public void writeTo(OutputStream outputStream) { + // Simulate an I/O failure during body write + throw new UncheckedIOException(new IOException("Simulated I/O failure during body write")); + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public long contentLength() { + return -1; + } + + @Override + public boolean repeatable() { + return false; + } + + @Override + public void close() { + // no-op + } + } + + private static final class FailingHttpClient implements HttpClient { + private final RuntimeException error; + + private FailingHttpClient(RuntimeException error) { + this.error = error; + } + + @Override + public Mono send(HttpRequest request) { + return Mono.error(error); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return send(request); + } + } } From 224e3fb681d3394ec79d8656dc401ac3654a46c2 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 12:38:54 +0100 Subject: [PATCH 08/16] Merged and improved assertions --- .../implementation/http/HttpClientHelperTests.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 14e43f692337..594ddae6374e 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 @@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; class HttpClientHelperTests { @@ -39,7 +40,7 @@ class HttpClientHelperTests { private static final HttpHeaderName X_MULTI_HEADER = HttpHeaderName.fromString("X-Multi"); @Test - void executeMapsRequestAndResponse() throws Exception { + 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.httpClientMapper(recordingClient); @@ -60,11 +61,13 @@ void executeMapsRequestAndResponse() throws Exception { 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() throws Exception { + void executeAsyncCompletesSuccessfully() { RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 204, new HttpHeaders(), "")); com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); @@ -74,6 +77,8 @@ void executeAsyncCompletesSuccessfully() throws Exception { CompletableFuture future = openAiClient.executeAsync(openAiRequest); try (com.openai.core.http.HttpResponse response = future.join()) { assertEquals(204, response.statusCode()); + } catch (Exception e) { + fail("Exception thrown while reading response", e); } assertEquals(1, recordingClient.getSendCount()); } From 672a6df7c18b687a71388c238decee28ce891d78 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 12:50:33 +0100 Subject: [PATCH 09/16] More PR feedback --- .../ai/agents/implementation/http/AzureHttpResponseAdapter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java index 7160c634e152..5f243a2ed6fa 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java @@ -54,6 +54,7 @@ public InputStream body() { @Override public void close() { try { + azureResponse.close(); bodyStream.close(); } catch (IOException ex) { throw LOGGER.logExceptionAsWarning(new UncheckedIOException("Failed to close response body stream", ex)); From d4505e696074cf593b291beb73ed8068a69cb6e9 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 13:01:51 +0100 Subject: [PATCH 10/16] More PR feedback --- .../azure/ai/agents/AgentsClientBuilder.java | 27 ++++++++----------- .../implementation/http/HttpClientHelper.java | 8 +++--- .../http/HttpClientHelperTests.java | 14 +++++----- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index f5cd13c5ece7..0dcb35c1f5ef 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -45,10 +45,11 @@ import com.openai.credential.BearerTokenCredential; import java.time.Duration; import java.util.ArrayList; -import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * A builder for creating a new instance of the AgentsClient type. @@ -334,7 +335,7 @@ public ConversationsAsyncClient buildConversationsAsyncClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); return new ConversationsAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); } })); } @@ -348,7 +349,7 @@ public ConversationsClient buildConversationsClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); return new ConversationsClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); } })); } @@ -362,7 +363,7 @@ public ResponsesClient buildResponsesClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); return new ResponsesClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); } })); } @@ -376,7 +377,7 @@ public ResponsesAsyncClient buildResponsesAsyncClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); return new ResponsesAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.httpClientMapper(decoratedHttpClient)); + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); } })); } @@ -430,17 +431,11 @@ private HttpClient getOpenAIHttpClient() { } private List getOrderedCustomPolicies() { - if (this.pipelinePolicies.isEmpty()) { - return Collections.emptyList(); - } - List orderedPolicies = new ArrayList<>(); - this.pipelinePolicies.stream() - .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_CALL) - .forEach(orderedPolicies::add); - this.pipelinePolicies.stream() - .filter(policy -> pipelinePosition(policy) == HttpPipelinePosition.PER_RETRY) - .forEach(orderedPolicies::add); - return orderedPolicies; + return this.pipelinePolicies.stream() + .sorted(Comparator.comparing(policy -> HttpPipelinePosition.PER_CALL == policy.getPipelinePosition() + ? HttpPipelinePosition.PER_CALL + : HttpPipelinePosition.PER_CALL)) + .collect(Collectors.toList()); } private static HttpPipelinePosition pipelinePosition(HttpPipelinePolicy policy) { 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 e49ea086dff2..4fcc0624738c 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 @@ -42,14 +42,12 @@ private HttpClientHelper() { * @param azureHttpClient The Azure HTTP client that should execute requests. * @return A bridge client that honors the OpenAI interface but delegates execution to the Azure pipeline. */ - public static com.openai.core.http.HttpClient httpClientMapper(com.azure.core.http.HttpClient azureHttpClient) { + public static com.openai.core.http.HttpClient mapToOpenAIHttpClient(com.azure.core.http.HttpClient azureHttpClient) { return new HttpClientWrapper(azureHttpClient); } private static final class HttpClientWrapper implements com.openai.core.http.HttpClient { - private static final HttpHeaderName CONTENT_TYPE = HttpHeaderName.CONTENT_TYPE; - private final com.azure.core.http.HttpClient azureHttpClient; private HttpClientWrapper(com.azure.core.http.HttpClient azureHttpClient) { @@ -116,8 +114,8 @@ private static com.azure.core.http.HttpRequest buildAzureRequest(HttpRequest req } HttpHeaders headers = toAzureHeaders(request.headers()); - if (!CoreUtils.isNullOrEmpty(contentType) && headers.getValue(CONTENT_TYPE) == null) { - headers.set(CONTENT_TYPE, contentType); + if (!CoreUtils.isNullOrEmpty(contentType) && headers.getValue(HttpHeaderName.CONTENT_TYPE) == null) { + headers.set(HttpHeaderName.CONTENT_TYPE, contentType); } com.azure.core.http.HttpRequest azureRequest 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 594ddae6374e..f07563b6442b 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 @@ -43,7 +43,7 @@ class HttpClientHelperTests { 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.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = createOpenAiRequest(); @@ -70,7 +70,7 @@ void executeMapsRequestAndResponse() { void executeAsyncCompletesSuccessfully() { RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 204, new HttpHeaders(), "")); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = createOpenAiRequest(); @@ -93,7 +93,7 @@ void executeWithNullRequestBodySucceeds() throws Exception { } return createMockResponse(request, 200, new HttpHeaders(), "success"); }); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.GET) @@ -111,7 +111,7 @@ void executeWithNullRequestBodySucceeds() throws Exception { void executeThrowsUncheckedIOExceptionOnBodyBufferingFailure() { RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.POST) @@ -132,7 +132,7 @@ void executeThrowsUncheckedIOExceptionOnBodyBufferingFailure() { void executeThrowsExceptionOnMalformedUrl() { RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.GET) @@ -149,7 +149,7 @@ void executeThrowsExceptionOnMalformedUrl() { void executeAsyncPropagatesRequestBuildingErrors() { RecordingHttpClient recordingClient = new RecordingHttpClient(request -> createMockResponse(request, 200, new HttpHeaders(), "")); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(recordingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(recordingClient); com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.POST) @@ -169,7 +169,7 @@ void executeAsyncPropagatesRequestBuildingErrors() { @Test void executeAsyncPropagatesHttpClientFailures() { FailingHttpClient failingClient = new FailingHttpClient(new RuntimeException("Network error")); - com.openai.core.http.HttpClient openAiClient = HttpClientHelper.httpClientMapper(failingClient); + com.openai.core.http.HttpClient openAiClient = HttpClientHelper.mapToOpenAIHttpClient(failingClient); com.openai.core.http.HttpRequest openAiRequest = com.openai.core.http.HttpRequest.builder() .method(com.openai.core.http.HttpMethod.GET) From bf085dafe91d56db55dbee5bbf5096a6c3d502f9 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 3 Dec 2025 13:15:05 +0100 Subject: [PATCH 11/16] More PR feedback --- .../java/com/azure/ai/agents/AgentsClientBuilder.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index 0dcb35c1f5ef..e7b2843fd1fc 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -423,11 +423,7 @@ private HttpClient getOpenAIHttpClient() { if (this.httpClient == null) { return null; } - List orderedPolicies = getOrderedCustomPolicies(); - if (orderedPolicies.isEmpty()) { - return this.httpClient; - } - return new PolicyDecoratingHttpClient(this.httpClient, orderedPolicies); + return new PolicyDecoratingHttpClient(this.httpClient, getOrderedCustomPolicies()); } private List getOrderedCustomPolicies() { @@ -438,11 +434,6 @@ private List getOrderedCustomPolicies() { .collect(Collectors.toList()); } - private static HttpPipelinePosition pipelinePosition(HttpPipelinePolicy policy) { - HttpPipelinePosition position = policy.getPipelinePosition(); - return position == null ? HttpPipelinePosition.PER_RETRY : position; - } - private static final ClientLogger LOGGER = new ClientLogger(AgentsClientBuilder.class); /** From 226c2c0b21835ba17c51a0cdef9423881a15e343 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Thu, 4 Dec 2025 11:35:36 +0100 Subject: [PATCH 12/16] lazy mapping of types for the AzureHttpResponseAdapter --- .../http/AzureHttpResponseAdapter.java | 17 +++-------------- .../implementation/http/HttpClientHelper.java | 3 ++- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java index 5f243a2ed6fa..e299a62a1d48 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/AzureHttpResponseAdapter.java @@ -9,9 +9,7 @@ import com.openai.core.http.Headers; import com.openai.core.http.HttpResponse; -import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; /** * Adapter that exposes an Azure {@link com.azure.core.http.HttpResponse} as an OpenAI {@link HttpResponse}. This keeps @@ -22,8 +20,6 @@ final class AzureHttpResponseAdapter implements HttpResponse { private static final ClientLogger LOGGER = new ClientLogger(AzureHttpResponseAdapter.class); private final com.azure.core.http.HttpResponse azureResponse; - private final Headers headers; - private final InputStream bodyStream; /** * Creates a new adapter instance for the provided Azure response. @@ -32,8 +28,6 @@ final class AzureHttpResponseAdapter implements HttpResponse { */ AzureHttpResponseAdapter(com.azure.core.http.HttpResponse azureResponse) { this.azureResponse = azureResponse; - this.headers = toOpenAiHeaders(azureResponse.getHeaders()); - this.bodyStream = azureResponse.getBodyAsBinaryData().toStream(); } @Override @@ -43,22 +37,17 @@ public int statusCode() { @Override public Headers headers() { - return headers; + return toOpenAiHeaders(azureResponse.getHeaders()); } @Override public InputStream body() { - return bodyStream; + return azureResponse.getBodyAsBinaryData().toStream(); } @Override public void close() { - try { - azureResponse.close(); - bodyStream.close(); - } catch (IOException ex) { - throw LOGGER.logExceptionAsWarning(new UncheckedIOException("Failed to close response body stream", ex)); - } + azureResponse.close(); } /** 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 4fcc0624738c..47eb31072c62 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 @@ -42,7 +42,8 @@ private HttpClientHelper() { * @param azureHttpClient The Azure HTTP client that should execute requests. * @return A bridge client that honors the OpenAI interface but delegates execution to the Azure pipeline. */ - public static com.openai.core.http.HttpClient mapToOpenAIHttpClient(com.azure.core.http.HttpClient azureHttpClient) { + public static com.openai.core.http.HttpClient + mapToOpenAIHttpClient(com.azure.core.http.HttpClient azureHttpClient) { return new HttpClientWrapper(azureHttpClient); } From 7c95643a1019e1fbd9cd0ce1c5462e22d1319308 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Thu, 4 Dec 2025 13:05:13 +0100 Subject: [PATCH 13/16] Simplified mapping --- .../azure/ai/agents/implementation/http/HttpClientHelper.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 47eb31072c62..09311ce4b2ba 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 @@ -93,8 +93,7 @@ public CompletableFuture executeAsync(HttpRequest request, Request } return this.azureHttpClient.send(azureRequest) - .map(AzureHttpResponseAdapter::new) - .map(response -> (HttpResponse) response) + .map(response -> (HttpResponse) new AzureHttpResponseAdapter(response)) .toFuture(); } From 867d0d67038afe27d936b56019af19c6b8c2db85 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 5 Dec 2025 16:35:10 +0100 Subject: [PATCH 14/16] Always using default httpPipeline --- .../azure/ai/agents/AgentsClientBuilder.java | 26 +------------------ .../implementation/http/HttpClientHelper.java | 2 +- .../http/PolicyDecoratingHttpClient.java | 14 ++++++---- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index e7b2843fd1fc..0cf2919337dc 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -387,12 +387,10 @@ private OpenAIOkHttpClient.Builder getOpenAIClientBuilder() { .credential( BearerTokenCredential.create(TokenUtils.getBearerTokenSupplier(this.tokenCredential, DEFAULT_SCOPES))); builder.baseUrl(this.endpoint + (this.endpoint.endsWith("/") ? "openai" : "/openai")); - builder.replaceHeaders("User-Agent", getUserAgent()); if (this.serviceVersion != null) { builder.azureServiceVersion(AzureOpenAIServiceVersion.fromString(this.serviceVersion.getVersion())); builder.azureUrlPathMode(AzureUrlPathMode.UNIFIED); } - builder.timeout(Duration.ofSeconds(30)); return builder; } @@ -401,37 +399,15 @@ private OpenAIOkHttpClientAsync.Builder getOpenAIAsyncClientBuilder() { .credential( BearerTokenCredential.create(TokenUtils.getBearerTokenSupplier(this.tokenCredential, DEFAULT_SCOPES))); builder.baseUrl(this.endpoint + (this.endpoint.endsWith("/") ? "openai" : "/openai")); - builder.replaceHeaders("User-Agent", getUserAgent()); if (this.serviceVersion != null) { builder.azureServiceVersion(AzureOpenAIServiceVersion.fromString(this.serviceVersion.getVersion())); builder.azureUrlPath(AzureUrlPathMode.UNIFIED); } - builder.timeout(Duration.ofSeconds(30)); return builder; } - private String getUserAgent() { - HttpLogOptions localHttpLogOptions = this.httpLogOptions == null ? new HttpLogOptions() : this.httpLogOptions; - ClientOptions localClientOptions = this.clientOptions == null ? new ClientOptions() : this.clientOptions; - String sdkName = PROPERTIES.getOrDefault(SDK_NAME, "UnknownName"); - String sdkVersion = PROPERTIES.getOrDefault(SDK_VERSION, "UnknownVersion"); - String applicationId = CoreUtils.getApplicationId(localClientOptions, localHttpLogOptions); - return UserAgentUtil.toUserAgentString(applicationId, sdkName, sdkVersion, configuration); - } - private HttpClient getOpenAIHttpClient() { - if (this.httpClient == null) { - return null; - } - return new PolicyDecoratingHttpClient(this.httpClient, getOrderedCustomPolicies()); - } - - private List getOrderedCustomPolicies() { - return this.pipelinePolicies.stream() - .sorted(Comparator.comparing(policy -> HttpPipelinePosition.PER_CALL == policy.getPipelinePosition() - ? HttpPipelinePosition.PER_CALL - : HttpPipelinePosition.PER_CALL)) - .collect(Collectors.toList()); + return new PolicyDecoratingHttpClient(createHttpPipeline()); } private static final ClientLogger LOGGER = new ClientLogger(AgentsClientBuilder.class); 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 09311ce4b2ba..fbcfee77a849 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 @@ -154,7 +154,7 @@ private static BinaryData toBinaryData(HttpRequestBody requestBody) { if (requestBody == null) { return null; } - + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { requestBody.writeTo(outputStream); return BinaryData.fromBytes(outputStream.toByteArray()); diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java index 18b9d1b5bf85..6e535050f056 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java @@ -22,7 +22,7 @@ */ public final class PolicyDecoratingHttpClient implements HttpClient { - private final HttpClient delegate; +// private final HttpClient delegate; private final HttpPipeline pipeline; /** @@ -31,17 +31,21 @@ public final class PolicyDecoratingHttpClient implements HttpClient { * @param delegate Underlying HTTP client that performs the actual network I/O. * @param policies Policies that should run before the request reaches the delegate. */ + @Deprecated(forRemoval = true) public PolicyDecoratingHttpClient(HttpClient delegate, List policies) { - this.delegate = Objects.requireNonNull(delegate, "delegate cannot be null"); - Objects.requireNonNull(policies, "policies cannot be null"); + Objects.requireNonNull(delegate, "delegate cannot be null"); - HttpPipelineBuilder builder = new HttpPipelineBuilder().httpClient(this.delegate); - if (!policies.isEmpty()) { + HttpPipelineBuilder builder = new HttpPipelineBuilder().httpClient(delegate); + if (policies == null || !policies.isEmpty()) { builder.policies(policies.toArray(new HttpPipelinePolicy[0])); } this.pipeline = builder.build(); } + public PolicyDecoratingHttpClient(HttpPipeline httpPipeline) { + this.pipeline = httpPipeline; + } + /** * Sends the request using the decorated pipeline without a custom {@link Context}. */ From 6bf38b72743dac97b819886eda30f8dcd4fc8c7e Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 5 Dec 2025 16:43:24 +0100 Subject: [PATCH 15/16] Using bare minimum code --- .../main/java/com/azure/ai/agents/AgentsClientBuilder.java | 6 +++++- .../ai/agents/implementation/http/HttpClientHelper.java | 2 +- .../implementation/http/PolicyDecoratingHttpClient.java | 5 +++-- .../http/PolicyDecoratingHttpClientTests.java | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index 0cf2919337dc..2dfff39f4ca3 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -19,6 +19,8 @@ import com.azure.core.http.HttpPipeline; import com.azure.core.http.HttpPipelineBuilder; import com.azure.core.http.HttpPipelinePosition; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.AddDatePolicy; import com.azure.core.http.policy.AddHeadersFromContextPolicy; import com.azure.core.http.policy.AddHeadersPolicy; @@ -43,6 +45,8 @@ import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.client.okhttp.OpenAIOkHttpClientAsync; import com.openai.credential.BearerTokenCredential; +import reactor.core.publisher.Mono; + import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; @@ -407,7 +411,7 @@ private OpenAIOkHttpClientAsync.Builder getOpenAIAsyncClientBuilder() { } private HttpClient getOpenAIHttpClient() { - return new PolicyDecoratingHttpClient(createHttpPipeline()); + return createHttpPipeline()::send; } private static final ClientLogger LOGGER = new ClientLogger(AgentsClientBuilder.class); 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 fbcfee77a849..09311ce4b2ba 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 @@ -154,7 +154,7 @@ private static BinaryData toBinaryData(HttpRequestBody requestBody) { if (requestBody == null) { return null; } - + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { requestBody.writeTo(outputStream); return BinaryData.fromBytes(outputStream.toByteArray()); diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java index 6e535050f056..180742b59dcd 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java @@ -20,9 +20,10 @@ * the provided delegate client. This makes it possible to reuse the Azure pipeline policy system with HTTP clients that * originate outside of the Azure SDK (e.g. OpenAI generated clients). */ +@Deprecated public final class PolicyDecoratingHttpClient implements HttpClient { -// private final HttpClient delegate; + // private final HttpClient delegate; private final HttpPipeline pipeline; /** @@ -31,7 +32,7 @@ public final class PolicyDecoratingHttpClient implements HttpClient { * @param delegate Underlying HTTP client that performs the actual network I/O. * @param policies Policies that should run before the request reaches the delegate. */ - @Deprecated(forRemoval = true) + @Deprecated public PolicyDecoratingHttpClient(HttpClient delegate, List policies) { Objects.requireNonNull(delegate, "delegate cannot be null"); diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java index 81052b047516..83bface8cb85 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java @@ -15,6 +15,7 @@ import com.azure.core.http.policy.HttpPipelinePolicy; import com.azure.core.test.http.MockHttpResponse; import com.azure.core.util.Context; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -29,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@Disabled class PolicyDecoratingHttpClientTests { private static final HttpHeaderName PER_CALL_HEADER = HttpHeaderName.fromString("X-Per-Call"); From 7c1b41480e63499e968908f6760833e0b57c9da4 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 5 Dec 2025 20:18:34 +0100 Subject: [PATCH 16/16] WIP: removing redundant code. Using default httpPipeline --- .../azure/ai/agents/AgentsClientBuilder.java | 38 +-- .../implementation/http/HttpClientHelper.java | 10 +- .../http/PolicyDecoratingHttpClient.java | 70 ----- .../ai/agents/ConversationsAsyncTests.java | 5 + .../http/PolicyDecoratingHttpClientTests.java | 283 ------------------ 5 files changed, 24 insertions(+), 382 deletions(-) delete mode 100644 sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java delete mode 100644 sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java index 2dfff39f4ca3..ca7a324f6a7f 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/AgentsClientBuilder.java @@ -6,7 +6,6 @@ import com.azure.ai.agents.implementation.AgentsClientImpl; import com.azure.ai.agents.implementation.TokenUtils; import com.azure.ai.agents.implementation.http.HttpClientHelper; -import com.azure.ai.agents.implementation.http.PolicyDecoratingHttpClient; import com.azure.core.annotation.Generated; import com.azure.core.annotation.ServiceClientBuilder; import com.azure.core.client.traits.ConfigurationTrait; @@ -19,8 +18,6 @@ import com.azure.core.http.HttpPipeline; import com.azure.core.http.HttpPipelineBuilder; import com.azure.core.http.HttpPipelinePosition; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.AddDatePolicy; import com.azure.core.http.policy.AddHeadersFromContextPolicy; import com.azure.core.http.policy.AddHeadersPolicy; @@ -36,7 +33,6 @@ import com.azure.core.util.ClientOptions; import com.azure.core.util.Configuration; import com.azure.core.util.CoreUtils; -import com.azure.core.util.UserAgentUtil; import com.azure.core.util.builder.ClientBuilderUtil; import com.azure.core.util.logging.ClientLogger; import com.azure.core.util.serializer.JacksonAdapter; @@ -45,15 +41,11 @@ import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.client.okhttp.OpenAIOkHttpClientAsync; import com.openai.credential.BearerTokenCredential; -import reactor.core.publisher.Mono; -import java.time.Duration; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; /** * A builder for creating a new instance of the AgentsClient type. @@ -337,11 +329,8 @@ private HttpPipeline createHttpPipeline() { */ public ConversationsAsyncClient buildConversationsAsyncClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); - return new ConversationsAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { - if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); - } - })); + return new ConversationsAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)))); } /** @@ -351,11 +340,8 @@ public ConversationsAsyncClient buildConversationsAsyncClient() { */ public ConversationsClient buildConversationsClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); - return new ConversationsClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { - if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); - } - })); + return new ConversationsClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)))); } /** @@ -365,11 +351,8 @@ public ConversationsClient buildConversationsClient() { */ public ResponsesClient buildResponsesClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); - return new ResponsesClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> { - if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); - } - })); + return new ResponsesClient(getOpenAIClientBuilder().build().withOptions(optionBuilder -> + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)))); } /** @@ -379,11 +362,8 @@ public ResponsesClient buildResponsesClient() { */ public ResponsesAsyncClient buildResponsesAsyncClient() { HttpClient decoratedHttpClient = getOpenAIHttpClient(); - return new ResponsesAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> { - if (decoratedHttpClient != null) { - optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)); - } - })); + return new ResponsesAsyncClient(getOpenAIAsyncClientBuilder().build().withOptions(optionBuilder -> + optionBuilder.httpClient(HttpClientHelper.mapToOpenAIHttpClient(decoratedHttpClient)))); } private OpenAIOkHttpClient.Builder getOpenAIClientBuilder() { @@ -395,6 +375,7 @@ private OpenAIOkHttpClient.Builder getOpenAIClientBuilder() { builder.azureServiceVersion(AzureOpenAIServiceVersion.fromString(this.serviceVersion.getVersion())); builder.azureUrlPathMode(AzureUrlPathMode.UNIFIED); } + builder.maxRetries(0); return builder; } @@ -407,6 +388,7 @@ private OpenAIOkHttpClientAsync.Builder getOpenAIAsyncClientBuilder() { builder.azureServiceVersion(AzureOpenAIServiceVersion.fromString(this.serviceVersion.getVersion())); builder.azureUrlPath(AzureUrlPathMode.UNIFIED); } + builder.maxRetries(0); return builder; } 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 09311ce4b2ba..8b3e7500cffa 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 @@ -94,7 +94,15 @@ public CompletableFuture executeAsync(HttpRequest request, Request return this.azureHttpClient.send(azureRequest) .map(response -> (HttpResponse) new AzureHttpResponseAdapter(response)) - .toFuture(); +// .onErrorMap(t -> { +// // 2 or 3 from Azure Errors, should be mapped to Stainless Error. +// // - Auth +// // - Resource not found +// // - HttpResponse Ex +// // +// // new StainlessException(t.getCause()) +// }) + .toFuture(); } /** diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java deleted file mode 100644 index 180742b59dcd..000000000000 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClient.java +++ /dev/null @@ -1,70 +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.http.HttpClient; -import com.azure.core.http.HttpPipeline; -import com.azure.core.http.HttpPipelineBuilder; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.core.util.Context; -import reactor.core.publisher.Mono; - -import java.util.List; -import java.util.Objects; - -/** - * Lightweight {@link HttpClient} decorator that inserts the supplied {@link HttpPipelinePolicy policies} in front of - * the provided delegate client. This makes it possible to reuse the Azure pipeline policy system with HTTP clients that - * originate outside of the Azure SDK (e.g. OpenAI generated clients). - */ -@Deprecated -public final class PolicyDecoratingHttpClient implements HttpClient { - - // private final HttpClient delegate; - private final HttpPipeline pipeline; - - /** - * Creates a new decorating client. - * - * @param delegate Underlying HTTP client that performs the actual network I/O. - * @param policies Policies that should run before the request reaches the delegate. - */ - @Deprecated - public PolicyDecoratingHttpClient(HttpClient delegate, List policies) { - Objects.requireNonNull(delegate, "delegate cannot be null"); - - HttpPipelineBuilder builder = new HttpPipelineBuilder().httpClient(delegate); - if (policies == null || !policies.isEmpty()) { - builder.policies(policies.toArray(new HttpPipelinePolicy[0])); - } - this.pipeline = builder.build(); - } - - public PolicyDecoratingHttpClient(HttpPipeline httpPipeline) { - this.pipeline = httpPipeline; - } - - /** - * Sends the request using the decorated pipeline without a custom {@link Context}. - */ - @Override - public Mono send(HttpRequest request) { - return pipeline.send(request); - } - - @Override - public Mono send(HttpRequest request, Context context) { - return pipeline.send(request, context); - } - - /** - * Synchronously sends the request by blocking on the reactive pipeline. Intended for compatibility with - * libraries that lack asynchronous plumbing. - */ - public HttpResponse sendSync(HttpRequest request, Context context) { - return send(request, context).block(); - } -} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ConversationsAsyncTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ConversationsAsyncTests.java index 8045fa5cf987..77139f02f4d3 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ConversationsAsyncTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ConversationsAsyncTests.java @@ -4,6 +4,8 @@ package com.azure.ai.agents; import com.azure.core.http.HttpClient; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.logging.LogLevel; import com.openai.core.JsonValue; import com.openai.models.conversations.Conversation; import com.openai.models.conversations.ConversationDeletedResource; @@ -24,6 +26,8 @@ @Disabled("Disabled for lack of recordings. Needs to be enabled on the Public Preview release.") public class ConversationsAsyncTests extends ClientTestBase { + ClientLogger LOGGER = new ClientLogger(ConversationsAsyncTests.class); + @ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS) @MethodSource("com.azure.ai.agents.TestUtils#getTestParameters") public void basicCRUDOperations(HttpClient httpClient, AgentsServiceVersion serviceVersion) @@ -35,6 +39,7 @@ public void basicCRUDOperations(HttpClient httpClient, AgentsServiceVersion serv String conversationId = createdConversation.id(); assertNotNull(conversationId); assertTrue(StringUtils.isNotBlank(conversationId)); + LOGGER.log(LogLevel.INFORMATIONAL, () -> "Create completed"); // update ConversationUpdateParams.Metadata metadata = ConversationUpdateParams.Metadata.builder() diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java deleted file mode 100644 index 83bface8cb85..000000000000 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/implementation/http/PolicyDecoratingHttpClientTests.java +++ /dev/null @@ -1,283 +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.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.HttpPipelineCallContext; -import com.azure.core.http.HttpPipelineNextPolicy; -import com.azure.core.http.HttpPipelinePosition; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.core.test.http.MockHttpResponse; -import com.azure.core.util.Context; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@Disabled -class PolicyDecoratingHttpClientTests { - - private static final HttpHeaderName PER_CALL_HEADER = HttpHeaderName.fromString("X-Per-Call"); - private static final HttpHeaderName PER_RETRY_HEADER = HttpHeaderName.fromString("X-Per-Retry"); - - @Test - void policiesMutateRequestBeforeDelegation() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - HttpPipelinePolicy perCallPolicy - = new HeaderAppendingPolicy(PER_CALL_HEADER, "one", HttpPipelinePosition.PER_CALL); - HttpPipelinePolicy perRetryPolicy - = new HeaderAppendingPolicy(PER_RETRY_HEADER, "two", HttpPipelinePosition.PER_RETRY); - - PolicyDecoratingHttpClient client - = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - client.sendSync(request, Context.NONE); - - HttpRequest sentRequest = recordingClient.getLastRequest(); - assertNotNull(sentRequest); - HttpHeaders headers = sentRequest.getHeaders(); - assertEquals("one", headers.getValue(PER_CALL_HEADER)); - assertEquals("two", headers.getValue(PER_RETRY_HEADER)); - assertEquals(1, recordingClient.getSendCount()); - } - - @Test - void nullArgumentsAreRejected() { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - HttpPipelinePolicy noopPolicy - = new HeaderAppendingPolicy(HttpHeaderName.fromString("X-Test"), "value", HttpPipelinePosition.PER_CALL); - - assertThrows(NullPointerException.class, - () -> new PolicyDecoratingHttpClient(null, Collections.singletonList(noopPolicy))); - assertThrows(NullPointerException.class, () -> new PolicyDecoratingHttpClient(recordingClient, null)); - } - - @Test - void sendDelegatesToUnderlyingClient() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(recordingClient, Collections.emptyList()); - - HttpRequest request = new HttpRequest(HttpMethod.POST, URI.create("https://example.com").toURL()); - HttpResponse response = client.sendSync(request, Context.NONE); - - assertNotNull(response); - assertEquals(1, recordingClient.getSendCount()); - assertEquals(request, recordingClient.getLastRequest()); - } - - @Test - void asyncSendAppliesPolicies() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - HttpPipelinePolicy perCallPolicy - = new HeaderAppendingPolicy(PER_CALL_HEADER, "async-one", HttpPipelinePosition.PER_CALL); - HttpPipelinePolicy perRetryPolicy - = new HeaderAppendingPolicy(PER_RETRY_HEADER, "async-two", HttpPipelinePosition.PER_RETRY); - - PolicyDecoratingHttpClient client - = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - HttpResponse response = client.send(request).block(); - - assertNotNull(response); - HttpRequest sentRequest = recordingClient.getLastRequest(); - assertNotNull(sentRequest); - HttpHeaders headers = sentRequest.getHeaders(); - assertEquals("async-one", headers.getValue(PER_CALL_HEADER)); - assertEquals("async-two", headers.getValue(PER_RETRY_HEADER)); - assertEquals(1, recordingClient.getSendCount()); - } - - @Test - void asyncSendWithContextAppliesPolicies() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - HttpPipelinePolicy perCallPolicy - = new HeaderAppendingPolicy(PER_CALL_HEADER, "context-one", HttpPipelinePosition.PER_CALL); - HttpPipelinePolicy perRetryPolicy - = new HeaderAppendingPolicy(PER_RETRY_HEADER, "context-two", HttpPipelinePosition.PER_RETRY); - - PolicyDecoratingHttpClient client - = new PolicyDecoratingHttpClient(recordingClient, Arrays.asList(perCallPolicy, perRetryPolicy)); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - HttpResponse response = client.send(request, Context.NONE).block(); - - assertNotNull(response); - HttpRequest sentRequest = recordingClient.getLastRequest(); - assertNotNull(sentRequest); - HttpHeaders headers = sentRequest.getHeaders(); - assertEquals("context-one", headers.getValue(PER_CALL_HEADER)); - assertEquals("context-two", headers.getValue(PER_RETRY_HEADER)); - assertEquals(1, recordingClient.getSendCount()); - } - - @Test - void policyErrorPropagatesInAsyncSend() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - RuntimeException policyException = new RuntimeException("Policy error"); - HttpPipelinePolicy failingPolicy = new FailingPolicy(policyException); - - PolicyDecoratingHttpClient client - = new PolicyDecoratingHttpClient(recordingClient, Collections.singletonList(failingPolicy)); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.send(request).block()); - assertEquals("Policy error", thrown.getMessage()); - assertEquals(0, recordingClient.getSendCount()); - } - - @Test - void underlyingClientErrorPropagatesInAsyncSend() throws Exception { - RuntimeException clientException = new RuntimeException("Client error"); - FailingHttpClient failingClient = new FailingHttpClient(clientException); - - PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(failingClient, Collections.emptyList()); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.send(request).block()); - assertEquals("Client error", thrown.getMessage()); - assertTrue(failingClient.wasCalled()); - } - - @Test - void policyErrorPropagatesInSyncSend() throws Exception { - RecordingHttpClient recordingClient = new RecordingHttpClient(); - RuntimeException policyException = new RuntimeException("Sync policy error"); - HttpPipelinePolicy failingPolicy = new FailingPolicy(policyException); - - PolicyDecoratingHttpClient client - = new PolicyDecoratingHttpClient(recordingClient, Collections.singletonList(failingPolicy)); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.sendSync(request, Context.NONE)); - assertEquals("Sync policy error", thrown.getMessage()); - assertEquals(0, recordingClient.getSendCount()); - } - - @Test - void underlyingClientErrorPropagatesInSyncSend() throws Exception { - RuntimeException clientException = new RuntimeException("Sync client error"); - FailingHttpClient failingClient = new FailingHttpClient(clientException); - - PolicyDecoratingHttpClient client = new PolicyDecoratingHttpClient(failingClient, Collections.emptyList()); - - HttpRequest request = new HttpRequest(HttpMethod.GET, URI.create("https://example.com").toURL()); - - RuntimeException thrown = assertThrows(RuntimeException.class, () -> client.sendSync(request, Context.NONE)); - assertEquals("Sync client error", thrown.getMessage()); - assertTrue(failingClient.wasCalled()); - } - - private static final class RecordingHttpClient implements HttpClient { - private HttpRequest lastRequest; - private final AtomicInteger sendCount = new AtomicInteger(); - - @Override - public Mono send(HttpRequest request) { - this.lastRequest = request; - this.sendCount.incrementAndGet(); - return Mono.just(new MockHttpResponse(request, 200, "ok".getBytes(StandardCharsets.UTF_8))); - } - - @Override - public Mono send(HttpRequest request, Context context) { - return send(request); - } - - HttpRequest getLastRequest() { - return lastRequest; - } - - int getSendCount() { - return sendCount.get(); - } - } - - private static final class HeaderAppendingPolicy implements HttpPipelinePolicy { - private final HttpHeaderName headerName; - private final String headerValue; - private final HttpPipelinePosition position; - - private HeaderAppendingPolicy(HttpHeaderName headerName, String headerValue, HttpPipelinePosition position) { - this.headerName = headerName; - this.headerValue = headerValue; - this.position = position; - } - - @Override - public HttpPipelinePosition getPipelinePosition() { - return position; - } - - @Override - public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { - context.getHttpRequest().getHeaders().set(headerName, headerValue); - return next.process(); - } - } - - private static final class FailingPolicy implements HttpPipelinePolicy { - private final RuntimeException exception; - - private FailingPolicy(RuntimeException exception) { - this.exception = exception; - } - - @Override - public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { - return Mono.error(exception); - } - - @Override - public HttpPipelinePosition getPipelinePosition() { - return HttpPipelinePosition.PER_CALL; - } - } - - private static final class FailingHttpClient implements HttpClient { - private final RuntimeException exception; - private boolean called = false; - - private FailingHttpClient(RuntimeException exception) { - this.exception = exception; - } - - @Override - public Mono send(HttpRequest request) { - this.called = true; - return Mono.error(exception); - } - - @Override - public Mono send(HttpRequest request, Context context) { - return send(request); - } - - boolean wasCalled() { - return called; - } - } -}