Skip to content

feat: [Orchestration] Filtering details and additional convenience on exception #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

rpanackal
Copy link
Member

@rpanackal rpanackal commented Jul 14, 2025

Context

AI/ai-sdk-java-backlog#253.

Previously, we didn't propagate the deserialized error response along with any thrown exception. This also meant our users have no access to diagnostic details returned by orchestration service (also applicable for openai).

The PR aims to rectify the above problem and especiallyy provide convenience for input and output filtering exceptions.

Here are two user issues created requesting the same convenience. They have both suggested some api. Maybe you can also look at this to have a better picture on what convenience may be added

#491
#468

Feature scope:

  • Better separation of module specific error handling with ClientExceptionFactory
  • Add getClientError that returns module specific error objects wrapping deserialized server response.
  • Add OrchestrationInputFilteringException and OrchestrationOutputFilteringException to distinguish filtering exception types with catch blocks.
  • Both filtering exception types contain Map<String, Object> getFilterDetails() to inspect the filtering details available.
  • Add getStatusCode to OrchestrationClientException to get the http status code
  • Extend unit testing for ClientResponseHandler
  • Add unit testing for ClientStreamingHandler
  • Improve Unit testing of input and output filtering
  • E2E test refactored to demo correct usage

Definition of Done

  • Functionality scope stated & covered
  • Tests cover the scope above
  • Error handling created / updated & covered by the tests above
  • Aligned changes with the JavaScript SDK
  • Documentation updated
  • Release notes updated

@rpanackal rpanackal self-assigned this Jul 14, 2025
@rpanackal rpanackal added the please-review Request to review a pull-request label Jul 14, 2025
*/
@Nullable
@Getter(onMethod_ = @Beta)
public ClientError clientError;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(MInor)

Please make it (package) private.
Please add Setter, maybe?

@Nonnull final Class<T> successType;

/** The HTTP error response type */
@Nonnull protected final Class<R> errorType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Question)

Why protected?

@@ -48,8 +50,8 @@ public class ClientResponseHandler<T, E extends ClientException>
*/
@Beta
@Nonnull
public ClientResponseHandler<T, E> objectMapper(@Nonnull final ObjectMapper jackson) {
objectMapper = jackson;
public ClientResponseHandler<T, R, E> objectMapper(@Nonnull final ObjectMapper jackson) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Question)

Is the fluent API reasonable here? Would the code look cleaner if this was a regular void setter?

if (response.getCode() >= 300) {
buildExceptionAndThrow(response);
throw buildException(response);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor)

In Java the "expensive" part of exceptions is the implicit buildup of current stacktrace. That happens at exception instantiation, not when throwing. That also means this will look surprising that the stacktrace points at "new" and not at "throw" (here). Therefore, this line has a code smell.

final HttpEntity responseEntity = response.getEntity();
if (responseEntity == null) {
throw exceptionConstructor.apply("Response was empty.", null);
throw exceptionFactory.create("Response was empty.", null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor)

I know this is semi-related but can we change the message to "HTTP response is empty"?

exception.addSuppressed(maybeContent.getCause());
throw exception;
baseException.addSuppressed(maybeContent.getCause());
return baseException;
}
val content = maybeContent.get();
if (content.isBlank()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Major) content may be null.

null);
val entity = response.getEntity();
@Nonnull
protected E buildException(@Nonnull final ClassicHttpResponse httpResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Comment)

I was thinking, If we moved this whole method and related logic into ClientExceptionFactory then we could get rid of generic argument R extends ClientError in this class :/ But this would risk further duplicate code though. It was just a thought

*/
@Beta
@Nullable
public Integer getStatusCode() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Comment)

We can leave it in for now. But we will likely extend the exception class with optional HttpResponse field reference. Once we have that, then this method will appear very confusing and we may have to change it.

It's beta anyway.

}

/** Exception thrown output filtering in orchestration when finish reason is content filter */
public static class OrchestrationOutputFilterException extends OrchestrationFilterException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor/Discussion)

I would rename it to "Output". Such that class reference becomes
OrchestrationFilterException.Output

public class OrchestrationFilterException extends OrchestrationClientException {

/** Details about the filter that caused the exception. */
@Getter @Nonnull protected Map<String, Object> filterDetails;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor)

IntelliJ is correctly warning me: This field cannot be "Nonnull" when default constructor of this class sets it to "null". I think there are multiple ways to address that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easiest (and obvious?) way is probably initializing it with empty map.

.flatMap(moduleResults -> Optional.ofNullable(moduleResults.getInputFiltering()))
.flatMap(inputFiltering -> Optional.ofNullable(inputFiltering.getData()))
.filter(Map.class::isInstance)
.map(map -> (Map<String, Object>) map)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor)

Technically this is an unsafe cast. We could suppress the warning. (do you see the warning?). Since JSON only assumes dictionaries/maps with String as key-type, this could be considered safe.

Casting to Map<String, Map<String,Object>> would be dangerous. I think.

throw exceptionConstructor.apply(message, baseException);
final R clientError = maybeClientError.get();
val extendErrorMessage = "%s: %s".formatted(baseErrorMessage, clientError.getMessage());
return exceptionFactory.fromClientError(extendErrorMessage, clientError);
Copy link
Contributor

@CharlesDuboisSAP CharlesDuboisSAP Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@newtork this return doesn't reuse the baseException, it creates a new exception. The forgotten baseException might have a performance cost, plus get picked up by logging frameworks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
please-review Request to review a pull-request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants