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