From fa6a86d9014695e40c524ea264cd739e8aa135fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 11:38:48 +0200 Subject: [PATCH 01/21] Initial --- .../ai/sdk/core/common/ClientException.java | 25 +++++++++++++++++++ .../core/common/ClientExceptionFactory.java | 11 ++++++++ .../core/common/ClientResponseHandler.java | 25 +++++++++++-------- .../core/common/ClientStreamingHandler.java | 8 +++--- .../core/common/IterableStreamConverter.java | 20 ++++++++------- .../common/IterableStreamConverterTest.java | 13 +++++++--- .../foundationmodels/openai/OpenAiClient.java | 7 +++--- .../openai/OpenAiClientException.java | 1 + .../openai/OpenAiExceptionFactory.java | 4 +-- .../openai/BaseOpenAiClientTest.java | 4 ++- 10 files changed, 86 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java index cd311993f..3a6bf3894 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java @@ -5,7 +5,10 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; import lombok.experimental.StandardException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; /** * Generic exception for errors occurring when using AI SDK clients. @@ -24,4 +27,26 @@ public class ClientException extends RuntimeException { @Getter(onMethod_ = @Beta, value = AccessLevel.PROTECTED) @Setter(onMethod_ = @Beta, value = AccessLevel.PROTECTED) ClientError clientError; + + /** + * The original HTTP response that caused this exception, if available. + * + * @since 1.10.0 + */ + @Nullable + @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Accessors(chain = true) + ClassicHttpResponse httpResponse; + + /** + * The original HTTP request that caused this exception, if available. + * + * @since 1.10.0 + */ + @Nullable + @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Accessors(chain = true) + ClassicHttpRequest httpRequest; } diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java index 991c11c52..5140abe21 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java @@ -24,6 +24,17 @@ public interface ClientExceptionFactory exceptionFactory.build("Failed to parse response entity.", e)); + .getOrElseThrow( + e -> + exceptionFactory + .build("Failed to parse response entity.", e) + .setHttpResponse(response)); try { return objectMapper.readValue(content, successType); } catch (final JsonProcessingException e) { log.error("Failed to parse response to type {}", successType); - throw exceptionFactory.build("Failed to parse response", e); + throw exceptionFactory.build("Failed to parse response", e).setHttpResponse(response); } } @@ -111,19 +115,19 @@ protected void buildAndThrowException(@Nonnull final ClassicHttpResponse httpRes if (entity == null) { val message = getErrorMessage(httpResponse, "The HTTP Response is empty"); - throw exceptionFactory.build(message, null); + throw exceptionFactory.build(message).setHttpResponse(httpResponse); } val maybeContent = tryGetContent(entity); if (maybeContent.isFailure()) { val message = getErrorMessage(httpResponse, "Failed to read the response content"); - val baseException = exceptionFactory.build(message, null); + val baseException = exceptionFactory.build(message).setHttpResponse(httpResponse); baseException.addSuppressed(maybeContent.getCause()); throw baseException; } val content = maybeContent.get(); if (content == null || content.isBlank()) { val message = getErrorMessage(httpResponse, "Empty or blank response content"); - throw exceptionFactory.build(message, null); + throw exceptionFactory.build(message).setHttpResponse(httpResponse); } log.error( @@ -133,7 +137,7 @@ protected void buildAndThrowException(@Nonnull final ClassicHttpResponse httpRes val contentType = ContentType.parse(entity.getContentType()); if (!ContentType.APPLICATION_JSON.isSameMimeType(contentType)) { val message = getErrorMessage(httpResponse, "The response Content-Type is not JSON"); - throw exceptionFactory.build(message, null); + throw exceptionFactory.build(message).setHttpResponse(httpResponse); } parseErrorResponseAndThrow(content, httpResponse); @@ -151,7 +155,7 @@ protected void parseErrorResponseAndThrow( val maybeClientError = Try.of(() -> objectMapper.readValue(content, errorType)); if (maybeClientError.isFailure()) { val message = getErrorMessage(httpResponse, "Failed to parse the JSON error response"); - val baseException = exceptionFactory.build(message, null); + val baseException = exceptionFactory.build(message).setHttpResponse(httpResponse); baseException.addSuppressed(maybeClientError.getCause()); throw baseException; } @@ -161,10 +165,9 @@ protected void parseErrorResponseAndThrow( } private static String getErrorMessage( - @Nonnull final ClassicHttpResponse httpResponse, @Nullable final String additionalMessage) { + @Nonnull final ClassicHttpResponse rsp, @Nullable final String additionalMessage) { val baseErrorMessage = - "Request failed with status %d (%s)" - .formatted(httpResponse.getCode(), httpResponse.getReasonPhrase()); + "Request failed with status %d (%s)".formatted(rsp.getCode(), rsp.getReasonPhrase()); val message = Optional.ofNullable(additionalMessage).orElse(""); return message.isEmpty() ? baseErrorMessage : "%s: %s".formatted(baseErrorMessage, message); diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java index 1d27edb9d..10f1ee2cf 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java @@ -63,14 +63,14 @@ public Stream handleStreamingResponse(@Nonnull final ClassicHttpResponse resp super.buildAndThrowException(response); } - return IterableStreamConverter.lines(response.getEntity(), exceptionFactory) + return IterableStreamConverter.lines(response, exceptionFactory) // half of the lines are empty newlines, the last line is "data: [DONE]" .filter(line -> !line.isEmpty() && !"data: [DONE]".equals(line.trim())) .peek( line -> { if (!line.startsWith("data: ")) { final String msg = "Failed to parse response"; - throw exceptionFactory.build(msg, null); + throw exceptionFactory.build(msg).setHttpResponse(response); } }) .map( @@ -80,7 +80,9 @@ public Stream handleStreamingResponse(@Nonnull final ClassicHttpResponse resp return objectMapper.readValue(data, successType); } catch (final IOException e) { // exception message e gets lost log.error("Failed to parse delta chunk to type {}", successType); - throw exceptionFactory.build("Failed to parse delta chunk", e); + throw exceptionFactory + .build("Failed to parse delta chunk", e) + .setHttpResponse(response); } }); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java b/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java index fb06af6fc..5fc95b7db 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java @@ -17,11 +17,10 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ClassicHttpResponse; /** * Internal utility class to convert from a reading handler to {@link Iterable} and {@link Stream}. @@ -89,7 +88,7 @@ public T next() { * InputStream} is closed, when the resulting Stream is closed (e.g. via try-with-resources) or * when an exception occurred. * - * @param entity The HTTP entity object. + * @param response The HTTP response object. * @param exceptionFactory The exception factory to use for creating exceptions. * @return A sequential Stream object. * @throws ClientException if the provided HTTP entity object is {@code null} or empty. @@ -97,18 +96,18 @@ public T next() { @SuppressWarnings("PMD.CloseResource") // Stream is closed automatically when consumed @Nonnull static Stream lines( - @Nullable final HttpEntity entity, + @Nonnull final ClassicHttpResponse response, @Nonnull final ClientExceptionFactory exceptionFactory) throws ClientException { - if (entity == null) { - throw exceptionFactory.build("The HTTP Response is empty", null); + if (response.getEntity() == null) { + throw exceptionFactory.build("The HTTP Response is empty").setHttpResponse(response); } final InputStream inputStream; try { - inputStream = entity.getContent(); + inputStream = response.getEntity().getContent(); } catch (final IOException e) { - throw exceptionFactory.build("Failed to read response content.", e); + throw exceptionFactory.build("Failed to read response content.", e).setHttpResponse(response); } final var reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8), BUFFER_SIZE); @@ -121,7 +120,10 @@ static Stream lines( "Could not close input stream with error: {} (ignored)", e.getClass().getSimpleName())); final Function errHandler = - e -> exceptionFactory.build("Parsing response content was interrupted", e); + e -> + exceptionFactory + .build("Parsing response content was interrupted", e) + .setHttpResponse(response); final var iterator = new IterableStreamConverter<>(reader::readLine, closeHandler, errHandler); final var spliterator = Spliterators.spliteratorUnknownSize(iterator, ORDERED | NONNULL); diff --git a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java index 355b523a7..52e21b72e 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java @@ -22,6 +22,7 @@ import lombok.experimental.StandardException; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.InputStreamEntity; +import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -34,8 +35,10 @@ void testLines() { final var input = TEMPLATE.repeat(IterableStreamConverter.BUFFER_SIZE); final var inputStream = spy(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); final var entity = new InputStreamEntity(inputStream, ContentType.TEXT_PLAIN); + final var response = new BasicClassicHttpResponse(200, "OK"); + response.setEntity(entity); - final var sut = IterableStreamConverter.lines(entity, new TestClientExceptionFactory()); + final var sut = IterableStreamConverter.lines(response, new TestClientExceptionFactory()); verify(inputStream, never()).read(); verify(inputStream, never()).read(any()); verify(inputStream, never()).read(any(), anyInt(), anyInt()); @@ -70,8 +73,10 @@ void testLinesFindFirst() { }); final var entity = new InputStreamEntity(inputStream, ContentType.TEXT_PLAIN); + final var response = new BasicClassicHttpResponse(200, "OK"); + response.setEntity(entity); - final var sut = IterableStreamConverter.lines(entity, new TestClientExceptionFactory()); + final var sut = IterableStreamConverter.lines(response, new TestClientExceptionFactory()); assertThat(sut.findFirst()).contains("Foo Bar"); verify(inputStream, times(1)).read(any(), anyInt(), anyInt()); verify(inputStream, never()).close(); @@ -94,8 +99,10 @@ void testLinesThrows() { .thenThrow(new IOException("Ups!")); final var entity = new InputStreamEntity(inputStream, ContentType.TEXT_PLAIN); + final var response = new BasicClassicHttpResponse(200, "OK"); + response.setEntity(entity); - final var sut = IterableStreamConverter.lines(entity, new TestClientExceptionFactory()); + final var sut = IterableStreamConverter.lines(response, new TestClientExceptionFactory()); assertThatThrownBy(sut::count) .isInstanceOf(TestClientException.class) .hasMessage("Parsing response content was interrupted") diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 71416711a..3556b9c1a 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -420,7 +420,8 @@ private static void serializeAndSetHttpEntity( final var json = JACKSON.writeValueAsString(payload); request.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); } catch (final JsonProcessingException e) { - throw new OpenAiClientException("Failed to serialize request parameters", e); + throw new OpenAiClientException("Failed to serialize request parameters", e) + .setHttpRequest(request); } } @@ -434,7 +435,7 @@ private T executeRequest( new ClientResponseHandler<>( responseType, OpenAiError.class, new OpenAiExceptionFactory())); } catch (final IOException e) { - throw new OpenAiClientException("Request to OpenAI model failed", e); + throw new OpenAiClientException("Request to OpenAI model failed", e).setHttpRequest(request); } } @@ -448,7 +449,7 @@ deltaType, OpenAiError.class, new OpenAiExceptionFactory()) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null)); } catch (final IOException e) { - throw new OpenAiClientException("Request to OpenAI model failed", e); + throw new OpenAiClientException("Request to OpenAI model failed", e).setHttpRequest(request); } } } diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java index ca3e9d50a..4a0b436d8 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java @@ -10,6 +10,7 @@ /** Generic exception for errors occurring when using OpenAI foundation models. */ @StandardException public class OpenAiClientException extends ClientException { + OpenAiClientException(@Nonnull final String message, @Nonnull final OpenAiError clientError) { super(message); setClientError(clientError); diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java index 4cdf65af7..151d7084f 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java @@ -9,8 +9,8 @@ class OpenAiExceptionFactory implements ClientExceptionFactory { @Nonnull - public OpenAiClientException build( - @Nonnull final String message, @Nullable final Throwable cause) { + @Override + public OpenAiClientException build(@Nonnull String message, @Nullable Throwable cause) { return new OpenAiClientException(message, cause); } diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/BaseOpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/BaseOpenAiClientTest.java index d1c97f8c7..04ac8cb95 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/BaseOpenAiClientTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/BaseOpenAiClientTest.java @@ -117,7 +117,9 @@ static void assertForErrorHandling(@Nonnull final Runnable request) { .assertThatThrownBy(request::run) .describedAs("Server errors should be handled") .isInstanceOf(OpenAiClientException.class) - .hasMessageContaining("500"); + .hasMessageContaining("500") + .extracting(e -> ((OpenAiClientException) e).getHttpResponse()) + .isNotNull(); softly .assertThatThrownBy(request::run) From 2e75315f7e45c47d3d08a447d66f0ec37a01db8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 11:38:58 +0200 Subject: [PATCH 02/21] Drive by code improvement --- .../java/com/sap/ai/sdk/app/controllers/OpenAiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java index 2abe83fb5..393ef85fb 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java @@ -102,7 +102,7 @@ public static void send(@Nonnull final ResponseBodyEmitter emitter, @Nonnull fin try { emitter.send(chunk); } catch (final IOException e) { - log.error(Arrays.toString(e.getStackTrace())); + log.error("Failed to send chunk: {}", e.getMessage(), e); emitter.completeWithError(e); } } From 8fee86546241e69b1e01c4ddafa8c11daeab10f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 14:02:36 +0200 Subject: [PATCH 03/21] Fix test --- .../ai/sdk/core/common/ClientStreamingHandlerTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/common/ClientStreamingHandlerTest.java b/core/src/test/java/com/sap/ai/sdk/core/common/ClientStreamingHandlerTest.java index 1b36bf6e2..370cc8616 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/common/ClientStreamingHandlerTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/common/ClientStreamingHandlerTest.java @@ -79,11 +79,7 @@ void testHandleStreamingResponse() { """; var response = spy(new BasicClassicHttpResponse(200, "OK")); - when(response.getEntity()) - .thenReturn(new StringEntity(validStreamContent)) - .thenReturn(new StringEntity(emptyStreamContent)) - .thenReturn(new StringEntity(malformedLineContent)) - .thenReturn(new StringEntity(invalidJsonContent)); + when(response.getEntity()).thenReturn(new StringEntity(validStreamContent)); var stream1 = sut.handleStreamingResponse(response); var deltas1 = stream1.toList(); @@ -93,14 +89,17 @@ void testHandleStreamingResponse() { assertThat(deltas1.get(1).getDeltaContent()).isEqualTo("delta2"); assertThat(deltas1.get(1).getFinishReason()).isEqualTo("length"); + when(response.getEntity()).thenReturn(new StringEntity(emptyStreamContent)); var stream2 = sut.handleStreamingResponse(response); assertThat(stream2).isEmpty(); + when(response.getEntity()).thenReturn(new StringEntity(malformedLineContent)); var stream3 = sut.handleStreamingResponse(response); assertThatThrownBy(stream3::toList) .isInstanceOf(MyException.class) .hasMessageContaining("Failed to parse response"); + when(response.getEntity()).thenReturn(new StringEntity(invalidJsonContent)); var stream4 = sut.handleStreamingResponse(response); assertThatThrownBy(stream4::toList) .isInstanceOf(MyException.class) From 5bed6c28c8818b9f178d56d2d4ee3c668d48e91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 14:04:30 +0200 Subject: [PATCH 04/21] Fix annotation --- .../ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java index 151d7084f..c047cdc1e 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java @@ -10,7 +10,8 @@ class OpenAiExceptionFactory implements ClientExceptionFactory Date: Wed, 6 Aug 2025 14:06:24 +0200 Subject: [PATCH 05/21] Add missing http response --- .../java/com/sap/ai/sdk/core/common/ClientResponseHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java index c03c43b57..8120e2fd7 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java @@ -161,7 +161,7 @@ protected void parseErrorResponseAndThrow( } final R clientError = maybeClientError.get(); val message = getErrorMessage(httpResponse, clientError.getMessage()); - throw exceptionFactory.buildFromClientError(message, clientError); + throw exceptionFactory.buildFromClientError(message, clientError).setHttpResponse(httpResponse); } private static String getErrorMessage( From c6ab27cd41bfaf5c2ddc88a734641704944e8d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 14:22:42 +0200 Subject: [PATCH 06/21] Reduce complexity of exception factory --- .../ai/sdk/core/common/ClientException.java | 5 +-- .../core/common/ClientExceptionFactory.java | 10 ++++-- .../core/common/ClientResponseHandler.java | 4 +-- .../common/ClientResponseHandlerTest.java | 16 +++------- .../common/IterableStreamConverterTest.java | 17 +++------- .../openai/OpenAiClientException.java | 5 ++- .../openai/OpenAiExceptionFactory.java | 13 ++------ .../OrchestrationChatResponse.java | 2 +- .../orchestration/OrchestrationClient.java | 2 +- .../OrchestrationClientException.java | 6 ---- .../OrchestrationExceptionFactory.java | 27 +++++----------- .../OrchestrationFilterException.java | 31 ++++--------------- 12 files changed, 42 insertions(+), 96 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java index 3a6bf3894..ec5f42ee1 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java @@ -24,8 +24,9 @@ public class ClientException extends RuntimeException { * used to extract more detailed error information. */ @Nullable - @Getter(onMethod_ = @Beta, value = AccessLevel.PROTECTED) - @Setter(onMethod_ = @Beta, value = AccessLevel.PROTECTED) + @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) + @Accessors(chain = true) ClientError clientError; /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java index 5140abe21..454bd5b57 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java @@ -12,6 +12,7 @@ * @param The subtype of {@link ClientError} payload that can be processed by this factory. */ @Beta +@FunctionalInterface public interface ClientExceptionFactory { /** @@ -22,7 +23,9 @@ public interface ClientExceptionFactory { - @Nonnull + @NotNull @Override - public MyException build(@Nonnull String message, Throwable cause) { - return new MyException(message, cause); - } - - @Nonnull - @Override - public MyException buildFromClientError(@Nonnull String message, @Nonnull MyError clientError) { - var ex = new MyException(message); - ex.clientError = clientError; - return ex; + public MyException build(@NotNull String message, @Nullable MyError clientError, @Nullable Throwable cause) { + return (MyException) new MyException(message, cause).setClientError(clientError); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java index 52e21b72e..540781264 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java @@ -23,6 +23,8 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.InputStreamEntity; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -119,19 +121,10 @@ public static class TestClientException extends ClientException {} static class TestClientExceptionFactory implements ClientExceptionFactory { - @Nonnull + @NotNull @Override - public TestClientException build(@Nonnull String message, Throwable cause) { - return new TestClientException(message, cause); - } - - @Nonnull - @Override - public TestClientException buildFromClientError( - @Nonnull String message, @Nonnull ClientError clientError) { - TestClientException exception = new TestClientException(message); - exception.clientError = clientError; - return exception; + public TestClientException build(@NotNull String message, @Nullable ClientError clientError, @Nullable Throwable cause) { + return (TestClientException) new TestClientException(message, cause).setClientError(clientError); } } } diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java index 4a0b436d8..eb91715cb 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java @@ -11,9 +11,8 @@ @StandardException public class OpenAiClientException extends ClientException { - OpenAiClientException(@Nonnull final String message, @Nonnull final OpenAiError clientError) { - super(message); - setClientError(clientError); + OpenAiClientException(@Nonnull final String message, @Nullable final Throwable cause) { + super(message, cause); } /** diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java index c047cdc1e..6cdc66958 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java @@ -5,20 +5,11 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -@Beta class OpenAiExceptionFactory implements ClientExceptionFactory { @Nonnull @Override - public OpenAiClientException build( - @Nonnull final String message, @Nullable final Throwable cause) { - return new OpenAiClientException(message, cause); - } - - @Nonnull - @Override - public OpenAiClientException buildFromClientError( - @Nonnull final String message, @Nonnull final OpenAiError openAiError) { - return new OpenAiClientException(message, openAiError); + public OpenAiClientException build(@Nonnull String message, @Nullable OpenAiError clientError, @Nullable Throwable cause) { + return (OpenAiClientException) new OpenAiClientException(message, cause).setClientError(clientError); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java index 52c178a35..dfd737953 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java @@ -44,7 +44,7 @@ public String getContent() throws OrchestrationFilterException.Output { if ("content_filter".equals(choice.getFinishReason())) { final var filterDetails = Try.of(this::getOutputFilteringChoices).getOrElseGet(e -> Map.of()); final var message = "Content filter filtered the output."; - throw new OrchestrationFilterException.Output(message, filterDetails); + throw new OrchestrationFilterException.Output(message).setFilterDetails(filterDetails); } return choice.getMessage().getContent(); } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java index a16101017..c47f8d5a3 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java @@ -120,7 +120,7 @@ private static void throwOnContentFilter(@Nonnull final OrchestrationChatComplet final var filterDetails = Try.of(() -> getOutputFilteringChoices(delta)).getOrElseGet(e -> Map.of()); final var message = "Content filter filtered the output."; - throw new OrchestrationFilterException.Output(message, filterDetails); + throw new OrchestrationFilterException.Output(message).setFilterDetails(filterDetails); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 34493792f..2ce957b4b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -12,12 +12,6 @@ @StandardException public class OrchestrationClientException extends ClientException { - OrchestrationClientException( - @Nonnull final String message, @Nonnull final OrchestrationError clientError) { - super(message); - setClientError(clientError); - } - /** * Retrieves the {@link ErrorResponse} from the orchestration service, if available. * diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java index 62a3a1635..5ab87727e 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java @@ -11,34 +11,23 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -@Beta class OrchestrationExceptionFactory implements ClientExceptionFactory { - @Nonnull - public OrchestrationClientException build( - @Nonnull final String message, @Nullable final Throwable cause) { - return new OrchestrationClientException(message, cause); - } - @Nonnull @Override - public OrchestrationClientException buildFromClientError( - @Nonnull final String message, @Nonnull final OrchestrationError clientError) { - - final var inputFilterDetails = extractInputFilterDetails(clientError); - if (!inputFilterDetails.isEmpty()) { - return new OrchestrationFilterException.Input(message, clientError, inputFilterDetails); - } - - return new OrchestrationClientException(message, clientError); + public OrchestrationClientException build(@Nonnull String message, @Nullable OrchestrationError clientError, @Nullable Throwable cause) { + final var inputFilterDetails = extractInputFilterDetails(clientError); + if (!inputFilterDetails.isEmpty()) { + return (OrchestrationClientException) new OrchestrationFilterException.Input(message,cause).setFilterDetails(inputFilterDetails).setClientError(clientError); + } + return (OrchestrationClientException) new OrchestrationClientException(message, cause).setClientError(clientError); } @SuppressWarnings("unchecked") @Nonnull - private Map extractInputFilterDetails(@Nonnull final OrchestrationError error) { - - return Optional.of(error.getErrorResponse()) + private Map extractInputFilterDetails(@Nullable final OrchestrationError error) { + return Optional.ofNullable(error).map(OrchestrationError::getErrorResponse) .map(ErrorResponse::getModuleResults) .map(ModuleResults::getInputFiltering) .map(GenericModuleResult::getData) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java index 13a83a117..1a9873a4d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java @@ -12,6 +12,8 @@ import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import lombok.experimental.StandardException; /** Base exception for errors occurring during orchestration filtering. */ @@ -20,6 +22,8 @@ public class OrchestrationFilterException extends OrchestrationClientException { /** Details about the filters that caused the exception. */ + @Accessors(chain = true) + @Setter(AccessLevel.PACKAGE) @Getter @Nonnull protected Map filterDetails = Map.of(); /** @@ -37,22 +41,8 @@ public LlamaGuard38b getLlamaGuard38b() { } /** Exception thrown when an error occurs during input filtering. */ + @StandardException public static class Input extends OrchestrationFilterException { - /** - * Constructs a new OrchestrationInputFilterException. - * - * @param message The detail message. - * @param clientError The specific client error. - * @param filterDetails Details about the filter that caused the exception. - */ - Input( - @Nonnull final String message, - @Nonnull final OrchestrationError clientError, - @Nonnull final Map filterDetails) { - super(message); - setClientError(clientError); - this.filterDetails = filterDetails; - } /** * Retrieves Azure Content Safety input details from {@code filterDetails}, if present. @@ -75,17 +65,8 @@ public AzureContentSafetyInput getAzureContentSafetyInput() { * Exception thrown when an error occurs during output filtering, specifically when the finish * reason is a content filter. */ + @StandardException public static class Output extends OrchestrationFilterException { - /** - * Constructs a new OrchestrationOutputFilterException. - * - * @param message The detail message. - * @param filterDetails Details about the filter that caused the exception. - */ - Output(@Nonnull final String message, @Nonnull final Map filterDetails) { - super(message); - this.filterDetails = filterDetails; - } /** * Retrieves Azure Content Safety output details from {@code filterDetails}, if present. From a53de5961b2925e4d93e243dced1e4a5bf76a4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 14:48:05 +0200 Subject: [PATCH 07/21] Fix compilation --- .../ai/sdk/core/common/ClientException.java | 62 +++++++++++++++---- .../core/common/ClientExceptionFactory.java | 8 ++- .../common/ClientResponseHandlerTest.java | 12 ++-- .../common/IterableStreamConverterTest.java | 12 ++-- .../openai/OpenAiClientException.java | 5 -- .../openai/OpenAiExceptionFactory.java | 8 ++- .../openai/BaseOpenAiClientTest.java | 4 +- .../OrchestrationClientException.java | 1 - .../OrchestrationExceptionFactory.java | 21 ++++--- .../OrchestrationFilterException.java | 4 +- 10 files changed, 94 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java index ec5f42ee1..41342a5ba 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java @@ -1,11 +1,10 @@ package com.sap.ai.sdk.core.common; import com.google.common.annotations.Beta; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; import lombok.experimental.StandardException; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -25,9 +24,7 @@ public class ClientException extends RuntimeException { */ @Nullable @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Accessors(chain = true) - ClientError clientError; + private ClientError clientError; /** * The original HTTP response that caused this exception, if available. @@ -36,9 +33,7 @@ public class ClientException extends RuntimeException { */ @Nullable @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Accessors(chain = true) - ClassicHttpResponse httpResponse; + private ClassicHttpResponse httpResponse; /** * The original HTTP request that caused this exception, if available. @@ -47,7 +42,52 @@ public class ClientException extends RuntimeException { */ @Nullable @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Setter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) - @Accessors(chain = true) - ClassicHttpRequest httpRequest; + private ClassicHttpRequest httpRequest; + + /** + * Sets the original HTTP request that caused this exception. + * + * @param clientError the original structured error payload received from the remote service, can + * be null if not available. + * @return the current instance of {@link ClientException} with the changed ClientError data + * @param the type of the exception, typically a subclass of {@link ClientException} + */ + @SuppressWarnings("unchecked") + @Nonnull + public T setClientError(@Nullable final ClientError clientError) { + this.clientError = clientError; + return (T) this; + } + + /** + * Sets the original HTTP request that caused this exception. + * + * @param httpResponse the original HTTP response that caused this exception, can be null if not + * available. + * @return the current instance of {@link ClientException} with the changed HTTP response + * @param the type of the exception, typically a subclass of {@link ClientException} + */ + @SuppressWarnings("unchecked") + @Nonnull + public T setHttpResponse( + @Nullable final ClassicHttpResponse httpResponse) { + this.httpResponse = httpResponse; + return (T) this; + } + + /** + * Sets the original HTTP request that caused this exception. + * + * @param httpRequest the original HTTP request that caused this exception, can be null if not + * available. + * @return the current instance of {@link ClientException} with the changed HTTP request + * @param the type of the exception, typically a subclass of {@link ClientException} + */ + @SuppressWarnings("unchecked") + @Nonnull + public T setHttpRequest( + @Nullable final ClassicHttpRequest httpRequest) { + this.httpRequest = httpRequest; + return (T) this; + } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java index 454bd5b57..bfe03379b 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientExceptionFactory.java @@ -43,10 +43,14 @@ default E build(@Nonnull final String message) { * deserialized into a {@link ClientError} object. * * @param message A descriptive message for the exception. - * @param clientError The structured {@link ClientError} object deserialized from the response, null if not exist. + * @param clientError The structured {@link ClientError} object deserialized from the response, + * null if not exist. * @param cause An optional cause of the exception, can be null if not applicable. * @return An instance of the specified {@link ClientException} type */ @Nonnull - E build(@Nonnull final String message, @Nullable final R clientError, @Nullable final Throwable cause); + E build( + @Nonnull final String message, + @Nullable final R clientError, + @Nullable final Throwable cause); } diff --git a/core/src/test/java/com/sap/ai/sdk/core/common/ClientResponseHandlerTest.java b/core/src/test/java/com/sap/ai/sdk/core/common/ClientResponseHandlerTest.java index fce634c29..f81652a98 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/common/ClientResponseHandlerTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/common/ClientResponseHandlerTest.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.core.JsonParseException; import java.io.IOException; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.Data; import lombok.SneakyThrows; import lombok.experimental.StandardException; @@ -17,8 +18,6 @@ import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; class ClientResponseHandlerTest { @@ -35,10 +34,13 @@ static class MyError implements ClientError { static class MyException extends ClientException {} static class MyExceptionFactory implements ClientExceptionFactory { - @NotNull + @Nonnull @Override - public MyException build(@NotNull String message, @Nullable MyError clientError, @Nullable Throwable cause) { - return (MyException) new MyException(message, cause).setClientError(clientError); + public MyException build( + @Nonnull final String message, + @Nullable final MyError clientError, + @Nullable final Throwable cause) { + return new MyException(message, cause).setClientError(clientError); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java index 540781264..3823849d2 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/common/IterableStreamConverterTest.java @@ -18,13 +18,12 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.SneakyThrows; import lombok.experimental.StandardException; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.InputStreamEntity; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -121,10 +120,13 @@ public static class TestClientException extends ClientException {} static class TestClientExceptionFactory implements ClientExceptionFactory { - @NotNull + @Nonnull @Override - public TestClientException build(@NotNull String message, @Nullable ClientError clientError, @Nullable Throwable cause) { - return (TestClientException) new TestClientException(message, cause).setClientError(clientError); + public TestClientException build( + @Nonnull final String message, + @Nullable final ClientError clientError, + @Nullable final Throwable cause) { + return new TestClientException(message, cause).setClientError(clientError); } } } diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java index eb91715cb..20e9b29e3 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java @@ -3,7 +3,6 @@ import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientException; import com.sap.ai.sdk.foundationmodels.openai.generated.model.ErrorResponse; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.experimental.StandardException; @@ -11,10 +10,6 @@ @StandardException public class OpenAiClientException extends ClientException { - OpenAiClientException(@Nonnull final String message, @Nullable final Throwable cause) { - super(message, cause); - } - /** * Retrieves the {@link ErrorResponse} from the OpenAI service, if available. * diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java index 6cdc66958..57718b171 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.foundationmodels.openai; -import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientExceptionFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -9,7 +8,10 @@ class OpenAiExceptionFactory implements ClientExceptionFactory ((OpenAiClientException) e).getHttpResponse()) - .isNotNull(); + .satisfies(e -> assertThat(((OpenAiClientException) e).getHttpResponse()).isNotNull()); softly .assertThatThrownBy(request::run) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 2ce957b4b..255b8bb30 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -4,7 +4,6 @@ import com.sap.ai.sdk.core.common.ClientException; import com.sap.ai.sdk.orchestration.model.ErrorResponse; import java.util.Optional; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.experimental.StandardException; diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java index 5ab87727e..505fcd72b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.orchestration; -import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientExceptionFactory; import com.sap.ai.sdk.orchestration.model.ErrorResponse; import com.sap.ai.sdk.orchestration.model.GenericModuleResult; @@ -16,18 +15,24 @@ class OrchestrationExceptionFactory @Nonnull @Override - public OrchestrationClientException build(@Nonnull String message, @Nullable OrchestrationError clientError, @Nullable Throwable cause) { - final var inputFilterDetails = extractInputFilterDetails(clientError); - if (!inputFilterDetails.isEmpty()) { - return (OrchestrationClientException) new OrchestrationFilterException.Input(message,cause).setFilterDetails(inputFilterDetails).setClientError(clientError); - } - return (OrchestrationClientException) new OrchestrationClientException(message, cause).setClientError(clientError); + public OrchestrationClientException build( + @Nonnull final String message, + @Nullable final OrchestrationError clientError, + @Nullable final Throwable cause) { + final var inputFilterDetails = extractInputFilterDetails(clientError); + if (!inputFilterDetails.isEmpty()) { + return new OrchestrationFilterException.Input(message, cause) + .setFilterDetails(inputFilterDetails) + .setClientError(clientError); + } + return new OrchestrationClientException(message, cause).setClientError(clientError); } @SuppressWarnings("unchecked") @Nonnull private Map extractInputFilterDetails(@Nullable final OrchestrationError error) { - return Optional.ofNullable(error).map(OrchestrationError::getErrorResponse) + return Optional.ofNullable(error) + .map(OrchestrationError::getErrorResponse) .map(ErrorResponse::getModuleResults) .map(ModuleResults::getInputFiltering) .map(GenericModuleResult::getData) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java index 1a9873a4d..c62d74fa2 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java @@ -24,7 +24,9 @@ public class OrchestrationFilterException extends OrchestrationClientException { /** Details about the filters that caused the exception. */ @Accessors(chain = true) @Setter(AccessLevel.PACKAGE) - @Getter @Nonnull protected Map filterDetails = Map.of(); + @Getter + @Nonnull + protected Map filterDetails = Map.of(); /** * Retrieves LlamaGuard 3.8b details from {@code filterDetails}, if present. From 95b954425c8b1f65884cd2aab9f5b0c58404b008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 15:06:43 +0200 Subject: [PATCH 08/21] Improve code style --- .../foundationmodels/openai/OpenAiClient.java | 11 +++-- .../openai/OpenAiClientException.java | 4 ++ .../openai/OpenAiExceptionFactory.java | 17 -------- .../OrchestrationClientException.java | 29 +++++++++++++ .../OrchestrationExceptionFactory.java | 43 ------------------- .../OrchestrationHttpExecutor.java | 9 ++-- 6 files changed, 42 insertions(+), 71 deletions(-) delete mode 100644 foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java delete mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 3556b9c1a..7ce79dd16 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.foundationmodels.openai; +import static com.sap.ai.sdk.foundationmodels.openai.OpenAiClientException.FACTORY; import static com.sap.ai.sdk.foundationmodels.openai.OpenAiUtils.getOpenAiObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; @@ -42,7 +43,8 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public final class OpenAiClient { private static final String DEFAULT_API_VERSION = "2024-02-01"; - static final ObjectMapper JACKSON = getOpenAiObjectMapper(); + + private static final ObjectMapper JACKSON = getOpenAiObjectMapper(); @Nullable private String systemPrompt = null; @@ -431,9 +433,7 @@ private T executeRequest( try { final var client = ApacheHttpClient5Accessor.getHttpClient(destination); return client.execute( - request, - new ClientResponseHandler<>( - responseType, OpenAiError.class, new OpenAiExceptionFactory())); + request, new ClientResponseHandler<>(responseType, OpenAiError.class, FACTORY)); } catch (final IOException e) { throw new OpenAiClientException("Request to OpenAI model failed", e).setHttpRequest(request); } @@ -444,8 +444,7 @@ private Stream streamRequest( final BasicClassicHttpRequest request, @Nonnull final Class deltaType) { try { final var client = ApacheHttpClient5Accessor.getHttpClient(destination); - return new ClientStreamingHandler<>( - deltaType, OpenAiError.class, new OpenAiExceptionFactory()) + return new ClientStreamingHandler<>(deltaType, OpenAiError.class, FACTORY) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null)); } catch (final IOException e) { diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java index 20e9b29e3..296e1a6a1 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java @@ -2,6 +2,7 @@ import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientException; +import com.sap.ai.sdk.core.common.ClientExceptionFactory; import com.sap.ai.sdk.foundationmodels.openai.generated.model.ErrorResponse; import javax.annotation.Nullable; import lombok.experimental.StandardException; @@ -10,6 +11,9 @@ @StandardException public class OpenAiClientException extends ClientException { + static final ClientExceptionFactory FACTORY = + (message, error, cause) -> new OpenAiClientException(message, cause).setClientError(error); + /** * Retrieves the {@link ErrorResponse} from the OpenAI service, if available. * diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java deleted file mode 100644 index 57718b171..000000000 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiExceptionFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sap.ai.sdk.foundationmodels.openai; - -import com.sap.ai.sdk.core.common.ClientExceptionFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -class OpenAiExceptionFactory implements ClientExceptionFactory { - - @Nonnull - @Override - public OpenAiClientException build( - @Nonnull final String message, - @Nullable final OpenAiError clientError, - @Nullable final Throwable cause) { - return new OpenAiClientException(message, cause).setClientError(clientError); - } -} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 255b8bb30..3ed440048 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -2,8 +2,15 @@ import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientException; +import com.sap.ai.sdk.core.common.ClientExceptionFactory; +import com.sap.ai.sdk.orchestration.OrchestrationFilterException.Input; import com.sap.ai.sdk.orchestration.model.ErrorResponse; +import com.sap.ai.sdk.orchestration.model.GenericModuleResult; +import com.sap.ai.sdk.orchestration.model.ModuleResults; +import java.util.Collections; +import java.util.Map; import java.util.Optional; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.experimental.StandardException; @@ -11,6 +18,28 @@ @StandardException public class OrchestrationClientException extends ClientException { + static final ClientExceptionFactory FACTORY = + (message, clientError, cause) -> { + final var details = extractInputFilterDetails(clientError); + if (details.isEmpty()) { + return new OrchestrationClientException(message, cause).setClientError(clientError); + } + return new Input(message, cause).setFilterDetails(details).setClientError(clientError); + }; + + @SuppressWarnings("unchecked") + @Nonnull + static Map extractInputFilterDetails(@Nullable final OrchestrationError error) { + return Optional.ofNullable(error) + .map(OrchestrationError::getErrorResponse) + .map(ErrorResponse::getModuleResults) + .map(ModuleResults::getInputFiltering) + .map(GenericModuleResult::getData) + .filter(Map.class::isInstance) + .map(map -> (Map) map) + .orElseGet(Collections::emptyMap); + } + /** * Retrieves the {@link ErrorResponse} from the orchestration service, if available. * diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java deleted file mode 100644 index 505fcd72b..000000000 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.sap.ai.sdk.orchestration; - -import com.sap.ai.sdk.core.common.ClientExceptionFactory; -import com.sap.ai.sdk.orchestration.model.ErrorResponse; -import com.sap.ai.sdk.orchestration.model.GenericModuleResult; -import com.sap.ai.sdk.orchestration.model.ModuleResults; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -class OrchestrationExceptionFactory - implements ClientExceptionFactory { - - @Nonnull - @Override - public OrchestrationClientException build( - @Nonnull final String message, - @Nullable final OrchestrationError clientError, - @Nullable final Throwable cause) { - final var inputFilterDetails = extractInputFilterDetails(clientError); - if (!inputFilterDetails.isEmpty()) { - return new OrchestrationFilterException.Input(message, cause) - .setFilterDetails(inputFilterDetails) - .setClientError(clientError); - } - return new OrchestrationClientException(message, cause).setClientError(clientError); - } - - @SuppressWarnings("unchecked") - @Nonnull - private Map extractInputFilterDetails(@Nullable final OrchestrationError error) { - return Optional.ofNullable(error) - .map(OrchestrationError::getErrorResponse) - .map(ErrorResponse::getModuleResults) - .map(ModuleResults::getInputFiltering) - .map(GenericModuleResult::getData) - .filter(Map.class::isInstance) - .map(map -> (Map) map) - .orElseGet(Collections::emptyMap); - } -} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index 6f4b061f1..49751cff4 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import static com.sap.ai.sdk.orchestration.OrchestrationClientException.FACTORY; import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; @@ -26,6 +27,7 @@ @Slf4j class OrchestrationHttpExecutor { private final Supplier destinationSupplier; + private static final ObjectMapper JACKSON = getOrchestrationObjectMapper(); OrchestrationHttpExecutor(@Nonnull final Supplier destinationSupplier) @@ -47,8 +49,7 @@ T execute( val client = getHttpClient(); val handler = - new ClientResponseHandler<>( - responseType, OrchestrationError.class, new OrchestrationExceptionFactory()) + new ClientResponseHandler<>(responseType, OrchestrationError.class, FACTORY) .objectMapper(JACKSON); return client.execute(request, handler); @@ -74,9 +75,7 @@ Stream stream(@Nonnull final Object payload) { val client = getHttpClient(); return new ClientStreamingHandler<>( - OrchestrationChatCompletionDelta.class, - OrchestrationError.class, - new OrchestrationExceptionFactory()) + OrchestrationChatCompletionDelta.class, OrchestrationError.class, FACTORY) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null)); From 825b9c1cd7ee40e86924eed82f0da2e81f47bfc1 Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Wed, 6 Aug 2025 13:11:30 +0000 Subject: [PATCH 09/21] Formatting --- .../java/com/sap/ai/sdk/app/controllers/OpenAiController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java index 393ef85fb..76a3415a1 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java @@ -7,7 +7,6 @@ import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiUsage; import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors; import java.io.IOException; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nonnull; import javax.annotation.Nullable; From 59cac0fb8a76432e853058ce44cd91942067365b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 15:41:18 +0200 Subject: [PATCH 10/21] Minor Code cleanup --- .../core/common/ClientResponseHandler.java | 7 +-- .../core/common/ClientStreamingHandler.java | 5 +-- .../core/common/IterableStreamConverter.java | 45 +++++++++---------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java index a81d28e63..0473b340a 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientResponseHandler.java @@ -82,13 +82,10 @@ private T parseSuccess(@Nonnull final ClassicHttpResponse response) throws E { throw exceptionFactory.build("The HTTP Response is empty").setHttpResponse(response); } + val message = "Failed to parse response entity."; val content = tryGetContent(responseEntity) - .getOrElseThrow( - e -> - exceptionFactory - .build("Failed to parse response entity.", e) - .setHttpResponse(response)); + .getOrElseThrow(e -> exceptionFactory.build(message, e).setHttpResponse(response)); try { return objectMapper.readValue(content, successType); } catch (final JsonProcessingException e) { diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java index 10f1ee2cf..70b684a6e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java @@ -80,9 +80,8 @@ public Stream handleStreamingResponse(@Nonnull final ClassicHttpResponse resp return objectMapper.readValue(data, successType); } catch (final IOException e) { // exception message e gets lost log.error("Failed to parse delta chunk to type {}", successType); - throw exceptionFactory - .build("Failed to parse delta chunk", e) - .setHttpResponse(response); + final String message = "Failed to parse delta chunk"; + throw exceptionFactory.build(message, e).setHttpResponse(response); } }); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java b/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java index 5fc95b7db..12ec15fac 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/IterableStreamConverter.java @@ -6,8 +6,6 @@ import io.vavr.control.Try; import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.util.Iterator; import java.util.NoSuchElementException; @@ -36,6 +34,10 @@ class IterableStreamConverter implements Iterator { /** see DEFAULT_CHAR_BUFFER_SIZE in {@link BufferedReader} * */ static final int BUFFER_SIZE = 8192; + private static final String ERR_CONTENT = "Failed to read response content."; + private static final String ERR_INTERRUPTED = "Parsing response content was interrupted"; + private static final String ERR_CLOSE = "Could not close input stream with error: {} (ignored)"; + /** Read next entry for Stream or {@code null} when no further entry can be read. */ private final Callable readHandler; @@ -43,7 +45,7 @@ class IterableStreamConverter implements Iterator { private final Runnable stopHandler; /** Error handler to be called when Stream is interrupted. */ - private final Function errorHandler; + private final Function errorHandler; private boolean isDone = false; private boolean isNextFetched = false; @@ -85,8 +87,8 @@ public T next() { /** * Create a sequential Stream of lines from an HTTP response string (UTF-8). The underlying {@link - * InputStream} is closed, when the resulting Stream is closed (e.g. via try-with-resources) or - * when an exception occurred. + * java.io.InputStream} is closed, when the resulting Stream is closed (e.g. via + * try-with-resources) or when an exception occurred. * * @param response The HTTP response object. * @param exceptionFactory The exception factory to use for creating exceptions. @@ -103,29 +105,26 @@ static Stream lines( throw exceptionFactory.build("The HTTP Response is empty").setHttpResponse(response); } - final InputStream inputStream; - try { - inputStream = response.getEntity().getContent(); - } catch (final IOException e) { - throw exceptionFactory.build("Failed to read response content.", e).setHttpResponse(response); - } + // access input stream + final var inputStream = + Try.of(() -> response.getEntity().getContent()) + .getOrElseThrow(e -> exceptionFactory.build(ERR_CONTENT, e).setHttpResponse(response)); + // initialize buffered reader final var reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8), BUFFER_SIZE); + + // define close handler final Runnable closeHandler = - () -> - Try.run(reader::close) - .onFailure( - e -> - log.debug( - "Could not close input stream with error: {} (ignored)", - e.getClass().getSimpleName())); - final Function errHandler = - e -> - exceptionFactory - .build("Parsing response content was interrupted", e) - .setHttpResponse(response); + () -> Try.run(reader::close).onFailure(e -> log.debug(ERR_CLOSE, e.getClass())); + + // define error handler + final Function errHandler = + e -> exceptionFactory.build(ERR_INTERRUPTED, e).setHttpResponse(response); + // initialize lazy stream iterator final var iterator = new IterableStreamConverter<>(reader::readLine, closeHandler, errHandler); + + // create lazy stream as output final var spliterator = Spliterators.spliteratorUnknownSize(iterator, ORDERED | NONNULL); return StreamSupport.stream(spliterator, /* NOT PARALLEL */ false).onClose(closeHandler); } From 2583f2cebf8699dce69ab859c5d6656ffc58c816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 6 Aug 2025 15:43:37 +0200 Subject: [PATCH 11/21] Add/Update release note --- docs/release_notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release_notes.md b/docs/release_notes.md index 383d64f75..8a6199f69 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -17,7 +17,9 @@ ### ✨ New Functionality - [Core] Added `ClientExceptionFactory` interface to provide custom exception mapping logic for different service clients. -- Extend `OpenAiClientException` and `OrchestrationClientException` to retrieve error diagnostics information received from remote service using `getErrorResponse`. +- Extend `OpenAiClientException` and `OrchestrationClientException` to retrieve error diagnostics information received from remote service. + New available accessors for troubleshooting: `getErrorResponse()`, `getHttpResponse()` and , `getHttpRequest()`. + Please note: depending on the error response, these methods may return `null` if the information is not available. - [Orchestration] Introduced filtering related exceptions along with convenience methods to obtain additional contextual information. - `OrchestrationInputFilterException` for prompt filtering and `OrchestrationOutputFilterException` for response filtering. - `getFilterDetails()`: Returns a map of all filter details. From 34e7ec40e105a4b5c94d3931a2d13949510831bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 12 Aug 2025 18:06:37 +0200 Subject: [PATCH 12/21] Fix merge conflict --- .../OrchestrationClientException.java | 30 ++++++++++++++----- .../OrchestrationExceptionFactory.java | 0 .../OrchestrationHttpExecutor.java | 9 ++---- 3 files changed, 24 insertions(+), 15 deletions(-) delete mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 30d46c844..f445f0620 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -4,10 +4,13 @@ import com.sap.ai.sdk.core.common.ClientException; import com.sap.ai.sdk.core.common.ClientExceptionFactory; import com.sap.ai.sdk.orchestration.OrchestrationFilterException.Input; +import com.sap.ai.sdk.orchestration.model.Error; import com.sap.ai.sdk.orchestration.model.ErrorResponse; import com.sap.ai.sdk.orchestration.model.ErrorResponseStreaming; +import com.sap.ai.sdk.orchestration.model.ErrorStreaming; import com.sap.ai.sdk.orchestration.model.GenericModuleResult; import com.sap.ai.sdk.orchestration.model.ModuleResults; +import com.sap.ai.sdk.orchestration.model.ModuleResultsStreaming; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -31,14 +34,25 @@ public class OrchestrationClientException extends ClientException { @SuppressWarnings("unchecked") @Nonnull static Map extractInputFilterDetails(@Nullable final OrchestrationError error) { - return Optional.ofNullable(error) - .map(OrchestrationError::getErrorResponse) - .map(ErrorResponse::getModuleResults) - .map(ModuleResults::getInputFiltering) - .map(GenericModuleResult::getData) - .filter(Map.class::isInstance) - .map(map -> (Map) map) - .orElseGet(Collections::emptyMap); + if (error instanceof OrchestrationError.Synchronous synchronousError) { + return Optional.of(synchronousError.getErrorResponse()) + .map(ErrorResponse::getError) + .map(Error::getIntermediateResults) + .map(ModuleResults::getInputFiltering) + .map(GenericModuleResult::getData) + .map(map -> (Map) map) + .orElseGet(Collections::emptyMap); + } else if (error instanceof OrchestrationError.Streaming streamingError) { + return Optional.of(streamingError.getErrorResponse()) + .map(ErrorResponseStreaming::getError) + .map(ErrorStreaming::getIntermediateResults) + .map(ModuleResultsStreaming::getInputFiltering) + .map(GenericModuleResult::getData) + .filter(Map.class::isInstance) + .map(map -> (Map) map) + .orElseGet(Collections::emptyMap); + } + return Collections.emptyMap(); } /** diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index ae665fa01..59954c929 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -49,10 +49,7 @@ T execute( val client = getHttpClient(); val handler = - new ClientResponseHandler<>( - responseType, - OrchestrationError.Synchronous.class, - FACTORY) + new ClientResponseHandler<>(responseType, OrchestrationError.Synchronous.class, FACTORY) .objectMapper(JACKSON); return client.execute(request, handler); @@ -79,9 +76,7 @@ Stream stream( val client = getHttpClient(); return new ClientStreamingHandler<>( - OrchestrationChatCompletionDelta.class, - OrchestrationError.Streaming.class, - FACTORY) + OrchestrationChatCompletionDelta.class, OrchestrationError.Streaming.class, FACTORY) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null)); From 43bec202679b04276c705b4b450702477cc501b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 12 Aug 2025 18:13:50 +0200 Subject: [PATCH 13/21] Establish public getMessage() in interface --- .../ai/sdk/core/common/ClientException.java | 2 ++ .../OrchestrationClientException.java | 13 ++++++++---- .../sdk/orchestration/OrchestrationError.java | 20 +++++++++---------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java index 41342a5ba..251bb9c00 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java +++ b/core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java @@ -21,6 +21,8 @@ public class ClientException extends RuntimeException { /** * Wraps a structured error payload received from the remote service, if available. This can be * used to extract more detailed error information. + * + * @since 1.10.0 */ @Nullable @Getter(onMethod_ = @Beta, value = AccessLevel.PUBLIC) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index f445f0620..013452b18 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -55,6 +55,12 @@ static Map extractInputFilterDetails(@Nullable final Orchestrati return Collections.emptyMap(); } + @Override + @Nullable + public OrchestrationError getClientError() { + return (OrchestrationError) super.getClientError(); + } + /** * Retrieves the {@link ErrorResponse} from the orchestration service, if available. * @@ -64,13 +70,13 @@ static Map extractInputFilterDetails(@Nullable final Orchestrati @Beta @Nullable public ErrorResponse getErrorResponse() { - final var clientError = super.getClientError(); - if (clientError instanceof OrchestrationError.Synchronous orchestrationError) { + if (getClientError() instanceof OrchestrationError.Synchronous orchestrationError) { return orchestrationError.getErrorResponse(); } return null; } + /** * Retrieves the {@link ErrorResponseStreaming} from the orchestration service, if available. * @@ -80,8 +86,7 @@ public ErrorResponse getErrorResponse() { @Beta @Nullable public ErrorResponseStreaming getErrorResponseStreaming() { - final var clientError = super.getClientError(); - if (clientError instanceof OrchestrationError.Streaming orchestrationError) { + if (getClientError() instanceof OrchestrationError.Streaming orchestrationError) { return orchestrationError.getErrorResponse(); } return null; diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java index ad6b9256e..83860fcde 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java @@ -20,6 +20,14 @@ @Beta public interface OrchestrationError extends ClientError { + /** + * Gets the error message from the orchestration error. + * + * @return the error message + */ + @Nonnull + String getMessage(); + /** * Orchestration error response for synchronous requests. * @@ -30,11 +38,7 @@ public interface OrchestrationError extends ClientError { class Synchronous implements OrchestrationError { ErrorResponse errorResponse; - /** - * Gets the error message from the contained original response. - * - * @return the error message - */ + @Override @Nonnull public String getMessage() { final Error e = errorResponse.getError(); @@ -53,11 +57,7 @@ public String getMessage() { class Streaming implements OrchestrationError { ErrorResponseStreaming errorResponse; - /** - * Gets the error message from the contained original response. - * - * @return the error message - */ + @Override @Nonnull public String getMessage() { final ErrorStreaming e = errorResponse.getError(); From 03966730fe24b581886303ef0b841da9f51d3ebd Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Tue, 12 Aug 2025 16:14:28 +0000 Subject: [PATCH 14/21] Formatting --- .../sap/ai/sdk/orchestration/OrchestrationClientException.java | 1 - 1 file changed, 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 013452b18..350679e4d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -76,7 +76,6 @@ public ErrorResponse getErrorResponse() { return null; } - /** * Retrieves the {@link ErrorResponseStreaming} from the orchestration service, if available. * From 6d068995a8a1457df2087e9504ce8982720f9163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 12 Aug 2025 18:18:02 +0200 Subject: [PATCH 15/21] Remove duplicate --- .../com/sap/ai/sdk/orchestration/OrchestrationError.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java index 83860fcde..c33eccb4f 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java @@ -19,15 +19,6 @@ */ @Beta public interface OrchestrationError extends ClientError { - - /** - * Gets the error message from the orchestration error. - * - * @return the error message - */ - @Nonnull - String getMessage(); - /** * Orchestration error response for synchronous requests. * From 65f6a4ec62ecc1c6be8fd6e76808bff45be53777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 13 Aug 2025 14:06:31 +0200 Subject: [PATCH 16/21] Initial --- orchestration/pom.xml | 2 +- .../OrchestrationChatResponse.java | 8 +- .../orchestration/OrchestrationClient.java | 5 +- .../OrchestrationClientException.java | 286 ++++++++++++++---- .../OrchestrationFilterException.java | 90 ------ .../OrchestrationHttpExecutor.java | 10 +- .../orchestration/OrchestrationUnitTest.java | 47 ++- .../controllers/OrchestrationController.java | 12 +- .../app/controllers/OrchestrationTest.java | 15 +- 9 files changed, 276 insertions(+), 199 deletions(-) delete mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java diff --git a/orchestration/pom.xml b/orchestration/pom.xml index f87821a45..f976147b5 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -41,7 +41,7 @@ 94% 75% 93% - 100% + 97% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java index 100291936..5ab354c16 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatResponse.java @@ -35,16 +35,18 @@ public class OrchestrationChatResponse { *

Note: If there are multiple choices only the first one is returned * * @return the message content or empty string. - * @throws OrchestrationFilterException.Output if the content filter filtered the output. + * @throws OrchestrationClientException.Synchronous.OutputFilter if the content filter filtered + * the output. */ @Nonnull - public String getContent() throws OrchestrationFilterException.Output { + public String getContent() throws OrchestrationClientException.Synchronous.OutputFilter { final var choice = getChoice(); if ("content_filter".equals(choice.getFinishReason())) { final var filterDetails = Try.of(this::getOutputFilteringChoices).getOrElseGet(e -> Map.of()); final var message = "Content filter filtered the output."; - throw new OrchestrationFilterException.Output(message).setFilterDetails(filterDetails); + throw new OrchestrationClientException.Synchronous.OutputFilter(message) + .setFilterDetails(filterDetails); } return choice.getMessage().getContent(); } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java index d7e165cdb..71e9d601f 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java @@ -116,13 +116,14 @@ public Stream streamChatCompletion( } private static void throwOnContentFilter(@Nonnull final OrchestrationChatCompletionDelta delta) - throws OrchestrationFilterException.Output { + throws OrchestrationClientException.Streaming.OutputFilter { final String finishReason = delta.getFinishReason(); if (finishReason != null && finishReason.equals("content_filter")) { final var filterDetails = Try.of(() -> getOutputFilteringChoices(delta)).getOrElseGet(e -> Map.of()); final var message = "Content filter filtered the output."; - throw new OrchestrationFilterException.Output(message).setFilterDetails(filterDetails); + throw new OrchestrationClientException.Streaming.OutputFilter(message) + .setFilterDetails(filterDetails); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 350679e4d..29d253af7 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -1,49 +1,65 @@ package com.sap.ai.sdk.orchestration; +import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.common.ClientException; import com.sap.ai.sdk.core.common.ClientExceptionFactory; -import com.sap.ai.sdk.orchestration.OrchestrationFilterException.Input; +import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInput; +import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutput; import com.sap.ai.sdk.orchestration.model.Error; import com.sap.ai.sdk.orchestration.model.ErrorResponse; import com.sap.ai.sdk.orchestration.model.ErrorResponseStreaming; import com.sap.ai.sdk.orchestration.model.ErrorStreaming; import com.sap.ai.sdk.orchestration.model.GenericModuleResult; +import com.sap.ai.sdk.orchestration.model.LlamaGuard38b; import com.sap.ai.sdk.orchestration.model.ModuleResults; import com.sap.ai.sdk.orchestration.model.ModuleResultsStreaming; +import io.vavr.control.Option; import java.util.Collections; import java.util.Map; import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import lombok.experimental.StandardException; /** Exception thrown by the {@link OrchestrationClient} in case of an error. */ @StandardException public class OrchestrationClientException extends ClientException { + private static final ObjectMapper MAPPER = getOrchestrationObjectMapper(); - static final ClientExceptionFactory FACTORY = - (message, clientError, cause) -> { - final var details = extractInputFilterDetails(clientError); - if (details.isEmpty()) { - return new OrchestrationClientException(message, cause).setClientError(clientError); - } - return new Input(message, cause).setFilterDetails(details).setClientError(clientError); - }; - - @SuppressWarnings("unchecked") + /** Details about the filters that caused the exception. */ + @Setter(AccessLevel.PACKAGE) + @Getter(AccessLevel.PACKAGE) + @Accessors(chain = true) @Nonnull - static Map extractInputFilterDetails(@Nullable final OrchestrationError error) { - if (error instanceof OrchestrationError.Synchronous synchronousError) { - return Optional.of(synchronousError.getErrorResponse()) - .map(ErrorResponse::getError) - .map(Error::getIntermediateResults) - .map(ModuleResults::getInputFiltering) - .map(GenericModuleResult::getData) - .map(map -> (Map) map) - .orElseGet(Collections::emptyMap); - } else if (error instanceof OrchestrationError.Streaming streamingError) { - return Optional.of(streamingError.getErrorResponse()) + protected Map filterDetails = Map.of(); + + /** Exception thrown during a streaming invocation. */ + @StandardException + public static class Streaming extends OrchestrationClientException { + static final ClientExceptionFactory FACTORY = + (message, clientError, cause) -> { + final var details = extractInputFilterDetails(clientError); + if (details.isEmpty()) { + return new Streaming(message, cause).setClientError(clientError); + } + return new InputFilter(message, cause) + .setFilterDetails(details) + .setClientError(clientError); + }; + + @SuppressWarnings("unchecked") + @Nonnull + private static Map extractInputFilterDetails( + @Nullable final OrchestrationError.Streaming error) { + return Optional.ofNullable(error) + .map(OrchestrationError.Streaming::getErrorResponse) .map(ErrorResponseStreaming::getError) .map(ErrorStreaming::getIntermediateResults) .map(ModuleResultsStreaming::getInputFiltering) @@ -52,57 +68,205 @@ static Map extractInputFilterDetails(@Nullable final Orchestrati .map(map -> (Map) map) .orElseGet(Collections::emptyMap); } - return Collections.emptyMap(); - } - @Override - @Nullable - public OrchestrationError getClientError() { - return (OrchestrationError) super.getClientError(); + /** + * Retrieves the {@link ErrorResponseStreaming} from the orchestration service, if available. + * + * @return The {@link ErrorResponseStreaming} object, or {@code null} if not available. + * @since 1.10.0 + */ + @Beta + @Nullable + public ErrorResponseStreaming getErrorResponse() { + return Option.of(getClientError()) + .map(OrchestrationError.Streaming::getErrorResponse) + .getOrNull(); + } + + /** + * Retrieves the client error details from the orchestration service, if available. + * + * @return The {@link OrchestrationError.Streaming} object, or {@code null} if not available. + * @since 1.10.0 + */ + @Override + public OrchestrationError.Streaming getClientError() { + return super.getClientError() instanceof OrchestrationError.Streaming e ? e : null; + } + + /** Exception thrown during a streaming invocation that contains input filter details. */ + @Beta + @StandardException + public static class InputFilter extends Streaming implements Filter.Input { + @Nonnull + @Override + public Map getFilterDetails() { + return super.getFilterDetails(); + } + } + + /** Exception thrown during a streaming invocation that contains output filter details. */ + @Beta + @StandardException + public static class OutputFilter extends Streaming implements Filter.Output { + @Nonnull + @Override + public Map getFilterDetails() { + return super.getFilterDetails(); + } + } } - /** - * Retrieves the {@link ErrorResponse} from the orchestration service, if available. - * - * @return The {@link ErrorResponse} object, or {@code null} if not available. - * @since 1.10.0 - */ - @Beta - @Nullable - public ErrorResponse getErrorResponse() { - if (getClientError() instanceof OrchestrationError.Synchronous orchestrationError) { - return orchestrationError.getErrorResponse(); + /** Exception thrown during a synchronous invocation. */ + @StandardException + public static class Synchronous extends OrchestrationClientException { + static final ClientExceptionFactory FACTORY = + (message, clientError, cause) -> { + final var details = extractInputFilterDetails(clientError); + if (details.isEmpty()) { + return new Synchronous(message, cause).setClientError(clientError); + } + return new InputFilter(message, cause) + .setFilterDetails(details) + .setClientError(clientError); + }; + + @SuppressWarnings("unchecked") + @Nonnull + private static Map extractInputFilterDetails( + @Nullable final OrchestrationError.Synchronous error) { + return Optional.ofNullable(error) + .map(OrchestrationError.Synchronous::getErrorResponse) + .map(ErrorResponse::getError) + .map(Error::getIntermediateResults) + .map(ModuleResults::getInputFiltering) + .map(GenericModuleResult::getData) + .map(map -> (Map) map) + .orElseGet(Collections::emptyMap); + } + + /** + * Retrieves the {@link ErrorResponse} from the orchestration service, if available. + * + * @return The {@link ErrorResponse} object, or {@code null} if not available. + * @since 1.10.0 + */ + @Beta + @Nullable + public ErrorResponse getErrorResponse() { + return Option.of(getClientError()) + .map(OrchestrationError.Synchronous::getErrorResponse) + .getOrNull(); + } + + /** + * Retrieves the client error details from the orchestration service, if available. + * + * @return The {@link OrchestrationError.Synchronous} object, or {@code null} if not available. + * @since 1.10.0 + */ + @Override + public OrchestrationError.Synchronous getClientError() { + return super.getClientError() instanceof OrchestrationError.Synchronous e ? e : null; + } + + /** Exception thrown during a synchronous invocation that contains input filter details. */ + @Beta + @StandardException + public static class InputFilter extends Synchronous implements Filter.Input { + @Nonnull + @Override + public Map getFilterDetails() { + return super.getFilterDetails(); + } + + /** + * Retrieves the HTTP status code from the original error response, if available. + * + * @return the HTTP status code, or {@code null} if not available + * @since 1.10.0 + */ + @Beta + @Nullable + public Integer getStatusCode() { + return Optional.ofNullable(getErrorResponse()) + .map(e -> e.getError().getCode()) + .orElse(null); + } + } + + /** Exception thrown during a synchronous invocation that contains output filter details. */ + @Beta + @StandardException + public static class OutputFilter extends Synchronous implements Filter.Output { + @Nonnull + @Override + public Map getFilterDetails() { + return super.getFilterDetails(); + } } - return null; } /** - * Retrieves the {@link ErrorResponseStreaming} from the orchestration service, if available. - * - * @return The {@link ErrorResponseStreaming} object, or {@code null} if not available. - * @since 1.10.0 + * Interface representing the filter details that can be included in an orchestration error + * response. */ @Beta - @Nullable - public ErrorResponseStreaming getErrorResponseStreaming() { - if (getClientError() instanceof OrchestrationError.Streaming orchestrationError) { - return orchestrationError.getErrorResponse(); + interface Filter { + /** + * Retrieves the filter details as a map. + * + * @return a map containing the filter details. + */ + @Nonnull + Map getFilterDetails(); + + /** + * Retrieves the Azure Content Safety input filter details, if available. + * + * @return the {@link AzureContentSafetyInput} object, or {@code null} if not available. + */ + @Nullable + default LlamaGuard38b getLlamaGuard38b() { + return Optional.ofNullable(getFilterDetails().get("llama_guard_3_8b")) + .map(obj -> MAPPER.convertValue(obj, LlamaGuard38b.class)) + .orElse(null); + } + + /** Interface for input filters that can be included in an orchestration error response. */ + interface Input extends Filter { + /** + * Retrieves the Azure Content Safety input filter details, if available. + * + * @return the {@link AzureContentSafetyInput} object, or {@code null} if not available. + */ + @Nullable + default AzureContentSafetyInput getAzureContentSafety() { + return Optional.ofNullable(getFilterDetails().get("azure_content_safety")) + .map(obj -> MAPPER.convertValue(obj, AzureContentSafetyInput.class)) + .orElse(null); + } + } + + /** Interface for output filters that can be included in an orchestration error response. */ + interface Output extends Filter { + /** + * Retrieves the Azure Content Safety output filter details, if available. + * + * @return the {@link AzureContentSafetyOutput} object, or {@code null} if not available. + */ + @Nullable + default AzureContentSafetyOutput getAzureContentSafety() { + return Optional.ofNullable(getFilterDetails().get("azure_content_safety")) + .map(obj -> MAPPER.convertValue(obj, AzureContentSafetyOutput.class)) + .orElse(null); + } } - return null; } - /** - * Retrieves the HTTP status code from the original error response, if available. - * - * @return the HTTP status code, or {@code null} if not available - * @since 1.10.0 - */ - @Beta + @Override @Nullable - public Integer getStatusCode() { - return Optional.ofNullable(getErrorResponse()) - .map(ErrorResponse::getError) - .map(Error::getCode) - .orElse(null); + public OrchestrationError getClientError() { + return (OrchestrationError) super.getClientError(); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java deleted file mode 100644 index c62d74fa2..000000000 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.sap.ai.sdk.orchestration; - -import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; - -import com.google.common.annotations.Beta; -import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInput; -import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutput; -import com.sap.ai.sdk.orchestration.model.LlamaGuard38b; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import lombok.experimental.StandardException; - -/** Base exception for errors occurring during orchestration filtering. */ -@Beta -@StandardException(access = AccessLevel.PRIVATE) -public class OrchestrationFilterException extends OrchestrationClientException { - - /** Details about the filters that caused the exception. */ - @Accessors(chain = true) - @Setter(AccessLevel.PACKAGE) - @Getter - @Nonnull - protected Map filterDetails = Map.of(); - - /** - * Retrieves LlamaGuard 3.8b details from {@code filterDetails}, if present. - * - * @return The LlamaGuard38b object, or {@code null} if not found or conversion fails. - * @throws IllegalArgumentException if the conversion of filter details to {@link LlamaGuard38b} - * fails due to invalid content. - */ - @Nullable - public LlamaGuard38b getLlamaGuard38b() { - return Optional.ofNullable(filterDetails.get("llama_guard_3_8b")) - .map(obj -> getOrchestrationObjectMapper().convertValue(obj, LlamaGuard38b.class)) - .orElse(null); - } - - /** Exception thrown when an error occurs during input filtering. */ - @StandardException - public static class Input extends OrchestrationFilterException { - - /** - * Retrieves Azure Content Safety input details from {@code filterDetails}, if present. - * - * @return The AzureContentSafetyInput object, or {@code null} if not found or conversion fails. - * @throws IllegalArgumentException if the conversion of filter details to {@link - * AzureContentSafetyInput} fails due to invalid content. - */ - @Nullable - public AzureContentSafetyInput getAzureContentSafetyInput() { - return Optional.ofNullable(filterDetails.get("azure_content_safety")) - .map( - obj -> - getOrchestrationObjectMapper().convertValue(obj, AzureContentSafetyInput.class)) - .orElse(null); - } - } - - /** - * Exception thrown when an error occurs during output filtering, specifically when the finish - * reason is a content filter. - */ - @StandardException - public static class Output extends OrchestrationFilterException { - - /** - * Retrieves Azure Content Safety output details from {@code filterDetails}, if present. - * - * @return The AzureContentSafetyOutput object, or {@code null} if not found or conversion - * fails. - * @throws IllegalArgumentException if the conversion of filter details to {@link - * AzureContentSafetyOutput} fails due to invalid content. - */ - @Nullable - public AzureContentSafetyOutput getAzureContentSafetyOutput() { - return Optional.ofNullable(filterDetails.get("azure_content_safety")) - .map( - obj -> - getOrchestrationObjectMapper().convertValue(obj, AzureContentSafetyOutput.class)) - .orElse(null); - } - } -} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index 59954c929..f25579f03 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.orchestration; -import static com.sap.ai.sdk.orchestration.OrchestrationClientException.FACTORY; import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; @@ -8,6 +7,8 @@ import com.sap.ai.sdk.core.DeploymentResolutionException; import com.sap.ai.sdk.core.common.ClientResponseHandler; import com.sap.ai.sdk.core.common.ClientStreamingHandler; +import com.sap.ai.sdk.orchestration.OrchestrationClientException.Streaming; +import com.sap.ai.sdk.orchestration.OrchestrationClientException.Synchronous; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; @@ -49,7 +50,8 @@ T execute( val client = getHttpClient(); val handler = - new ClientResponseHandler<>(responseType, OrchestrationError.Synchronous.class, FACTORY) + new ClientResponseHandler<>( + responseType, OrchestrationError.Synchronous.class, Synchronous.FACTORY) .objectMapper(JACKSON); return client.execute(request, handler); @@ -76,7 +78,9 @@ Stream stream( val client = getHttpClient(); return new ClientStreamingHandler<>( - OrchestrationChatCompletionDelta.class, OrchestrationError.Streaming.class, FACTORY) + OrchestrationChatCompletionDelta.class, + OrchestrationError.Streaming.class, + Streaming.FACTORY) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null)); diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 060ca8430..fea7b08e5 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -379,12 +379,11 @@ void testBadRequest() { assertThatThrownBy(() -> client.chatCompletion(prompt, config)) .isInstanceOfSatisfying( - OrchestrationClientException.class, + OrchestrationClientException.Synchronous.class, e -> { assertThat(e.getMessage()) .isEqualTo( "Request failed with status 400 (Bad Request): Missing required parameters: ['input']"); - assertThat(e.getErrorResponseStreaming()).isNull(); assertThat(e.getErrorResponse()).isNotNull(); assertThat(e.getErrorResponse().getError().getMessage()) .isEqualTo("Missing required parameters: ['input']"); @@ -449,7 +448,7 @@ void inputFilteringStrict() { assertThatThrownBy(() -> client.chatCompletion(prompt, configWithFilter)) .isInstanceOfSatisfying( - OrchestrationFilterException.Input.class, + OrchestrationClientException.Synchronous.InputFilter.class, e -> { assertThat(e.getMessage()) .isEqualTo( @@ -475,11 +474,11 @@ void inputFilteringStrict() { .isEqualTo( "400 - Filtering Module - Input Filter: Prompt filtered due to safety violations. Please modify the prompt and try again."); - assertThat(e.getAzureContentSafetyInput()).isNotNull(); - assertThat(e.getAzureContentSafetyInput().getHate()).isEqualTo(NUMBER_6); - assertThat(e.getAzureContentSafetyInput().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyInput().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyInput().getViolence()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafety()).isNotNull(); + assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_6); assertThat(e.getLlamaGuard38b()).isNotNull(); assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue(); @@ -503,7 +502,7 @@ void outputFilteringStrict() { assertThatThrownBy(client.chatCompletion(prompt, configWithFilter)::getContent) .isInstanceOfSatisfying( - OrchestrationFilterException.Output.class, + OrchestrationClientException.Synchronous.OutputFilter.class, e -> { assertThat(e.getMessage()).isEqualTo("Content filter filtered the output."); assertThat(e.getFilterDetails()) @@ -520,13 +519,12 @@ void outputFilteringStrict() { "llama_guard_3_8b", Map.of("violent_crimes", true))); assertThat(e.getErrorResponse()).isNull(); - assertThat(e.getStatusCode()).isNull(); - assertThat(e.getAzureContentSafetyOutput()).isNotNull(); - assertThat(e.getAzureContentSafetyOutput().getHate()).isEqualTo(NUMBER_6); - assertThat(e.getAzureContentSafetyOutput().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyOutput().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyOutput().getViolence()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafety()).isNotNull(); + assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_6); assertThat(e.getLlamaGuard38b()).isNotNull(); assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue(); @@ -759,9 +757,10 @@ void testThrowsOnContentFilter() { // this must not throw, since the stream is lazily evaluated var stream = mock.streamChatCompletion(new OrchestrationPrompt(""), config); assertThatThrownBy(stream::toList) - .isInstanceOf(OrchestrationFilterException.Output.class) + .isInstanceOf(OrchestrationClientException.Streaming.OutputFilter.class) .hasMessage("Content filter filtered the output.") - .extracting(e -> ((OrchestrationFilterException.Output) e).getFilterDetails()) + .extracting( + e -> ((OrchestrationClientException.Streaming.OutputFilter) e).getFilterDetails()) .isEqualTo(Map.of("azure_content_safety", Map.of("hate", 0, "self_harm", 0))); } @@ -785,11 +784,9 @@ void streamChatCompletionOutputFilterErrorHandling() throws IOException { assertThatThrownBy(() -> stream.forEach(System.out::println)) .hasMessage("Content filter filtered the output.") .isInstanceOfSatisfying( - OrchestrationFilterException.Output.class, + OrchestrationClientException.Streaming.OutputFilter.class, e -> { assertThat(e.getErrorResponse()).isNull(); - assertThat(e.getErrorResponseStreaming()).isNull(); - assertThat(e.getStatusCode()).isNull(); assertThat(e.getFilterDetails()) .isEqualTo( @@ -799,11 +796,11 @@ void streamChatCompletionOutputFilterErrorHandling() throws IOException { "azure_content_safety", Map.of("Hate", 0, "SelfHarm", 0, "Sexual", 0, "Violence", 4))); - assertThat(e.getAzureContentSafetyOutput()).isNotNull(); - assertThat(e.getAzureContentSafetyOutput().getHate()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyOutput().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyOutput().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafetyOutput().getViolence()).isEqualTo(NUMBER_4); + assertThat(e.getAzureContentSafety()).isNotNull(); + assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_4); }); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 6f1d48b1f..8c6d232e8 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -5,7 +5,7 @@ import com.sap.ai.sdk.app.services.OrchestrationService; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; import com.sap.ai.sdk.orchestration.OrchestrationChatResponse; -import com.sap.ai.sdk.orchestration.OrchestrationFilterException; +import com.sap.ai.sdk.orchestration.OrchestrationClientException; import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInput; import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutput; import com.sap.ai.sdk.orchestration.model.DPIEntities; @@ -127,13 +127,13 @@ Object inputFiltering( final OrchestrationChatResponse response; try { response = service.inputFiltering(policy); - } catch (OrchestrationFilterException.Input e) { + } catch (OrchestrationClientException.Synchronous.InputFilter e) { final var msg = new StringBuilder( "[Http %d] Failed to obtain a response as the content was flagged by input filter. " .formatted(e.getStatusCode())); - Optional.ofNullable(e.getAzureContentSafetyInput()) + Optional.ofNullable(e.getAzureContentSafety()) .map(AzureContentSafetyInput::getViolence) .filter(rating -> rating.compareTo(policy.getAzureThreshold()) > 0) .ifPresent(rating -> msg.append("Violence score %d".formatted(rating.getValue()))); @@ -159,12 +159,12 @@ Object outputFiltering( final String content; try { content = response.getContent(); - } catch (OrchestrationFilterException.Output e) { + } catch (OrchestrationClientException.Synchronous.OutputFilter e) { final var msg = new StringBuilder( "Failed to obtain a response as the content was flagged by output filter. "); - Optional.ofNullable(e.getAzureContentSafetyOutput()) + Optional.ofNullable(e.getAzureContentSafety()) .map(AzureContentSafetyOutput::getViolence) .filter(rating -> rating.compareTo(policy.getAzureThreshold()) > 0) .ifPresent(rating -> msg.append("Violence score %d ".formatted(rating.getValue()))); @@ -188,7 +188,7 @@ Object llamaGuardInputFiltering( final OrchestrationChatResponse response; try { response = service.llamaGuardInputFilter(enabled); - } catch (OrchestrationFilterException.Input e) { + } catch (OrchestrationClientException.Synchronous.InputFilter e) { var msg = "[Http %d] Failed to obtain a response as the content was flagged by input filter. " .formatted(e.getStatusCode()); diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java index 379abd476..fe6624a04 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java @@ -15,7 +15,6 @@ import com.sap.ai.sdk.orchestration.Message; import com.sap.ai.sdk.orchestration.OrchestrationClient; import com.sap.ai.sdk.orchestration.OrchestrationClientException; -import com.sap.ai.sdk.orchestration.OrchestrationFilterException; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import com.sap.ai.sdk.orchestration.OrchestrationPrompt; import com.sap.ai.sdk.orchestration.TemplateConfig; @@ -221,9 +220,9 @@ void testInputFilteringStrict() { "Prompt filtered due to safety violations. Please modify the prompt and try again.") .hasMessageContaining("400 (Bad Request)") .isInstanceOfSatisfying( - OrchestrationFilterException.Input.class, + OrchestrationClientException.Synchronous.InputFilter.class, e -> { - var actualAzureContentSafety = e.getAzureContentSafetyInput(); + var actualAzureContentSafety = e.getAzureContentSafety(); assertThat(actualAzureContentSafety).isNotNull(); assertThat(actualAzureContentSafety.getViolence()).isGreaterThan(NUMBER_0); assertThat(actualAzureContentSafety.getSelfHarm()).isEqualTo(NUMBER_0); @@ -253,9 +252,9 @@ void testOutputFilteringStrict() { assertThatThrownBy(response::getContent) .hasMessageContaining("Content filter filtered the output.") .isInstanceOfSatisfying( - OrchestrationFilterException.Output.class, + OrchestrationClientException.Synchronous.OutputFilter.class, e -> { - var actualAzureContentSafety = e.getAzureContentSafetyOutput(); + var actualAzureContentSafety = e.getAzureContentSafety(); assertThat(actualAzureContentSafety).isNotNull(); assertThat(actualAzureContentSafety.getViolence()).isGreaterThan(NUMBER_0); assertThat(actualAzureContentSafety.getSelfHarm()).isEqualTo(NUMBER_0); @@ -280,12 +279,12 @@ void testOutputFilteringLenient() { @Test void testLlamaGuardEnabled() { assertThatThrownBy(() -> service.llamaGuardInputFilter(true)) - .isInstanceOf(OrchestrationFilterException.Input.class) + .isInstanceOf(OrchestrationClientException.Synchronous.InputFilter.class) .hasMessageContaining( "Prompt filtered due to safety violations. Please modify the prompt and try again.") .hasMessageContaining("400 (Bad Request)") .isInstanceOfSatisfying( - OrchestrationFilterException.Input.class, + OrchestrationClientException.Synchronous.InputFilter.class, e -> { var llamaGuard38b = e.getLlamaGuard38b(); assertThat(llamaGuard38b).isNotNull(); @@ -419,7 +418,7 @@ void testStreamingErrorHandlingInputFilter() { val configWithFilter = config.withInputFiltering(filterConfig); assertThatThrownBy(() -> client.streamChatCompletion(prompt, configWithFilter)) - .isInstanceOf(OrchestrationFilterException.Input.class) + .isInstanceOf(OrchestrationClientException.Streaming.InputFilter.class) .hasMessageContaining("status 400 (Bad Request)") .hasMessageContaining("Filtering Module - Input Filter"); } From 466ad990190538c3dfc0d56dcfdb141374e14cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 13 Aug 2025 14:15:03 +0200 Subject: [PATCH 17/21] Fix merge conflict --- .../sap/ai/sdk/orchestration/OrchestrationFilterException.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationFilterException.java deleted file mode 100644 index e69de29bb..000000000 From 04957120a985c16578676c3f016b63c0260504ac Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Wed, 13 Aug 2025 12:18:07 +0000 Subject: [PATCH 18/21] Formatting --- .../com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index 24f08de2d..f25579f03 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.orchestration; -import static com.sap.ai.sdk.orchestration.OrchestrationClientException.FACTORY; import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; From 14df58ccea62edde5398f0a4b2f408ceb8e0b19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 13 Aug 2025 14:32:28 +0200 Subject: [PATCH 19/21] Format --- .../com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index 24f08de2d..f25579f03 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.orchestration; -import static com.sap.ai.sdk.orchestration.OrchestrationClientException.FACTORY; import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; From 763b745f1fd535303fadc4a66291de3365e5736d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 13 Aug 2025 14:54:00 +0200 Subject: [PATCH 20/21] Revert drive-by change --- .../OrchestrationClientException.java | 4 +-- .../orchestration/OrchestrationUnitTest.java | 30 +++++++++---------- .../controllers/OrchestrationController.java | 4 +-- .../app/controllers/OrchestrationTest.java | 4 +-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java index 29d253af7..def28cf2f 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java @@ -241,7 +241,7 @@ interface Input extends Filter { * @return the {@link AzureContentSafetyInput} object, or {@code null} if not available. */ @Nullable - default AzureContentSafetyInput getAzureContentSafety() { + default AzureContentSafetyInput getAzureContentSafetyInput() { return Optional.ofNullable(getFilterDetails().get("azure_content_safety")) .map(obj -> MAPPER.convertValue(obj, AzureContentSafetyInput.class)) .orElse(null); @@ -256,7 +256,7 @@ interface Output extends Filter { * @return the {@link AzureContentSafetyOutput} object, or {@code null} if not available. */ @Nullable - default AzureContentSafetyOutput getAzureContentSafety() { + default AzureContentSafetyOutput getAzureContentSafetyOutput() { return Optional.ofNullable(getFilterDetails().get("azure_content_safety")) .map(obj -> MAPPER.convertValue(obj, AzureContentSafetyOutput.class)) .orElse(null); diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index fea7b08e5..340bc4daa 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -474,11 +474,11 @@ void inputFilteringStrict() { .isEqualTo( "400 - Filtering Module - Input Filter: Prompt filtered due to safety violations. Please modify the prompt and try again."); - assertThat(e.getAzureContentSafety()).isNotNull(); - assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_6); - assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafetyInput()).isNotNull(); + assertThat(e.getAzureContentSafetyInput().getHate()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafetyInput().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyInput().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyInput().getViolence()).isEqualTo(NUMBER_6); assertThat(e.getLlamaGuard38b()).isNotNull(); assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue(); @@ -520,11 +520,11 @@ void outputFilteringStrict() { Map.of("violent_crimes", true))); assertThat(e.getErrorResponse()).isNull(); - assertThat(e.getAzureContentSafety()).isNotNull(); - assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_6); - assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafetyOutput()).isNotNull(); + assertThat(e.getAzureContentSafetyOutput().getHate()).isEqualTo(NUMBER_6); + assertThat(e.getAzureContentSafetyOutput().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyOutput().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyOutput().getViolence()).isEqualTo(NUMBER_6); assertThat(e.getLlamaGuard38b()).isNotNull(); assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue(); @@ -796,11 +796,11 @@ void streamChatCompletionOutputFilterErrorHandling() throws IOException { "azure_content_safety", Map.of("Hate", 0, "SelfHarm", 0, "Sexual", 0, "Violence", 4))); - assertThat(e.getAzureContentSafety()).isNotNull(); - assertThat(e.getAzureContentSafety().getHate()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getSelfHarm()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getSexual()).isEqualTo(NUMBER_0); - assertThat(e.getAzureContentSafety().getViolence()).isEqualTo(NUMBER_4); + assertThat(e.getAzureContentSafetyOutput()).isNotNull(); + assertThat(e.getAzureContentSafetyOutput().getHate()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyOutput().getSelfHarm()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyOutput().getSexual()).isEqualTo(NUMBER_0); + assertThat(e.getAzureContentSafetyOutput().getViolence()).isEqualTo(NUMBER_4); }); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 8c6d232e8..bb5f130ed 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -133,7 +133,7 @@ Object inputFiltering( "[Http %d] Failed to obtain a response as the content was flagged by input filter. " .formatted(e.getStatusCode())); - Optional.ofNullable(e.getAzureContentSafety()) + Optional.ofNullable(e.getAzureContentSafetyInput()) .map(AzureContentSafetyInput::getViolence) .filter(rating -> rating.compareTo(policy.getAzureThreshold()) > 0) .ifPresent(rating -> msg.append("Violence score %d".formatted(rating.getValue()))); @@ -164,7 +164,7 @@ Object outputFiltering( new StringBuilder( "Failed to obtain a response as the content was flagged by output filter. "); - Optional.ofNullable(e.getAzureContentSafety()) + Optional.ofNullable(e.getAzureContentSafetyOutput()) .map(AzureContentSafetyOutput::getViolence) .filter(rating -> rating.compareTo(policy.getAzureThreshold()) > 0) .ifPresent(rating -> msg.append("Violence score %d ".formatted(rating.getValue()))); diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java index fe6624a04..5543b6b15 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java @@ -222,7 +222,7 @@ void testInputFilteringStrict() { .isInstanceOfSatisfying( OrchestrationClientException.Synchronous.InputFilter.class, e -> { - var actualAzureContentSafety = e.getAzureContentSafety(); + var actualAzureContentSafety = e.getAzureContentSafetyInput(); assertThat(actualAzureContentSafety).isNotNull(); assertThat(actualAzureContentSafety.getViolence()).isGreaterThan(NUMBER_0); assertThat(actualAzureContentSafety.getSelfHarm()).isEqualTo(NUMBER_0); @@ -254,7 +254,7 @@ void testOutputFilteringStrict() { .isInstanceOfSatisfying( OrchestrationClientException.Synchronous.OutputFilter.class, e -> { - var actualAzureContentSafety = e.getAzureContentSafety(); + var actualAzureContentSafety = e.getAzureContentSafetyOutput(); assertThat(actualAzureContentSafety).isNotNull(); assertThat(actualAzureContentSafety.getViolence()).isGreaterThan(NUMBER_0); assertThat(actualAzureContentSafety.getSelfHarm()).isEqualTo(NUMBER_0); From 0e0424508a1ab3cd2a31220a6528c5b7a3d03133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 13 Aug 2025 14:56:13 +0200 Subject: [PATCH 21/21] Improve code style --- .../ai/sdk/orchestration/OrchestrationHttpExecutor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java index f25579f03..c65060b37 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationHttpExecutor.java @@ -7,8 +7,6 @@ import com.sap.ai.sdk.core.DeploymentResolutionException; import com.sap.ai.sdk.core.common.ClientResponseHandler; import com.sap.ai.sdk.core.common.ClientStreamingHandler; -import com.sap.ai.sdk.orchestration.OrchestrationClientException.Streaming; -import com.sap.ai.sdk.orchestration.OrchestrationClientException.Synchronous; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; @@ -51,7 +49,9 @@ T execute( val handler = new ClientResponseHandler<>( - responseType, OrchestrationError.Synchronous.class, Synchronous.FACTORY) + responseType, + OrchestrationError.Synchronous.class, + OrchestrationClientException.Synchronous.FACTORY) .objectMapper(JACKSON); return client.execute(request, handler); @@ -80,7 +80,7 @@ Stream stream( return new ClientStreamingHandler<>( OrchestrationChatCompletionDelta.class, OrchestrationError.Streaming.class, - Streaming.FACTORY) + OrchestrationClientException.Streaming.FACTORY) .objectMapper(JACKSON) .handleStreamingResponse(client.executeOpen(null, request, null));