diff --git a/docs/release_notes.md b/docs/release_notes.md
index 7d44802f2..29e8b1652 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -13,6 +13,7 @@
### ✨ New Functionality
- [OpenAI] You can now add multiple custom headers to an `OpenAiClient` at once via `.withHeaders()`.
+- [Orchestration] Added `GEMINI_3_1_PRO_PREVIEW_EA` to model list in `OrchestrationAiModel`.
### 📈 Improvements
@@ -20,4 +21,4 @@
### 🐛 Fixed Issues
--
+- [Orchestration] Some `OrchestrationClientException` were reported as `OrchestrationFilterException.Input`.
diff --git a/orchestration/pom.xml b/orchestration/pom.xml
index 8b8d7f0f1..b41927f4b 100644
--- a/orchestration/pom.xml
+++ b/orchestration/pom.xml
@@ -39,8 +39,8 @@
82%
94%
93%
- 75%
- 95%
+ 74%
+ 94%
100%
diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
index 41064c274..5145e9174 100644
--- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
+++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationAiModel.java
@@ -429,6 +429,10 @@ public class OrchestrationAiModel {
public static final OrchestrationAiModel GEMINI_3_5_FLASH =
new OrchestrationAiModel("gemini-3.5-flash");
+ /** Google Cloud Platform Gemini 3.1 Pro preview early access model */
+ public static final OrchestrationAiModel GEMINI_3_1_PRO_PREVIEW_EA =
+ new OrchestrationAiModel("gemini-3.1-pro-preview-ea");
+
/**
* Alephalpha-pharia-1-7b-control model
*
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 857d1e46f..1177e68a0 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,5 +1,7 @@
package com.sap.ai.sdk.orchestration;
+import static java.util.Locale.ROOT;
+
import com.google.common.annotations.Beta;
import com.sap.ai.sdk.core.common.ClientException;
import com.sap.ai.sdk.core.common.ClientExceptionFactory;
@@ -27,17 +29,36 @@ public class OrchestrationClientException extends ClientException {
static final ClientExceptionFactory FACTORY =
(message, clientError, cause) -> {
- final var details = extractInputFilterDetails(clientError);
- if (details.isEmpty()) {
- if (message.contains("No Prompt Template found in the Prompt Registry.")) {
- message +=
- "\n Please make sure to provide a resource group id and verify that it matches the provided template reference details if the template is referenced from a resource-group scope, otherwise use the tenant scope without providing resource group id.";
- }
- return new OrchestrationClientException(message, cause).setClientError(clientError);
+ if (isInputFilterError(clientError)) {
+ return new Input(message, cause)
+ .setFilterDetails(extractInputFilterDetails(clientError))
+ .setClientError(clientError);
+ } else if (message.contains("No Prompt Template found in the Prompt Registry.")) {
+ message +=
+ "\n Please make sure to provide a resource group id and verify that it matches the provided template reference details if the template is referenced from a resource-group scope, otherwise use the tenant scope without providing resource group id.";
}
- return new Input(message, cause).setFilterDetails(details).setClientError(clientError);
+ return new OrchestrationClientException(message, cause).setClientError(clientError);
};
+ static boolean isInputFilterError(@Nullable final OrchestrationError error) {
+ if (error instanceof OrchestrationError.Synchronous synchronousError) {
+ return Optional.of(synchronousError.getErrorResponse())
+ .map(ErrorResponse::getError)
+ .flatMap(OrchestrationClientException::lastError)
+ .map(Error::getLocation)
+ .map(filter -> filter.toLowerCase(ROOT).contains("filter"))
+ .orElse(false);
+ } else if (error instanceof OrchestrationError.Streaming streamingError) {
+ return Optional.of(streamingError.getErrorResponse())
+ .map(ErrorResponseStreaming::getError)
+ .flatMap(OrchestrationClientException::lastErrorStreaming)
+ .map(ErrorStreaming::getLocation)
+ .map(filter -> filter.toLowerCase(ROOT).contains("filter"))
+ .orElse(false);
+ }
+ return false;
+ }
+
@SuppressWarnings("unchecked")
@Nonnull
static Map extractInputFilterDetails(@Nullable final OrchestrationError error) {
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 92d27ee15..3e21116ee 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
@@ -5,6 +5,7 @@
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE;
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.*;
import static com.sap.ai.sdk.orchestration.model.ResponseChatMessage.RoleEnum.ASSISTANT;
+import static java.util.Locale.ROOT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -209,8 +210,8 @@ void testGrounding() {
assertThat(llmChoice.getFinishReason()).isEqualTo("stop");
assertThat(result.getIntermediateResults().getGrounding()).isNotNull();
assertThat(result.getIntermediateResults().getGrounding().getData()).isNotNull();
- assertThat(result.getIntermediateResults().getGrounding().getMessage())
- .isEqualTo("grounding result");
+ assertThat(result.getIntermediateResults().getGrounding().getMessage().toLowerCase(ROOT))
+ .contains("grounding");
var groundingData =
(Map) result.getIntermediateResults().getGrounding().getData();
assertThat(groundingData.get("grounding_result")).contains("metadata");
@@ -244,8 +245,7 @@ void testInputFilteringStrict() {
var policy = AzureFilterThreshold.ALLOW_SAFE;
assertThatThrownBy(() -> service.inputFiltering(policy))
- .hasMessageContaining(
- "Content filtered due to safety violations. Please modify the prompt and try again.")
+ .hasMessageContainingAll("Filtering", "blocked")
.hasMessageContaining("400 (Bad Request)")
.isInstanceOfSatisfying(
OrchestrationFilterException.Input.class,
@@ -269,7 +269,7 @@ void testInputFilteringLenient() {
assertThat(response.getContent()).isNotEmpty();
var filterResult = response.getOriginalResponse().getIntermediateResults().getInputFiltering();
- assertThat(filterResult.getMessage()).contains("passed"); // prompt shield is a filter
+ assertThat(filterResult.getMessage()).isNotEmpty(); // prompt shield is a filter
}
@Test
@@ -302,15 +302,14 @@ void testOutputFilteringLenient() {
assertThat(response.getContent()).isNotEmpty();
var filterResult = response.getOriginalResponse().getIntermediateResults().getOutputFiltering();
- assertThat(filterResult.getMessage()).containsPattern("Choice 0: Filtering was skipped.");
+ assertThat(filterResult.getMessage()).contains("Filtering").containsAnyOf("passed", "skipped");
}
@Test
void testLlamaGuardEnabled() {
assertThatThrownBy(() -> service.llamaGuardInputFilter(true))
.isInstanceOf(OrchestrationFilterException.Input.class)
- .hasMessageContaining(
- "Content filtered due to safety violations. Please modify the prompt and try again.")
+ .hasMessageContainingAll("Filtering", "blocked")
.hasMessageContaining("400 (Bad Request)")
.isInstanceOfSatisfying(
OrchestrationFilterException.Input.class,
@@ -332,7 +331,7 @@ void testLlamaGuardDisabled() {
assertThat(response.getContent()).isNotEmpty();
var filterResult = response.getOriginalResponse().getIntermediateResults().getInputFiltering();
- assertThat(filterResult.getMessage()).contains("skipped");
+ assertThat(filterResult.getMessage()).contains("Filtering").containsAnyOf("passed", "skipped");
}
@Test
@@ -550,7 +549,7 @@ void testTranslation() {
assertThat(inputTranslation).isNotNull();
assertThat(inputTranslation.getMessage())
.isNotNull()
- .contains("Successfully translated placeholders:")
+ .contains("Successfully", " placeholders:")
.contains("exam_type")
.contains("topic");
diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationTest.java
index e080ccfc6..93c8ec384 100644
--- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationTest.java
+++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationTest.java
@@ -66,8 +66,7 @@ void testInputFilteringStrict() {
assertThatThrownBy(() -> service.inputFiltering(policy))
.isInstanceOf(OrchestrationClientException.class)
- .hasMessageContaining(
- "Content filtered due to safety violations. Please modify the prompt and try again.")
+ .hasMessageContainingAll("Filtering", "blocked")
.hasMessageContaining("400 (Bad Request)");
}
@@ -86,7 +85,7 @@ void testInputFilteringLenient() {
.getOriginalResponse()
.getIntermediateResults()
.getInputFiltering();
- assertThat(filterResult.getMessage()).contains("skipped");
+ assertThat(filterResult.getMessage()).contains("Filtering").containsAnyOf("passed", "skipped");
}
@Test
@@ -104,8 +103,7 @@ void testOutputFilteringStrict() {
.getOriginalResponse()
.getIntermediateResults()
.getOutputFiltering();
- assertThat(filterResult.getMessage())
- .contains("Choice 0: Content filtered due to safety violations.");
+ assertThat(filterResult.getMessage()).contains("Filtering", "blocked");
}
@Test
@@ -123,7 +121,7 @@ void testOutputFilteringLenient() {
.getOriginalResponse()
.getIntermediateResults()
.getOutputFiltering();
- assertThat(filterResult.getMessage()).contains("Choice 0: Filtering was skipped.");
+ assertThat(filterResult.getMessage()).contains("Filtering").containsAnyOf("passed", "skipped");
}
@Test