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 extends ClientError> 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