Skip to content
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

[OPIK-853] handle gemini client errors #1138

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

Conversation

idoberko2
Copy link
Contributor

@idoberko2 idoberko2 commented Jan 26, 2025

Details

Gemini client errors were not handled correctly. The purpose of this PR is fix the Gemini error handling method to respond with a human readable error.
Gemini API throws RuntimeExceptions with the error details as a JSON inside the message. This PR adds parsing to the error message in order to extract the meaningful part out of it.

Issues

OPIK-853

Testing

Tested locally:

❯ curl -v -X "POST" -H "Content-Type: application/json" --data '{"model":"gemini-1.5-flash","messages":[{"role":"user","content":"why are the sky blue?"}],"stream":true,"stream_options":{"include_usage":true},"temperature":0,"max_completion_tokens":1024,"top_p":1}' http://localhost:8080/v1/private/chat/completions

* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> POST /v1/private/chat/completions HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 200
>
* upload completely sent off: 200 bytes
< HTTP/1.1 200 OK
< Date: Sun, 26 Jan 2025 11:23:34 GMT
< Content-Type: text/event-stream
< Transfer-Encoding: chunked
<
{"code":429,"message":"Resource has been exhausted (e.g. check quota).","details":"RESOURCE_EXHAUSTED"}
* Connection #0 to host localhost left intact

@idoberko2 idoberko2 self-assigned this Jan 26, 2025
@idoberko2 idoberko2 requested a review from a team as a code owner January 26, 2025 11:32
Comment on lines +64 to +67
JsonNode errorNode = JsonUtils.MAPPER.readTree(jsonPart).get("error");
if (errorNode != null) {
var code = errorNode.get("code").asInt();
if (familyOf(code) == Response.Status.Family.CLIENT_ERROR) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are familiar with the structure they send use, they not creating a DTO and trying to parse the DTO?

Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree here, let's just deserialise to an object for the known format.

Copy link
Contributor

@thiagohora thiagohora left a comment

Choose a reason for hiding this comment

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

Minor suggestion: It looks good!

}
}
} catch (Exception e) {
return Optional.empty();
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add some log here for visibility. If something goes wrong now it just swallows it.

Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree, probably not at error level as this can be triggered by errors in the parsing of the LLM provider response which we don't control, but maybe at warning level.

Additionally, in order to be sure that this is a parsing error and not a programmatic error from our side, you should probably narrow down this catch to the smaller exception type for that. Probably UncheckedIOException which is what comes from our JsonUtils calls.

JsonNode errorNode = JsonUtils.MAPPER.readTree(jsonPart).get("error");
if (errorNode != null) {
var code = errorNode.get("code").asInt();
if (familyOf(code) == Response.Status.Family.CLIENT_ERROR) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we return only errors related to CLIENT_ERROR? Potential SERVER_ERRORs are ignored.

Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree here, we should forward any known error received from the LLM provider, in this case Gemini. What we shouldn't leak externally is any programmatic or internal error from our side.

Copy link
Contributor

@BorisTkachenko BorisTkachenko left a comment

Choose a reason for hiding this comment

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

Left a few comments

Copy link
Collaborator

@andrescrz andrescrz left a comment

Choose a reason for hiding this comment

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

Please address the important open comments.

Comment on lines +49 to +58
/// gemini throws RuntimeExceptions with message structure as follows:
/// ```
/// java.lang.RuntimeException: HTTP error (429): {
/// "error": {
/// "code": 429,
/// "message": "Resource has been exhausted (e.g. check quota).",
/// "status": "RESOURCE_EXHAUSTED"
/// }
/// }
/// ```
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: please move it as Javadoc comment for this method.

Comment on lines +60 to +61
if (message.contains("{")) {
String jsonPart = message.substring(message.indexOf("{")); // Extract JSON part
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: you can simplify the logic here by just calling indexOf first and checking that not equal to -1.

Comment on lines +64 to +67
JsonNode errorNode = JsonUtils.MAPPER.readTree(jsonPart).get("error");
if (errorNode != null) {
var code = errorNode.get("code").asInt();
if (familyOf(code) == Response.Status.Family.CLIENT_ERROR) {
Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree here, let's just deserialise to an object for the known format.

JsonNode errorNode = JsonUtils.MAPPER.readTree(jsonPart).get("error");
if (errorNode != null) {
var code = errorNode.get("code").asInt();
if (familyOf(code) == Response.Status.Family.CLIENT_ERROR) {
Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree here, we should forward any known error received from the LLM provider, in this case Gemini. What we shouldn't leak externally is any programmatic or internal error from our side.

}
}
} catch (Exception e) {
return Optional.empty();
Copy link
Collaborator

@andrescrz andrescrz Jan 29, 2025

Choose a reason for hiding this comment

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

Agree, probably not at error level as this can be triggered by errors in the parsing of the LLM provider response which we don't control, but maybe at warning level.

Additionally, in order to be sure that this is a parsing error and not a programmatic error from our side, you should probably narrow down this catch to the smaller exception type for that. Probably UncheckedIOException which is what comes from our JsonUtils calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants