Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.inference.external.http.retry;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xpack.core.inference.results.UnifiedChatCompletionException;
import org.elasticsearch.xpack.inference.external.http.HttpResult;
Expand All @@ -22,9 +23,9 @@
public class ChatCompletionErrorResponseHandler {
private static final String STREAM_ERROR = "stream_error";

private final UnifiedChatCompletionErrorParser unifiedChatCompletionErrorParser;
private final UnifiedChatCompletionErrorParserContract unifiedChatCompletionErrorParser;

public ChatCompletionErrorResponseHandler(UnifiedChatCompletionErrorParser errorParser) {
public ChatCompletionErrorResponseHandler(UnifiedChatCompletionErrorParserContract errorParser) {
this.unifiedChatCompletionErrorParser = Objects.requireNonNull(errorParser);
}

Expand Down Expand Up @@ -62,14 +63,18 @@ private UnifiedChatCompletionException buildChatCompletionErrorInternal(
restStatus,
errorMessage,
errorResponse.type(),
errorResponse.code(),
code(errorResponse.code(), restStatus),
errorResponse.param()
);
} else {
return buildDefaultChatCompletionError(errorResponse, errorMessage, restStatus);
}
}

private static String code(@Nullable String code, RestStatus status) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an oversight from the previous PR. The OpenAI implementation either uses the provided code or the rest status code.

return code != null ? code : status.name().toLowerCase(Locale.ROOT);
}

/**
* Builds a default {@link UnifiedChatCompletionException} for a streaming request.
* This method is used when an error response is received we were unable to parse it in the format we were expecting.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import org.elasticsearch.xpack.inference.external.http.HttpResult;

public interface UnifiedChatCompletionErrorParser {
public interface UnifiedChatCompletionErrorParserContract {
UnifiedChatCompletionErrorResponse parse(HttpResult result);

UnifiedChatCompletionErrorResponse parse(String result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,51 @@
package org.elasticsearch.xpack.inference.external.http.retry;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xpack.inference.external.http.HttpResult;

import java.util.Objects;
import java.util.Optional;

public class UnifiedChatCompletionErrorResponse extends ErrorResponse {

private static final ConstructingObjectParser<Optional<UnifiedChatCompletionErrorResponse>, Void> CONSTRUCTING_OBJECT_PARSER =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These new lines were taken from StreamingErrorResponse. I consolidated the classes.

Copy link
Member

@davidkyle davidkyle Aug 14, 2025

Choose a reason for hiding this comment

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

Nit rename CONSTRUCTING_OBJECT_PARSER -> ERROR_OBJECT_PARSER

new ConstructingObjectParser<>("streaming_error", true, args -> Optional.ofNullable((UnifiedChatCompletionErrorResponse) args[0]));
private static final ConstructingObjectParser<UnifiedChatCompletionErrorResponse, Void> ERROR_BODY_PARSER =
new ConstructingObjectParser<>(
"streaming_error",
true,
args -> new UnifiedChatCompletionErrorResponse((String) args[0], (String) args[1], (String) args[2], (String) args[3])
);

static {
ERROR_BODY_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("message"));
ERROR_BODY_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("type"));
ERROR_BODY_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("code"));
ERROR_BODY_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("param"));

CONSTRUCTING_OBJECT_PARSER.declareObjectOrNull(
ConstructingObjectParser.optionalConstructorArg(),
ERROR_BODY_PARSER,
null,
new ParseField("error")
);
}

public static final UnifiedChatCompletionErrorParserContract ERROR_PARSER = UnifiedChatCompletionErrorResponseUtils
.createErrorParserWithObjectParser(CONSTRUCTING_OBJECT_PARSER);
public static final UnifiedChatCompletionErrorResponse UNDEFINED_ERROR = new UnifiedChatCompletionErrorResponse();

/**
* Standard error response parser. This can be overridden for those subclasses that
* have a different error response structure.
Copy link
Member

Choose a reason for hiding this comment

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

nit static methods cannot be overridden

Suggested change
* Standard error response parser. This can be overridden for those subclasses that
* have a different error response structure.
* Standard error response parser.

* @param response The error response as an HttpResult
*/
public static UnifiedChatCompletionErrorResponse fromHttpResult(HttpResult response) {
return ERROR_PARSER.parse(response);
}

@Nullable
private final String code;
@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.inference.external.http.retry;

import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.inference.external.http.HttpResult;

import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.Callable;

public class UnifiedChatCompletionErrorResponseUtils {

public static UnifiedChatCompletionErrorParserContract createErrorParserWithStringify(String type) {
return new UnifiedChatCompletionErrorParserContract() {
@Override
public UnifiedChatCompletionErrorResponse parse(HttpResult result) {
try {
String errorMessage = new String(result.body(), StandardCharsets.UTF_8);
return new UnifiedChatCompletionErrorResponse(errorMessage, type, null, null);
} catch (Exception e) {
// swallow the error
}

return UnifiedChatCompletionErrorResponse.UNDEFINED_ERROR;
}

@Override
public UnifiedChatCompletionErrorResponse parse(String result) {
return new UnifiedChatCompletionErrorResponse(result, type, null, null);
}
};
}

public static UnifiedChatCompletionErrorParserContract createErrorParserWithObjectParser(
ConstructingObjectParser<Optional<UnifiedChatCompletionErrorResponse>, Void> objectParser
) {
return new UnifiedChatCompletionErrorParser<>((parser) -> objectParser.apply(parser, null));
}

public static <E extends Exception> UnifiedChatCompletionErrorParserContract createErrorParserWithGenericParser(
CheckedFunction<XContentParser, Optional<UnifiedChatCompletionErrorResponse>, E> genericParser
) {
return new UnifiedChatCompletionErrorParser<>(genericParser);
}

private record UnifiedChatCompletionErrorParser<E extends Exception>(
CheckedFunction<XContentParser, Optional<UnifiedChatCompletionErrorResponse>, E> genericParser
) implements UnifiedChatCompletionErrorParserContract {

@Override
public UnifiedChatCompletionErrorResponse parse(HttpResult result) {
return executeGenericParser(genericParser, createHttpResultXContentParserFunction(result));
}

@Override
public UnifiedChatCompletionErrorResponse parse(String result) {
return executeGenericParser(genericParser, createStringXContentParserFunction(result));
}

}

private static Callable<XContentParser> createHttpResultXContentParserFunction(HttpResult response) {
return () -> XContentFactory.xContent(XContentType.JSON).createParser(XContentParserConfiguration.EMPTY, response.body());
}

private static Callable<XContentParser> createStringXContentParserFunction(String response) {
return () -> XContentFactory.xContent(XContentType.JSON).createParser(XContentParserConfiguration.EMPTY, response);
}

private static <E extends Exception> UnifiedChatCompletionErrorResponse executeGenericParser(
CheckedFunction<XContentParser, Optional<UnifiedChatCompletionErrorResponse>, E> genericParser,
Callable<XContentParser> createXContentParser
) {
try (XContentParser parser = createXContentParser.call()) {
return genericParser.apply(parser).orElse(UnifiedChatCompletionErrorResponse.UNDEFINED_ERROR);
} catch (Exception e) {
// swallow the error
}

return UnifiedChatCompletionErrorResponse.UNDEFINED_ERROR;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import org.elasticsearch.xpack.core.inference.results.UnifiedChatCompletionException;
import org.elasticsearch.xpack.inference.external.http.HttpResult;
import org.elasticsearch.xpack.inference.external.http.retry.ChatCompletionErrorResponseHandler;
import org.elasticsearch.xpack.inference.external.http.retry.UnifiedChatCompletionErrorParser;
import org.elasticsearch.xpack.inference.external.http.retry.UnifiedChatCompletionErrorParserContract;
import org.elasticsearch.xpack.inference.external.http.retry.UnifiedChatCompletionErrorResponse;
import org.elasticsearch.xpack.inference.external.request.Request;
import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventParser;
Expand Down Expand Up @@ -70,7 +70,7 @@ protected void checkForErrorObject(Request request, HttpResult result) {
chatCompletionErrorResponseHandler.checkForErrorObject(request, result);
}

private static class GoogleVertexAiErrorParser implements UnifiedChatCompletionErrorParser {
private static class GoogleVertexAiErrorParser implements UnifiedChatCompletionErrorParserContract {

@Override
public UnifiedChatCompletionErrorResponse parse(HttpResult result) {
Expand Down
Loading