Skip to content

Commit 719f910

Browse files
jpalvarezlCopilot
andauthored
Responses protocol methods (#49244)
* Add protocol methods to ResponsesClient and ResponsesAsyncClient Adds BinaryData + com.openai.core.RequestOptions protocol-style methods on the Responses sync and async clients that delegate to the openai-java ResponseService.withRawResponse() / ResponseServiceAsync.withRawResponse() surface and return the openai-java raw HTTP response types directly. Specifically: - createResponseWithResponse / createResponseStreamWithResponse - getResponseWithResponse - deleteResponseWithResponse - cancelResponseWithResponse The async variants wrap the underlying CompletableFuture in Mono.fromFuture(...). All methods continue to flow through the Azure HTTP pipeline via HttpClientHelper.mapToOpenAIHttpClient. Adds OpenAIJsonHelper.jsonBodyToValueMap(BinaryData) helper used by the create methods to forward the raw JSON body verbatim through ResponseCreateParams.Builder.additionalBodyProperties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add protocol-method tests for ResponsesClient and ResponsesAsyncClient Mirrors the existing basicCRUDOperations tests against the new createResponseWithResponse protocol methods (BinaryData + RequestOptions), parses the returned HttpResponseFor<Response>, and asserts on the same shape (non-null Response with an id) as the typed-method tests. Tests remain @disabled pending public preview recordings, consistent with the existing typed tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enable Responses tests with full create/retrieve/delete/cancel/input-items coverage Cleanup, expansion, and enablement of ResponsesTests and ResponsesAsyncTests: 1. Removed the stale '// currently returning 500' comments and the orphan getOpenAIClient() pseudo-code blocks from the initial Agents V2 commit (PR #47134). Live calls against the service confirmed all four previously-flagged endpoints (retrieve, delete, cancel, input_items) now return 200. 2. Expanded both typed and protocol-method tests to actually exercise the full surface: create -> retrieve -> input_items -> delete in basicCRUDOperations, plus a separate cancelBackgroundResponse / cancelBackgroundResponseWithResponse that creates a background:true response and cancels it (asserting CANCELLED or COMPLETED, since the response may finish before cancel hits). 4 test methods per class (sync + async) x 2 surfaces = 8 tests total. 3. Dropped the class-level @disabled annotations now that recordings exist. Captured live recordings via AZURE_TEST_MODE=RECORD against the Foundry endpoint, pushed via 'test-proxy push -a assets.json' to Azure/azure-sdk-assets, and bumped the assets.json Tag to java/ai/azure-ai-agents_7a1b5ec037. Verified by re-running in default (playback) mode: all 8 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Narrow Responses protocol-methods scope to create + createStream only Drop the getResponseWithResponse, deleteResponseWithResponse, and cancelResponseWithResponse protocol methods added previously. The ResponsesClient/ResponsesAsyncClient surface now exposes protocol-method variants only for createAzureResponse and createStreamingAzureResponse, matching the original typed surface. Tests: - Trim basicCRUDOperationsWithResponse to exercise createResponseWithResponse only. - Add basicStreamingOperationsWithResponse to cover createResponseStreamWithResponse. - Drop cancelBackgroundResponseWithResponse. - Re-recorded affected sessions; bumped assets.json tag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use OpenAI client directly for response get/delete/cancel in tests ResponsesClient and ResponsesAsyncClient are thin Azure-aware wrappers, so non-Azure operations (retrieve, delete, cancel) should go through the underlying com.openai client directly rather than the wrappers' service getters. Adds getResponseServiceSyncClient/getResponseServiceAsyncClient helpers in ClientTestBase that build the OpenAIClient/OpenAIClientAsync and expose .responses(). Updates basicCRUDOperations and cancelBackgroundResponse in both test classes to use the new helpers for retrieve/delete/cancel; create still flows through ResponsesClient. Recordings unchanged (the OpenAI client built by AgentsClientBuilder shares the same Azure HTTP pipeline as the wrappers' service). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback on Responses protocol methods - Reword JavaDoc on createResponseWithResponse in both clients to clarify the JSON body is forwarded semantically (parsed and re-serialized via additionalBodyProperties), not byte-for-byte, since property ordering may change and duplicate top-level keys are not preserved. - Apply the same clarification to the streaming variant JavaDoc. - Wrap HttpResponseFor<Response> in try-with-resources in basicCRUDOperationsWithResponse for both sync and async test classes so the response is reliably closed. - Remove unused CREATE_BACKGROUND_RESPONSE_BODY constant in both test classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ae12cde commit 719f910

8 files changed

Lines changed: 379 additions & 51 deletions

File tree

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

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

55
### Features Added
66

7+
- Added protocol-style methods on `ResponsesClient` and `ResponsesAsyncClient` that accept a raw JSON request body (`BinaryData`) and a `com.openai.core.RequestOptions`, and return the openai-java raw HTTP response. These mirror the existing `createAzureResponse` and `createStreamingAzureResponse` typed surface: `createResponseWithResponse` (returns `HttpResponseFor<Response>`) and `createResponseStreamWithResponse` (returns `HttpResponseFor<StreamResponse<ResponseStreamEvent>>`). They delegate to the underlying openai-java `ResponseService.withRawResponse()` surface and continue to flow through the Azure HTTP pipeline.
8+
9+
### Other Changes
10+
11+
- Enabled `ResponsesTests` and `ResponsesAsyncTests` (previously `@Disabled`) with create/retrieve/delete/input-items and background-cancel coverage for the typed (`ResponseService` / `ResponseServiceAsync`) surface, plus coverage for the new protocol-method surface. Recordings published to `Azure/azure-sdk-assets` and referenced from `assets.json`.
12+
713
### Breaking Changes
814

915
### Bugs Fixed

sdk/ai/azure-ai-agents/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "java",
44
"TagPrefix": "java/ai/azure-ai-agents",
5-
"Tag": "java/ai/azure-ai-agents_69308f6d56"
5+
"Tag": "java/ai/azure-ai-agents_a3142fe843"
66
}

sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/ResponsesAsyncClient.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
import com.azure.core.annotation.ReturnType;
1111
import com.azure.core.annotation.ServiceClient;
1212
import com.azure.core.annotation.ServiceMethod;
13+
import com.azure.core.util.BinaryData;
1314
import com.openai.client.OpenAIClientAsync;
1415
import com.openai.core.JsonValue;
16+
import com.openai.core.RequestOptions;
17+
import com.openai.core.http.HttpResponseFor;
18+
import com.openai.core.http.StreamResponse;
1519
import com.openai.models.responses.Response;
1620
import com.openai.models.responses.ResponseCreateParams;
1721
import com.openai.models.responses.ResponseStreamEvent;
@@ -84,4 +88,74 @@ public Flux<ResponseStreamEvent> createStreamingAzureResponse(AzureCreateRespons
8488
params.additionalBodyProperties(additionalBodyProperties);
8589
return StreamingUtils.toFlux(this.responseServiceAsync.createStreaming(params.build()));
8690
}
91+
92+
/**
93+
* Creates a response from a raw JSON request body and returns the raw HTTP response.
94+
*
95+
* <p>This protocol method delegates to the OpenAI Java SDK's
96+
* {@link ResponseServiceAsync.WithRawResponse#create(ResponseCreateParams, RequestOptions)}. The
97+
* {@code createResponseRequest} payload is forwarded as the request body (semantically, not
98+
* byte-for-byte: the JSON is parsed and re-serialized via {@code additionalBodyProperties},
99+
* so property ordering and exact formatting may change and duplicate top-level keys are not
100+
* preserved), so callers can include Azure-specific extensions (such as
101+
* {@link com.azure.ai.agents.models.AgentReference}) without going through the strongly-typed
102+
* {@link ResponseCreateParams.Builder}.</p>
103+
*
104+
* <p>The returned {@link HttpResponseFor} exposes the status code, headers, and the raw
105+
* response stream via {@code body()}, or the typed {@link Response} via {@code parse()}. Only
106+
* one of {@code body()} or {@code parse()} may be invoked per response, and the caller must
107+
* close the response (e.g. via try-with-resources) to release the underlying connection.</p>
108+
*
109+
* <p>Note: the second parameter is the openai-java {@link RequestOptions} (not the azure-core
110+
* type) so that the OpenAI-supported options (timeout, response validation) translate
111+
* faithfully. Additional headers or query parameters must be supplied via the OpenAI request
112+
* builder pattern (e.g. by using {@link #createAzureResponse} for fully-typed requests).</p>
113+
*
114+
* @param createResponseRequest the JSON body representing the create-response request; must be a JSON object.
115+
* @param requestOptions optional OpenAI request options; pass {@code null} to use the defaults.
116+
* @return a {@link Mono} emitting the raw HTTP response, parseable as a {@link Response}.
117+
*/
118+
@ServiceMethod(returns = ReturnType.SINGLE)
119+
public Mono<HttpResponseFor<Response>> createResponseWithResponse(BinaryData createResponseRequest,
120+
RequestOptions requestOptions) {
121+
Objects.requireNonNull(createResponseRequest, "createResponseRequest cannot be null");
122+
123+
ResponseCreateParams params = ResponseCreateParams.builder()
124+
.additionalBodyProperties(OpenAIJsonHelper.jsonBodyToValueMap(createResponseRequest))
125+
.build();
126+
return Mono.fromFuture(this.responseServiceAsync.withRawResponse()
127+
.create(params, requestOptions == null ? RequestOptions.none() : requestOptions));
128+
}
129+
130+
/**
131+
* Creates a streaming response from a raw JSON request body and returns the raw HTTP response.
132+
*
133+
* <p>Delegates to the OpenAI Java SDK's
134+
* {@link ResponseServiceAsync.WithRawResponse#createStreaming(ResponseCreateParams, RequestOptions)}.
135+
* The {@code createResponseRequest} payload is forwarded as the request body (semantically,
136+
* not byte-for-byte: the JSON is parsed and re-serialized, so property ordering and exact
137+
* formatting may change and duplicate top-level keys are not preserved).</p>
138+
*
139+
* <p>The returned {@link HttpResponseFor} wraps a {@link StreamResponse} of
140+
* {@link ResponseStreamEvent} items, which the caller iterates via {@link HttpResponseFor#parse()}.
141+
* Note that the underlying stream produced by the openai-java SDK's raw async streaming API is
142+
* iterator-based and blocking; for a Reactor-friendly streaming surface use
143+
* {@link #createStreamingAzureResponse(AzureCreateResponseOptions, ResponseCreateParams.Builder)}
144+
* instead.</p>
145+
*
146+
* @param createResponseRequest the JSON body representing the create-response request; must be a JSON object.
147+
* @param requestOptions optional OpenAI request options; pass {@code null} to use the defaults.
148+
* @return a {@link Mono} emitting the raw HTTP response, parseable as a {@link StreamResponse} of {@link ResponseStreamEvent}.
149+
*/
150+
@ServiceMethod(returns = ReturnType.COLLECTION)
151+
public Mono<HttpResponseFor<StreamResponse<ResponseStreamEvent>>>
152+
createResponseStreamWithResponse(BinaryData createResponseRequest, RequestOptions requestOptions) {
153+
Objects.requireNonNull(createResponseRequest, "createResponseRequest cannot be null");
154+
155+
ResponseCreateParams params = ResponseCreateParams.builder()
156+
.additionalBodyProperties(OpenAIJsonHelper.jsonBodyToValueMap(createResponseRequest))
157+
.build();
158+
return Mono.fromFuture(this.responseServiceAsync.withRawResponse()
159+
.createStreaming(params, requestOptions == null ? RequestOptions.none() : requestOptions));
160+
}
87161
}

sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/ResponsesClient.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
import com.azure.core.annotation.ServiceClient;
1212
import com.azure.core.annotation.ServiceMethod;
1313
import com.azure.core.annotation.ReturnType;
14+
import com.azure.core.util.BinaryData;
1415
import com.azure.core.util.IterableStream;
1516
import com.openai.client.OpenAIClient;
1617
import com.openai.core.JsonValue;
18+
import com.openai.core.RequestOptions;
19+
import com.openai.core.http.HttpResponseFor;
20+
import com.openai.core.http.StreamResponse;
1721
import com.openai.models.responses.Response;
1822
import com.openai.models.responses.ResponseCreateParams;
1923
import com.openai.models.responses.ResponseStreamEvent;
@@ -97,4 +101,72 @@ public static AzureCreateResponseDetails getAzureFields(Response response) {
97101
AzureCreateResponseDetails::fromJson);
98102
}
99103

104+
/**
105+
* Creates a response from a raw JSON request body and returns the raw HTTP response.
106+
*
107+
* <p>This protocol method delegates to the OpenAI Java SDK's
108+
* {@link ResponseService.WithRawResponse#create(ResponseCreateParams, RequestOptions)}. The
109+
* {@code createResponseRequest} payload is forwarded as the request body (semantically, not
110+
* byte-for-byte: the JSON is parsed and re-serialized via {@code additionalBodyProperties},
111+
* so property ordering and exact formatting may change and duplicate top-level keys are not
112+
* preserved), so callers can include Azure-specific extensions (such as
113+
* {@link com.azure.ai.agents.models.AgentReference}) without going through the strongly-typed
114+
* {@link ResponseCreateParams.Builder}.</p>
115+
*
116+
* <p>The returned {@link HttpResponseFor} exposes the status code, headers, and the raw
117+
* response stream via {@code body()}, or the typed {@link Response} via {@code parse()}. Only
118+
* one of {@code body()} or {@code parse()} may be invoked per response, and the caller must
119+
* close the response (e.g. via try-with-resources) to release the underlying connection.</p>
120+
*
121+
* <p>Note: the second parameter is the openai-java {@link RequestOptions} (not the azure-core
122+
* type) so that the OpenAI-supported options (timeout, response validation) translate
123+
* faithfully. Additional headers or query parameters must be supplied via the OpenAI request
124+
* builder pattern (e.g. by using {@link #createAzureResponse} for fully-typed requests).</p>
125+
*
126+
* @param createResponseRequest the JSON body representing the create-response request; must be a JSON object.
127+
* @param requestOptions optional OpenAI request options; pass {@code null} to use the defaults.
128+
* @return the raw HTTP response, parseable as a {@link Response}.
129+
*/
130+
@ServiceMethod(returns = ReturnType.SINGLE)
131+
public HttpResponseFor<Response> createResponseWithResponse(BinaryData createResponseRequest,
132+
RequestOptions requestOptions) {
133+
Objects.requireNonNull(createResponseRequest, "createResponseRequest cannot be null");
134+
135+
ResponseCreateParams params = ResponseCreateParams.builder()
136+
.additionalBodyProperties(OpenAIJsonHelper.jsonBodyToValueMap(createResponseRequest))
137+
.build();
138+
return this.responseService.withRawResponse()
139+
.create(params, requestOptions == null ? RequestOptions.none() : requestOptions);
140+
}
141+
142+
/**
143+
* Creates a streaming response from a raw JSON request body and returns the raw HTTP response.
144+
*
145+
* <p>Delegates to the OpenAI Java SDK's
146+
* {@link ResponseService.WithRawResponse#createStreaming(ResponseCreateParams, RequestOptions)}.
147+
* The {@code createResponseRequest} payload is forwarded as the request body (semantically,
148+
* not byte-for-byte: the JSON is parsed and re-serialized, so property ordering and exact
149+
* formatting may change and duplicate top-level keys are not preserved).</p>
150+
*
151+
* <p>The returned {@link HttpResponseFor} wraps a {@link StreamResponse} of
152+
* {@link ResponseStreamEvent} items, which the caller iterates via {@link HttpResponseFor#parse()}.
153+
* The underlying stream must be closed when iteration completes; the typical pattern is
154+
* try-with-resources on either the {@link HttpResponseFor} or the parsed {@link StreamResponse}.</p>
155+
*
156+
* @param createResponseRequest the JSON body representing the create-response request; must be a JSON object.
157+
* @param requestOptions optional OpenAI request options; pass {@code null} to use the defaults.
158+
* @return the raw HTTP response, parseable as a {@link StreamResponse} of {@link ResponseStreamEvent}.
159+
*/
160+
@ServiceMethod(returns = ReturnType.COLLECTION)
161+
public HttpResponseFor<StreamResponse<ResponseStreamEvent>>
162+
createResponseStreamWithResponse(BinaryData createResponseRequest, RequestOptions requestOptions) {
163+
Objects.requireNonNull(createResponseRequest, "createResponseRequest cannot be null");
164+
165+
ResponseCreateParams params = ResponseCreateParams.builder()
166+
.additionalBodyProperties(OpenAIJsonHelper.jsonBodyToValueMap(createResponseRequest))
167+
.build();
168+
return this.responseService.withRawResponse()
169+
.createStreaming(params, requestOptions == null ? RequestOptions.none() : requestOptions);
170+
}
171+
100172
}

sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/OpenAIJsonHelper.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,43 @@ public static <T extends JsonSerializable<T>> Map<String, JsonValue> toJsonValue
160160
}
161161
try {
162162
String json = BinaryData.fromObject(obj).toString();
163-
Map<String, Object> map = MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {
164-
});
165-
Map<String, JsonValue> result = new HashMap<>();
166-
for (Map.Entry<String, Object> entry : map.entrySet()) {
167-
result.put(entry.getKey(), JsonValue.from(entry.getValue()));
168-
}
169-
return result;
163+
return jsonStringToJsonValueMap(json);
170164
} catch (IOException e) {
171165
throw new RuntimeException("Failed to flatten JsonSerializable to JsonValue map", e);
172166
}
173167
}
174168

169+
/**
170+
* Parses raw JSON bytes representing a top-level JSON object into a map of property names to
171+
* {@link JsonValue} entries, suitable for placing on an openai-java request builder via
172+
* {@code additionalBodyProperties(Map)}. The strongly-typed builder fields remain unset, so
173+
* the serialized output of the resulting params matches the original JSON.
174+
*
175+
* @param body the JSON body bytes; the content must represent a JSON object.
176+
* @return a map of property names to {@link JsonValue} entries, or an empty map if the input is null.
177+
* @throws RuntimeException if the input is not a valid JSON object.
178+
*/
179+
public static Map<String, JsonValue> jsonBodyToValueMap(BinaryData body) {
180+
if (body == null) {
181+
return new HashMap<>();
182+
}
183+
try {
184+
return jsonStringToJsonValueMap(body.toString());
185+
} catch (IOException e) {
186+
throw new RuntimeException("Failed to parse JSON body to JsonValue map", e);
187+
}
188+
}
189+
190+
private static Map<String, JsonValue> jsonStringToJsonValueMap(String json) throws IOException {
191+
Map<String, Object> map = MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {
192+
});
193+
Map<String, JsonValue> result = new HashMap<>();
194+
for (Map.Entry<String, Object> entry : map.entrySet()) {
195+
result.put(entry.getKey(), JsonValue.from(entry.getValue()));
196+
}
197+
return result;
198+
}
199+
175200
/**
176201
* Deserializes a map of {@link JsonValue} entries (typically from
177202
* {@code response._additionalProperties()}) into an Azure SDK type that implements

sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ClientTestBase.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import com.azure.core.util.Configuration;
1616
import com.azure.identity.DefaultAzureCredentialBuilder;
1717
import com.openai.services.async.ConversationServiceAsync;
18+
import com.openai.services.async.ResponseServiceAsync;
1819
import com.openai.services.blocking.ConversationService;
20+
import com.openai.services.blocking.ResponseService;
1921

2022
import java.util.ArrayList;
2123
import java.util.Arrays;
@@ -84,6 +86,16 @@ protected ResponsesAsyncClient getResponsesAsyncClient(HttpClient httpClient,
8486
return getClientBuilder(httpClient, agentsServiceVersion).buildResponsesAsyncClient();
8587
}
8688

89+
protected ResponseService getResponseServiceSyncClient(HttpClient httpClient,
90+
AgentsServiceVersion agentsServiceVersion) {
91+
return getClientBuilder(httpClient, agentsServiceVersion).buildOpenAIClient().responses();
92+
}
93+
94+
protected ResponseServiceAsync getResponseServiceAsyncClient(HttpClient httpClient,
95+
AgentsServiceVersion agentsServiceVersion) {
96+
return getClientBuilder(httpClient, agentsServiceVersion).buildOpenAIAsyncClient().responses();
97+
}
98+
8799
protected MemoryStoresClient getMemoryStoresSyncClient(HttpClient httpClient,
88100
AgentsServiceVersion agentsServiceVersion) {
89101
return getClientBuilder(httpClient, agentsServiceVersion).buildMemoryStoresClient();

0 commit comments

Comments
 (0)