Skip to content

Commit c7a5ada

Browse files
fix: [Orchestration] Fixed getting OrchestrationFilterException.Input for bad requests with input filter.
1 parent 41b4717 commit c7a5ada

File tree

13 files changed

+123
-10
lines changed

13 files changed

+123
-10
lines changed

core/src/main/java/com/sap/ai/sdk/core/common/ClientStreamingHandler.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sap.ai.sdk.core.common;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
34
import com.fasterxml.jackson.databind.ObjectMapper;
45
import com.google.common.annotations.Beta;
56
import java.io.IOException;
@@ -77,12 +78,26 @@ public Stream<D> handleStreamingResponse(@Nonnull final ClassicHttpResponse resp
7778
line -> {
7879
final String data = line.substring(5); // remove "data: "
7980
try {
80-
return objectMapper.readValue(data, successType);
81+
final D delta = objectMapper.readValue(data, successType);
82+
if (delta.isError()) {
83+
throwErrorType(response, data);
84+
}
85+
return delta;
8186
} catch (final IOException e) { // exception message e gets lost
8287
log.error("Failed to parse delta chunk to type {}", successType);
8388
final String message = "Failed to parse delta chunk";
8489
throw exceptionFactory.build(message, e).setHttpResponse(response);
8590
}
8691
});
8792
}
93+
94+
private void throwErrorType(final @Nonnull ClassicHttpResponse response, final String data)
95+
throws JsonProcessingException, E {
96+
final R error = objectMapper.readValue(data, errorType);
97+
final String msg =
98+
(error != null && error.getMessage() != null)
99+
? error.getMessage()
100+
: "Error, unable to parse http response.";
101+
throw exceptionFactory.build(msg).setHttpResponse(response);
102+
}
88103
}

core/src/main/java/com/sap/ai/sdk/core/common/StreamedDelta.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,11 @@ public interface StreamedDelta {
4242
*/
4343
@Nullable
4444
String getFinishReason();
45+
46+
/**
47+
* Indicates if the delta is an error of type {@link ClientError}
48+
*
49+
* @return true if the delta is an error, false otherwise.
50+
*/
51+
boolean isError();
4552
}

core/src/test/java/com/sap/ai/sdk/core/common/ClientStreamingHandlerTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public String getDeltaContent() {
4040
public String getFinishReason() {
4141
return finishReason;
4242
}
43+
44+
@Override
45+
public boolean isError() {
46+
return false;
47+
}
4348
}
4449

4550
@SneakyThrows

docs/release_notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040

4141
### 🐛 Fixed Issues
4242

43-
-
43+
- [Orchestration] Fixed getting `OrchestrationFilterException.Input` for bad requests with input filter.

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionDelta.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public String getFinishReason() {
5151
return null;
5252
}
5353

54+
@Override
55+
public boolean isError() {
56+
return originalResponse.getCustomFieldNames().contains("error");
57+
}
58+
5459
/**
5560
* Retrieves the completion usage from the response, or null if it is not available.
5661
*

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiChatCompletionDelta.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ && getChoices().get(0).getIndex() == 0) {
5959
}
6060
return null;
6161
}
62+
63+
@Override
64+
public boolean isError() {
65+
return false;
66+
}
6267
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatCompletionDelta.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public String getDeltaContent() {
3131
public String getFinishReason() {
3232
return getFinalResult().getChoices().get(0).getFinishReason();
3333
}
34+
35+
@Override
36+
public boolean isError() {
37+
return getCustomFieldNames().contains("error");
38+
}
3439
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ static Map<String, Object> extractInputFilterDetails(@Nullable final Orchestrati
3939
.map(ErrorResponse::getError)
4040
.map(Error::getIntermediateResults)
4141
.map(ModuleResults::getInputFiltering)
42+
.filter(filter -> !filter.getMessage().equals("Input Filter passed successfully."))
4243
.map(GenericModuleResult::getData)
4344
.map(map -> (Map<String, Object>) map)
4445
.orElseGet(Collections::emptyMap);
@@ -47,6 +48,7 @@ static Map<String, Object> extractInputFilterDetails(@Nullable final Orchestrati
4748
.map(ErrorResponseStreaming::getError)
4849
.map(ErrorStreaming::getIntermediateResults)
4950
.map(ModuleResultsStreaming::getInputFiltering)
51+
.filter(filter -> !filter.getMessage().equals("Input Filter passed successfully."))
5052
.map(GenericModuleResult::getData)
5153
.filter(Map.class::isInstance)
5254
.map(map -> (Map<String, Object>) map)

orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -673,33 +673,33 @@ void testErrorHandling(@Nonnull final Runnable request) {
673673
softly
674674
.assertThatThrownBy(request::run)
675675
.describedAs("Server errors should be handled")
676-
.isInstanceOf(OrchestrationClientException.class)
676+
.isExactlyInstanceOf(OrchestrationClientException.class)
677677
.hasMessageContaining("500");
678678

679679
softly
680680
.assertThatThrownBy(request::run)
681681
.describedAs("Error objects from Orchestration should be interpreted")
682-
.isInstanceOf(OrchestrationClientException.class)
682+
.isExactlyInstanceOf(OrchestrationClientException.class)
683683
.hasMessageContaining("'config' is a required property");
684684

685685
softly
686686
.assertThatThrownBy(request::run)
687687
.describedAs("Failures while parsing error message should be handled")
688-
.isInstanceOf(OrchestrationClientException.class)
688+
.isExactlyInstanceOf(OrchestrationClientException.class)
689689
.hasMessageContaining("400")
690690
.extracting(e -> e.getSuppressed()[0])
691691
.isInstanceOf(JsonParseException.class);
692692

693693
softly
694694
.assertThatThrownBy(request::run)
695695
.describedAs("Non-JSON responses should be handled")
696-
.isInstanceOf(OrchestrationClientException.class)
696+
.isExactlyInstanceOf(OrchestrationClientException.class)
697697
.hasMessageContaining("Failed to parse");
698698

699699
softly
700700
.assertThatThrownBy(request::run)
701701
.describedAs("Empty responses should be handled")
702-
.isInstanceOf(OrchestrationClientException.class)
702+
.isExactlyInstanceOf(OrchestrationClientException.class)
703703
.hasMessageContaining("HTTP Response is empty");
704704

705705
softly.assertAll();
@@ -839,6 +839,35 @@ void streamChatCompletionOutputFilterErrorHandling() throws IOException {
839839
}
840840
}
841841

842+
@Test
843+
void testStreamingErrorHandlingBadRequest() throws IOException {
844+
try (var inputStream = fileLoader.apply("streamError.txt")) {
845+
final var httpClient = mock(HttpClient.class);
846+
ApacheHttpClient5Accessor.setHttpClientFactory(destination -> httpClient);
847+
848+
// Create a mock response
849+
final var mockResponse = new BasicClassicHttpResponse(200, "OK");
850+
final var inputStreamEntity = new InputStreamEntity(inputStream, ContentType.TEXT_PLAIN);
851+
mockResponse.setEntity(inputStreamEntity);
852+
mockResponse.setHeader("Content-Type", "text/event-stream");
853+
854+
// Configure the HttpClient mock to return the mock response
855+
doReturn(mockResponse).when(httpClient).executeOpen(any(), any(), any());
856+
857+
val wrongConfig =
858+
new OrchestrationModuleConfig()
859+
.withLlmConfig(GPT_4O_MINI.withVersion("wrong-version"))
860+
.withInputFiltering(new AzureContentFilter().hate(AzureFilterThreshold.ALLOW_SAFE));
861+
val prompt = new OrchestrationPrompt("HelloWorld!");
862+
863+
assertThatThrownBy(
864+
() -> client.streamChatCompletion(prompt, wrongConfig).forEach(System.out::println))
865+
.isExactlyInstanceOf(OrchestrationClientException.class)
866+
.hasMessageContaining("400")
867+
.hasMessageContaining("Model gpt-5 in version wrong-version not found.");
868+
}
869+
}
870+
842871
@Test
843872
void streamChatCompletionDeltas() throws IOException {
844873
try (var inputStream = spy(fileLoader.apply("streamChatCompletion.txt"))) {

orchestration/src/test/resources/__files/errorResponse.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
"code": 400,
55
"message": "'config' is a required property",
66
"location": "request body",
7-
"intermediate_results": {}
7+
"intermediate_results": {
8+
"input_filtering": {
9+
"message": "Input Filter passed successfully.",
10+
"data": {
11+
"azure_content_safety": {
12+
"userPromptAnalysis": {
13+
"attackDetected": false
14+
}
15+
}
16+
}
17+
}
18+
}
819
}
9-
}
20+
}

0 commit comments

Comments
 (0)