diff --git a/core/pom.xml b/core/pom.xml index b4129db86..9a79ae4ba 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -30,6 +30,12 @@ ${project.basedir}/../ + 60% + 69% + 68% + 52% + 73% + 87% @@ -66,6 +72,10 @@ org.apache.httpcomponents.client5 httpclient5 + + org.apache.httpcomponents.core5 + httpcore5 + com.google.code.findbugs jsr305 @@ -129,11 +139,6 @@ wiremock test - - org.apache.httpcomponents.core5 - httpcore5 - test - org.assertj assertj-core diff --git a/core/src/main/java/com/sap/ai/sdk/core/commons/ClientError.java b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientError.java new file mode 100644 index 000000000..12b18210a --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientError.java @@ -0,0 +1,21 @@ +package com.sap.ai.sdk.core.commons; + +import com.google.common.annotations.Beta; +import javax.annotation.Nullable; + +/** + * Generic class that contains a JSON error response. + * + * @since 1.1.0 + */ +@Beta +@FunctionalInterface +public interface ClientError { + /** + * Get the error message. + * + * @return The error message + */ + @Nullable + String getMessage(); +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/commons/ClientException.java b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientException.java new file mode 100644 index 000000000..519fc3c30 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientException.java @@ -0,0 +1,13 @@ +package com.sap.ai.sdk.core.commons; + +import com.google.common.annotations.Beta; +import lombok.experimental.StandardException; + +/** + * Generic exception for errors occurring when using AI SDK clients. + * + * @since 1.1.0 + */ +@Beta +@StandardException +public class ClientException extends RuntimeException {} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationResponseHandler.java b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientResponseHandler.java similarity index 55% rename from orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationResponseHandler.java rename to core/src/main/java/com/sap/ai/sdk/core/commons/ClientResponseHandler.java index 10ec183c3..76320d20d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationResponseHandler.java +++ b/core/src/main/java/com/sap/ai/sdk/core/commons/ClientResponseHandler.java @@ -1,12 +1,15 @@ -package com.sap.ai.sdk.orchestration; +package com.sap.ai.sdk.core.commons; -import static com.sap.ai.sdk.orchestration.OrchestrationClient.JACKSON; +import static com.sap.ai.sdk.core.JacksonConfiguration.getDefaultObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; -import com.sap.ai.sdk.orchestration.model.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.Beta; import io.vavr.control.Try; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.function.BiFunction; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,28 +21,80 @@ import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; +/** + * Parse incoming JSON responses and handles any errors. For internal use only. + * + * @param The type of the response. + * @param The type of the exception to throw. + * @since 1.1.0 + */ +@Beta @Slf4j @RequiredArgsConstructor -class OrchestrationResponseHandler implements HttpClientResponseHandler { +public class ClientResponseHandler + implements HttpClientResponseHandler { @Nonnull private final Class responseType; + @Nonnull private final Class errorType; + @Nonnull private final BiFunction exceptionType; + /** The parses for JSON responses, will be private once we can remove mixins */ + @Nonnull public ObjectMapper JACKSON = getDefaultObjectMapper(); + + /** + * Processes a {@link ClassicHttpResponse} and returns some value corresponding to that response. + * + * @param response The response to process + * @return A model class instantiated from the response + * @throws E in case of a problem or the connection was aborted + */ + @Nonnull + @Override + public T handleResponse(@Nonnull final ClassicHttpResponse response) throws E { + if (response.getCode() >= 300) { + buildExceptionAndThrow(response); + } + return parseResponse(response); + } + + // The InputStream of the HTTP entity is closed by EntityUtils.toString + @SuppressWarnings("PMD.CloseResource") @Nonnull - private static String getContent(@Nonnull final HttpEntity entity) { + private T parseResponse(@Nonnull final ClassicHttpResponse response) throws E { + final HttpEntity responseEntity = response.getEntity(); + if (responseEntity == null) { + throw exceptionType.apply("Response was empty.", null); + } + val content = getContent(responseEntity); + log.debug("Parsing response from JSON response: {}", content); + try { + return JACKSON.readValue(content, responseType); + } catch (final JsonProcessingException e) { + log.error("Failed to parse the following response: {}", content); + throw exceptionType.apply("Failed to parse response", e); + } + } + + @Nonnull + private String getContent(@Nonnull final HttpEntity entity) { try { return EntityUtils.toString(entity, StandardCharsets.UTF_8); } catch (IOException | ParseException e) { - throw new OrchestrationClientException("Failed to read response content.", e); + throw exceptionType.apply("Failed to read response content.", e); } } - // The InputStream of the HTTP entity is closed by EntityUtils.toString + /** + * Parse the error response and throw an exception. + * + * @param response The response to process + */ @SuppressWarnings("PMD.CloseResource") - static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response) - throws OrchestrationClientException { + public void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response) throws E { val exception = - new OrchestrationClientException( - "Request to orchestration service failed with status %s %s" - .formatted(response.getCode(), response.getReasonPhrase())); + exceptionType.apply( + "Request failed with status %s %s" + .formatted(response.getCode(), response.getReasonPhrase()), + null); val entity = response.getEntity(); if (entity == null) { throw exception; @@ -54,9 +109,7 @@ static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response) throw exception; } - log.error( - "The orchestration service responded with an HTTP error and the following content: {}", - content); + log.error("The service responded with an HTTP error and the following content: {}", content); val contentType = ContentType.parse(entity.getContentType()); if (!ContentType.APPLICATION_JSON.isSameMimeType(contentType)) { throw exception; @@ -68,59 +121,19 @@ static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response) /** * Parse the error response and throw an exception. * - * @param errorResponse the error response, most likely a JSON of {@link ErrorResponse}. + * @param errorResponse the error response, most likely a unique JSON class. * @param baseException a base exception to add the error message to. */ - static void parseErrorAndThrow( - @Nonnull final String errorResponse, - @Nonnull final OrchestrationClientException baseException) - throws OrchestrationClientException { - val maybeError = Try.of(() -> JACKSON.readValue(errorResponse, ErrorResponse.class)); + public void parseErrorAndThrow( + @Nonnull final String errorResponse, @Nonnull final E baseException) throws E { + val maybeError = Try.of(() -> JACKSON.readValue(errorResponse, errorType)); if (maybeError.isFailure()) { baseException.addSuppressed(maybeError.getCause()); throw baseException; } - throw new OrchestrationClientException( - "%s and error message: '%s'" - .formatted(baseException.getMessage(), maybeError.get().getMessage())); - } - - /** - * Processes a {@link ClassicHttpResponse} and returns some value corresponding to that response. - * - * @param response The response to process - * @return A model class instantiated from the response - * @throws OrchestrationClientException in case of a problem or the connection was aborted - */ - @Override - public T handleResponse(@Nonnull final ClassicHttpResponse response) - throws OrchestrationClientException { - if (response.getCode() >= 300) { - buildExceptionAndThrow(response); - } - val result = parseResponse(response); - log.debug("Received the following response from orchestration service: {}", result); - return result; - } - - // The InputStream of the HTTP entity is closed by EntityUtils.toString - @SuppressWarnings("PMD.CloseResource") - @Nonnull - private T parseResponse(@Nonnull final ClassicHttpResponse response) - throws OrchestrationClientException { - final HttpEntity responseEntity = response.getEntity(); - if (responseEntity == null) { - throw new OrchestrationClientException("Response from Orchestration service was empty."); - } - val content = getContent(responseEntity); - log.debug("Parsing response from JSON response: {}", content); - try { - return JACKSON.readValue(content, responseType); - } catch (final JsonProcessingException e) { - log.error("Failed to parse the following response from orchestration service: {}", content); - throw new OrchestrationClientException( - "Failed to parse response from orchestration service", e); - } + val error = Objects.requireNonNullElse(maybeError.get().getMessage(), ""); + val message = "%s and error message: '%s'".formatted(baseException.getMessage(), error); + throw exceptionType.apply(message, baseException); } } diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml index d87d8d01a..412b4717c 100644 --- a/foundation-models/openai/pom.xml +++ b/foundation-models/openai/pom.xml @@ -33,6 +33,12 @@ ${project.basedir}/../../ + 70% + 76% + 75% + 66% + 83% + 85% 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 1f1804f01..ac525c79a 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 @@ -7,6 +7,7 @@ import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.DeploymentResolutionException; +import com.sap.ai.sdk.core.commons.ClientResponseHandler; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters; @@ -14,6 +15,7 @@ import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage.OpenAiChatUserMessage; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters; +import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiError; import com.sap.ai.sdk.foundationmodels.openai.model.StreamedDelta; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; @@ -283,7 +285,9 @@ private T executeRequest( final BasicClassicHttpRequest request, @Nonnull final Class responseType) { try { final var client = ApacheHttpClient5Accessor.getHttpClient(destination); - return client.execute(request, new OpenAiResponseHandler<>(responseType)); + return client.execute( + request, + new ClientResponseHandler<>(responseType, OpenAiError.class, OpenAiClientException::new)); } catch (final IOException e) { throw new OpenAiClientException("Request to OpenAI model failed", 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 3c25a1c8b..f0d131040 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 @@ -1,7 +1,8 @@ package com.sap.ai.sdk.foundationmodels.openai; +import com.sap.ai.sdk.core.commons.ClientException; import lombok.experimental.StandardException; /** Generic exception for errors occurring when using OpenAI foundation models. */ @StandardException -public class OpenAiClientException extends RuntimeException {} +public class OpenAiClientException extends ClientException {} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiResponseHandler.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiResponseHandler.java deleted file mode 100644 index 9d7dd0bdd..000000000 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiResponseHandler.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.sap.ai.sdk.foundationmodels.openai; - -import static com.sap.ai.sdk.foundationmodels.openai.OpenAiClient.JACKSON; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiError; -import io.vavr.control.Try; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.io.HttpClientResponseHandler; -import org.apache.hc.core5.http.io.entity.EntityUtils; - -@Slf4j -@RequiredArgsConstructor -class OpenAiResponseHandler implements HttpClientResponseHandler { - - @Nonnull private final Class responseType; - - /** - * Processes a {@link ClassicHttpResponse} and returns some value corresponding to that response. - * - * @param response The response to process - * @return A model class instantiated from the response - * @throws OpenAiClientException in case of a problem or the connection was aborted - */ - @Override - public T handleResponse(@Nonnull final ClassicHttpResponse response) - throws OpenAiClientException { - if (response.getCode() >= 300) { - buildExceptionAndThrow(response); - } - return parseResponse(response); - } - - // The InputStream of the HTTP entity is closed by EntityUtils.toString - @SuppressWarnings("PMD.CloseResource") - @Nonnull - private T parseResponse(@Nonnull final ClassicHttpResponse response) - throws OpenAiClientException { - final HttpEntity responseEntity = response.getEntity(); - if (responseEntity == null) { - throw new OpenAiClientException("Response from OpenAI model was empty."); - } - final var content = getContent(responseEntity); - try { - return JACKSON.readValue(content, responseType); - } catch (final JsonProcessingException e) { - log.error("Failed to parse the following response from OpenAI model: {}", content); - throw new OpenAiClientException("Failed to parse response from OpenAI model", e); - } - } - - @Nonnull - private static String getContent(@Nonnull final HttpEntity entity) { - try { - return EntityUtils.toString(entity, StandardCharsets.UTF_8); - } catch (IOException | ParseException e) { - throw new OpenAiClientException("Failed to read response content.", e); - } - } - - // The InputStream of the HTTP entity is closed by EntityUtils.toString - @SuppressWarnings("PMD.CloseResource") - static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response) - throws OpenAiClientException { - final var exception = - new OpenAiClientException( - "Request to OpenAI model failed with status %s %s" - .formatted(response.getCode(), response.getReasonPhrase())); - final var entity = response.getEntity(); - if (entity == null) { - throw exception; - } - final var maybeContent = Try.of(() -> getContent(entity)); - if (maybeContent.isFailure()) { - exception.addSuppressed(maybeContent.getCause()); - throw exception; - } - final var content = maybeContent.get(); - if (content.isBlank()) { - throw exception; - } - - log.error("OpenAI model responded with an HTTP error and the following content: {}", content); - final var contentType = ContentType.parse(entity.getContentType()); - if (!ContentType.APPLICATION_JSON.isSameMimeType(contentType)) { - throw exception; - } - - parseErrorAndThrow(content, exception); - } - - /** - * Parse the error response and throw an exception. - * - * @param errorResponse the error response, most likely a JSON of {@link OpenAiError}. - * @param baseException a base exception to add the error message to. - */ - static void parseErrorAndThrow( - @Nonnull final String errorResponse, @Nonnull final OpenAiClientException baseException) - throws OpenAiClientException { - final var maybeError = Try.of(() -> JACKSON.readValue(errorResponse, OpenAiError.class)); - if (maybeError.isFailure()) { - baseException.addSuppressed(maybeError.getCause()); - throw baseException; - } - - final var error = maybeError.get().getError(); - if (error == null) { - throw baseException; - } - throw new OpenAiClientException( - "%s and error message: '%s'".formatted(baseException.getMessage(), error.getMessage())); - } -} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiStreamingHandler.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiStreamingHandler.java index b33507939..5f4eaaee3 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiStreamingHandler.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiStreamingHandler.java @@ -1,9 +1,9 @@ package com.sap.ai.sdk.foundationmodels.openai; import static com.sap.ai.sdk.foundationmodels.openai.OpenAiClient.JACKSON; -import static com.sap.ai.sdk.foundationmodels.openai.OpenAiResponseHandler.buildExceptionAndThrow; -import static com.sap.ai.sdk.foundationmodels.openai.OpenAiResponseHandler.parseErrorAndThrow; +import com.sap.ai.sdk.core.commons.ClientResponseHandler; +import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiError; import com.sap.ai.sdk.foundationmodels.openai.model.StreamedDelta; import java.io.IOException; import java.util.stream.Stream; @@ -17,6 +17,12 @@ class OpenAiStreamingHandler { @Nonnull private final Class deltaType; + private static final ClientResponseHandler HANDLER = + new ClientResponseHandler<>(OpenAiError.class, OpenAiError.class, OpenAiClientException::new); + + static { + HANDLER.JACKSON = JACKSON; + } /** * @param response The response to process @@ -27,7 +33,7 @@ class OpenAiStreamingHandler { Stream handleResponse(@Nonnull final ClassicHttpResponse response) throws OpenAiClientException { if (response.getCode() >= 300) { - buildExceptionAndThrow(response); + HANDLER.buildExceptionAndThrow(response); } return IterableStreamConverter.lines(response.getEntity()) // half of the lines are empty newlines, the last line is "data: [DONE]" @@ -36,7 +42,7 @@ Stream handleResponse(@Nonnull final ClassicHttpResponse response) line -> { if (!line.startsWith("data: ")) { final String msg = "Failed to parse response from OpenAI model"; - parseErrorAndThrow(line, new OpenAiClientException(msg)); + HANDLER.parseErrorAndThrow(line, new OpenAiClientException(msg)); } }) .map( diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiError.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiError.java index 847f1fe3c..9f3f5dd6a 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiError.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiError.java @@ -2,20 +2,23 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.Beta; +import com.sap.ai.sdk.core.commons.ClientError; import javax.annotation.Nullable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.experimental.Accessors; +import lombok.experimental.Delegate; /** OpenAI error. */ @Accessors(chain = true) @EqualsAndHashCode @ToString @Beta -public class OpenAiError { +public class OpenAiError implements ClientError { /** The error object. */ @JsonProperty("error") @Getter(onMethod_ = @Nullable) + @Delegate(types = {ClientError.class}) private OpenAiErrorBase error; } diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 722b1c825..41af12499 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -31,6 +31,12 @@ ${project.basedir}/../ + 70% + 86% + 86% + 67% + 85% + 100% 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 986730149..ea0fa01ae 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 @@ -9,6 +9,7 @@ import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.DeploymentResolutionException; +import com.sap.ai.sdk.core.commons.ClientResponseHandler; import com.sap.ai.sdk.orchestration.model.CompletionPostRequest; import com.sap.ai.sdk.orchestration.model.CompletionPostResponse; import com.sap.ai.sdk.orchestration.model.LLMModuleResult; @@ -224,8 +225,13 @@ CompletionPostResponse executeRequest(@Nonnull final String request) { val destination = destinationSupplier.get(); log.debug("Using destination {} to connect to orchestration service", destination); val client = ApacheHttpClient5Accessor.getHttpClient(destination); - return client.execute( - postRequest, new OrchestrationResponseHandler<>(CompletionPostResponse.class)); + val handler = + new ClientResponseHandler<>( + CompletionPostResponse.class, + OrchestrationError.class, + OrchestrationClientException::new); + handler.JACKSON = JACKSON; + return client.execute(postRequest, handler); } catch (DeploymentResolutionException | DestinationAccessException | DestinationNotFoundException 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 0db92ba9b..8f0fa2602 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,7 +1,8 @@ package com.sap.ai.sdk.orchestration; +import com.sap.ai.sdk.core.commons.ClientException; import lombok.experimental.StandardException; /** Exception thrown by the {@link OrchestrationClient} in case of an error. */ @StandardException -public class OrchestrationClientException extends RuntimeException {} +public class OrchestrationClientException extends ClientException {} 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 new file mode 100644 index 000000000..0d0178443 --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java @@ -0,0 +1,23 @@ +package com.sap.ai.sdk.orchestration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.google.common.annotations.Beta; +import com.sap.ai.sdk.core.commons.ClientError; +import com.sap.ai.sdk.orchestration.model.ErrorResponse; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.experimental.Delegate; + +/** + * Orchestration error response. + * + * @since 1.1.0 + */ +@AllArgsConstructor(onConstructor = @__({@JsonCreator}), access = AccessLevel.PROTECTED) +@Value +@Beta +public class OrchestrationError implements ClientError { + @Delegate(types = {ClientError.class}) + ErrorResponse originalResponse; +} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationStreamingHandler.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationStreamingHandler.java index 5a20002d5..71042319b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationStreamingHandler.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationStreamingHandler.java @@ -1,9 +1,8 @@ package com.sap.ai.sdk.orchestration; import static com.sap.ai.sdk.orchestration.OrchestrationClient.JACKSON; -import static com.sap.ai.sdk.orchestration.OrchestrationResponseHandler.buildExceptionAndThrow; -import static com.sap.ai.sdk.orchestration.OrchestrationResponseHandler.parseErrorAndThrow; +import com.sap.ai.sdk.core.commons.ClientResponseHandler; import java.io.IOException; import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -16,6 +15,16 @@ class OrchestrationStreamingHandler { @Nonnull private final Class deltaType; + private static final ClientResponseHandler + HANDLER = + new ClientResponseHandler<>( + OrchestrationError.class, + OrchestrationError.class, + OrchestrationClientException::new); + + static { + HANDLER.JACKSON = JACKSON; + } /** * @param response The response to process @@ -26,7 +35,7 @@ class OrchestrationStreamingHandler { Stream handleResponse(@Nonnull final ClassicHttpResponse response) throws OrchestrationClientException { if (response.getCode() >= 300) { - buildExceptionAndThrow(response); + HANDLER.buildExceptionAndThrow(response); } return IterableStreamConverter.lines(response.getEntity()) // half of the lines are empty newlines, the last line is "data: [DONE]" @@ -36,7 +45,7 @@ Stream handleResponse(@Nonnull final ClassicHttpResponse response) line -> { if (!line.startsWith("data: ")) { final String msg = "Failed to parse response from the Orchestration service"; - parseErrorAndThrow(line, new OrchestrationClientException(msg)); + HANDLER.parseErrorAndThrow(line, new OrchestrationClientException(msg)); } }) .map( 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 29240790f..6b553735c 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 @@ -220,7 +220,7 @@ void testBadRequest() { assertThatThrownBy(() -> client.chatCompletion(prompt, config)) .isInstanceOf(OrchestrationClientException.class) .hasMessage( - "Request to orchestration service failed with status 400 Bad Request and error message: 'Missing required parameters: ['input']'"); + "Request failed with status 400 Bad Request and error message: 'Missing required parameters: ['input']'"); } @Test @@ -268,7 +268,7 @@ void filteringStrict() { assertThatThrownBy(() -> client.chatCompletion(prompt, configWithFilter)) .isInstanceOf(OrchestrationClientException.class) .hasMessage( - "Request to orchestration service failed with status 400 Bad Request and error message: 'Content filtered due to Safety violations. Please modify the prompt and try again.'"); + "Request failed with status 400 Bad Request and error message: 'Content filtered due to Safety violations. Please modify the prompt and try again.'"); } @Test diff --git a/pom.xml b/pom.xml index 661862128..b8b96a0c1 100644 --- a/pom.xml +++ b/pom.xml @@ -75,13 +75,13 @@ false false false - - 77% - 68% - 71% - 79% + + 100% + 100% + 100% + 100% 100% - 85% + 100% @@ -563,6 +563,11 @@ https://gitbox.apache.org/repos/asf?p=maven-pmd-plugin.git;a=blob_plain;f=src/ma COVEREDRATIO ${coverage.branch} + + METHOD + COVEREDRATIO + ${coverage.method} + CLASS COVEREDRATIO