Skip to content

Commit f949fb2

Browse files
committed
Include deprecated code with tests
- keep completion and embedding calls with old api in client - Keep tests - improve test for reusability
1 parent 334ed8b commit f949fb2

File tree

4 files changed

+639
-222
lines changed

4 files changed

+639
-222
lines changed

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
import com.sap.ai.sdk.core.common.ClientResponseHandler;
1111
import com.sap.ai.sdk.core.common.ClientStreamingHandler;
1212
import com.sap.ai.sdk.core.common.StreamedDelta;
13+
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput;
14+
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters;
15+
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput;
16+
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters;
1317
import com.sap.ai.sdk.foundationmodels.openai.model2.ChatCompletionStreamOptions;
1418
import com.sap.ai.sdk.foundationmodels.openai.model2.ChatCompletionsCreate200Response;
1519
import com.sap.ai.sdk.foundationmodels.openai.model2.CreateChatCompletionRequest;
1620
import com.sap.ai.sdk.foundationmodels.openai.model2.CreateChatCompletionResponse;
1721
import com.sap.ai.sdk.foundationmodels.openai.model2.EmbeddingsCreate200Response;
1822
import com.sap.ai.sdk.foundationmodels.openai.model2.EmbeddingsCreateRequest;
23+
import com.sap.ai.sdk.foundationmodels.openai.model2.EmbeddingsCreateRequestInput;
1924
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
2025
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
2126
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
@@ -110,13 +115,6 @@ public static OpenAiClient withCustomDestination(@Nonnull final Destination dest
110115
return client.withApiVersion(DEFAULT_API_VERSION);
111116
}
112117

113-
private static void throwOnContentFilter(@Nonnull final OpenAiChatCompletionDelta delta) {
114-
final String finishReason = delta.getFinishReason();
115-
if (finishReason != null && finishReason.equals("content_filter")) {
116-
throw new OpenAiClientException("Content filter filtered the output.");
117-
}
118-
}
119-
120118
/**
121119
* Use this method to set a system prompt that should be used across multiple chat completions
122120
* with basic string prompts {@link #streamChatCompletionDeltas(OpenAiChatCompletionRequest)}.
@@ -153,32 +151,39 @@ public OpenAiChatCompletionResponse chatCompletion(@Nonnull final String prompt)
153151
return chatCompletion(request.toCreateChatCompletionRequest());
154152
}
155153

154+
private static void throwOnContentFilter(@Nonnull final OpenAiChatCompletionDelta delta) {
155+
final String finishReason = delta.getFinishReason();
156+
if (finishReason != null && finishReason.equals("content_filter")) {
157+
throw new OpenAiClientException("Content filter filtered the output.");
158+
}
159+
}
160+
156161
/**
157-
* Generate a completion for the given conversation and other request parameters.
162+
* Generate a completion for the given low-level request object.
158163
*
159164
* @param request the completion request.
160165
* @return the completion output
161166
* @throws OpenAiClientException if the request fails
162167
*/
163168
@Nonnull
164169
public OpenAiChatCompletionResponse chatCompletion(
165-
@Nonnull final OpenAiChatCompletionRequest request) throws OpenAiClientException {
166-
warnIfUnsupportedUsage();
167-
return chatCompletion(request.toCreateChatCompletionRequest());
170+
@Nonnull final CreateChatCompletionRequest request) throws OpenAiClientException {
171+
return new OpenAiChatCompletionResponse(
172+
execute("/chat/completions", request, CreateChatCompletionResponse.class));
168173
}
169174

170175
/**
171-
* Generate a completion for the given low-level request object.
176+
* Generate a completion for the given conversation and request parameters.
172177
*
173178
* @param request the completion request.
174179
* @return the completion output
175180
* @throws OpenAiClientException if the request fails
176181
*/
177182
@Nonnull
178183
public OpenAiChatCompletionResponse chatCompletion(
179-
@Nonnull final CreateChatCompletionRequest request) throws OpenAiClientException {
180-
return new OpenAiChatCompletionResponse(
181-
execute("/chat/completions", request, CreateChatCompletionResponse.class));
184+
@Nonnull final OpenAiChatCompletionRequest request) throws OpenAiClientException {
185+
warnIfUnsupportedUsage();
186+
return chatCompletion(request.toCreateChatCompletionRequest());
182187
}
183188

184189
/**
@@ -223,7 +228,23 @@ public Stream<String> streamChatCompletion(@Nonnull final String prompt)
223228
}
224229

225230
/**
226-
* Stream a completion for the given conversation and other request parameters.
231+
* Generate a completion for the given conversation and request parameters.
232+
*
233+
* @param parameters the completion request.
234+
* @return the completion output
235+
* @throws OpenAiClientException if the request fails
236+
* @deprecated Use {@link #chatCompletion(OpenAiChatCompletionRequest)} instead.
237+
*/
238+
@Deprecated(since = "1.3.0")
239+
@Nonnull
240+
public OpenAiChatCompletionOutput chatCompletion(
241+
@Nonnull final OpenAiChatCompletionParameters parameters) throws OpenAiClientException {
242+
warnIfUnsupportedUsage();
243+
return execute("/chat/completions", parameters, OpenAiChatCompletionOutput.class);
244+
}
245+
246+
/**
247+
* Stream a completion for the given conversation and request parameters.
227248
*
228249
* <p>Returns a <b>lazily</b> populated stream of delta objects. To simply stream the text chunks
229250
* use {@link #streamChatCompletion(String)}
@@ -272,13 +293,84 @@ public Stream<OpenAiChatCompletionDelta> streamChatCompletionDeltas(
272293
return executeStream("/chat/completions", request, OpenAiChatCompletionDelta.class);
273294
}
274295

296+
/**
297+
* Stream a completion for the given conversation and request parameters.
298+
*
299+
* <p>Returns a <b>lazily</b> populated stream of delta objects. To simply stream the text chunks
300+
* use {@link #streamChatCompletion(String)}
301+
*
302+
* <p>The stream should be consumed using a try-with-resources block to ensure that the underlying
303+
* HTTP connection is closed.
304+
*
305+
* <p>Example:
306+
*
307+
* <pre>{@code
308+
* try (var stream = client.streamChatCompletionDeltas(prompt)) {
309+
* stream
310+
* .peek(delta -> System.out.println(delta.getUsage()))
311+
* .map(com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta::getDeltaContent)
312+
* .forEach(System.out::println);
313+
* }
314+
* }</pre>
315+
*
316+
* <p>Please keep in mind that using a terminal stream operation like {@link Stream#forEach} will
317+
* block until all chunks are consumed. Also, for obvious reasons, invoking {@link
318+
* Stream#parallel()} on this stream is not supported.
319+
*
320+
* @param parameters The prompt, including a list of messages.
321+
* @return A stream of message deltas
322+
* @throws OpenAiClientException if the request fails or if the finish reason is content_filter
323+
* @deprecated Use {@link #streamChatCompletionDeltas(OpenAiChatCompletionRequest)} instead.
324+
*/
325+
@Deprecated(since = "1.3.0")
326+
@Nonnull
327+
public Stream<com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta>
328+
streamChatCompletionDeltas(@Nonnull final OpenAiChatCompletionParameters parameters)
329+
throws OpenAiClientException {
330+
warnIfUnsupportedUsage();
331+
parameters.enableStreaming();
332+
return executeStream(
333+
"/chat/completions",
334+
parameters,
335+
com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta.class);
336+
}
337+
275338
private void warnIfUnsupportedUsage() {
276339
if (systemPrompt != null) {
277340
log.warn(
278341
"Previously set messages will be ignored, set it as an argument of this method instead.");
279342
}
280343
}
281344

345+
/**
346+
* Get a vector representation of a given string input that can be easily consumed by machine
347+
* learning models and algorithms.
348+
*
349+
* @param input the input text.
350+
* @return the embedding output
351+
* @throws OpenAiClientException if the request fails
352+
*/
353+
@Nonnull
354+
public EmbeddingsCreate200Response embedding(@Nonnull final String input)
355+
throws OpenAiClientException {
356+
return embedding(
357+
new EmbeddingsCreateRequest().input(EmbeddingsCreateRequestInput.create(input)));
358+
}
359+
360+
/**
361+
* Get a vector representation of a given request with input that can be easily consumed by
362+
* machine learning models and algorithms.
363+
*
364+
* @param request the request with input text.
365+
* @return the embedding output
366+
* @throws OpenAiClientException if the request fails
367+
*/
368+
@Nonnull
369+
public EmbeddingsCreate200Response embedding(@Nonnull final EmbeddingsCreateRequest request)
370+
throws OpenAiClientException {
371+
return execute("/embeddings", request, EmbeddingsCreate200Response.class);
372+
}
373+
282374
/**
283375
* Get a vector representation of a given input that can be easily consumed by machine learning
284376
* models and algorithms.
@@ -288,9 +380,9 @@ private void warnIfUnsupportedUsage() {
288380
* @throws OpenAiClientException if the request fails
289381
*/
290382
@Nonnull
291-
public EmbeddingsCreate200Response embedding(@Nonnull final EmbeddingsCreateRequest parameters)
383+
public OpenAiEmbeddingOutput embedding(@Nonnull final OpenAiEmbeddingParameters parameters)
292384
throws OpenAiClientException {
293-
return execute("/embeddings", parameters, EmbeddingsCreate200Response.class);
385+
return execute("/embeddings", parameters, OpenAiEmbeddingOutput.class);
294386
}
295387

296388
@Nonnull
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
4+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
5+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
6+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
7+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
8+
import static org.mockito.ArgumentMatchers.any;
9+
import static org.mockito.Mockito.doReturn;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.spy;
12+
13+
import com.fasterxml.jackson.databind.ObjectMapper;
14+
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
15+
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
16+
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
17+
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Cache;
18+
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.util.Objects;
22+
import java.util.function.Function;
23+
import org.apache.hc.client5.http.classic.HttpClient;
24+
import org.apache.hc.core5.http.ContentType;
25+
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
26+
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
27+
import org.junit.jupiter.api.AfterEach;
28+
import org.junit.jupiter.api.BeforeEach;
29+
30+
@WireMockTest
31+
abstract class BaseOpenAiClientTest {
32+
33+
protected static final ObjectMapper MAPPER = new ObjectMapper();
34+
protected static OpenAiClient client;
35+
protected final Function<String, InputStream> fileLoader =
36+
filename -> Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(filename));
37+
38+
static void stubForChatCompletion() {
39+
40+
stubFor(
41+
post(urlPathEqualTo("/chat/completions"))
42+
.withQueryParam("api-version", equalTo("2024-02-01"))
43+
.willReturn(
44+
aResponse()
45+
.withBodyFile("chatCompletionResponse.json")
46+
.withHeader("Content-Type", "application/json")));
47+
}
48+
49+
static void stubForEmbedding() {
50+
stubFor(
51+
post(urlPathEqualTo("/embeddings"))
52+
.willReturn(
53+
aResponse()
54+
.withBodyFile("embeddingResponse.json")
55+
.withHeader("Content-Type", "application/json")));
56+
}
57+
58+
@BeforeEach
59+
void setup(WireMockRuntimeInfo server) {
60+
final DefaultHttpDestination destination =
61+
DefaultHttpDestination.builder(server.getHttpBaseUrl()).build();
62+
client = OpenAiClient.withCustomDestination(destination);
63+
ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED);
64+
}
65+
66+
@AfterEach
67+
void reset() {
68+
ApacheHttpClient5Accessor.setHttpClientCache(null);
69+
ApacheHttpClient5Accessor.setHttpClientFactory(null);
70+
}
71+
72+
InputStream stubChatCompletionDeltas(String responseFile) throws IOException {
73+
var inputStream = spy(fileLoader.apply(responseFile));
74+
75+
final var httpClient = mock(HttpClient.class);
76+
ApacheHttpClient5Accessor.setHttpClientFactory(destination -> httpClient);
77+
78+
// Create a mock response
79+
final var mockResponse = new BasicClassicHttpResponse(200, "OK");
80+
final var inputStreamEntity = new InputStreamEntity(inputStream, ContentType.TEXT_PLAIN);
81+
mockResponse.setEntity(inputStreamEntity);
82+
mockResponse.setHeader("Content-Type", "text/event-stream");
83+
84+
// Configure the HttpClient mock to return the mock response
85+
doReturn(mockResponse).when(httpClient).executeOpen(any(), any(), any());
86+
87+
return inputStream;
88+
}
89+
}

0 commit comments

Comments
 (0)