-
Notifications
You must be signed in to change notification settings - Fork 26
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
Refactor to add a base class and dedicated classes for Azure and Anyscale #47
Conversation
Codecov ReportAttention:
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #47 +/- ##
============================================
- Coverage 87.94% 85.91% -2.03%
- Complexity 602 609 +7
============================================
Files 105 109 +4
Lines 937 1001 +64
Branches 27 33 +6
============================================
+ Hits 824 860 +36
- Misses 68 89 +21
- Partials 45 52 +7 ☔ View full report in Codecov by Sentry. |
@the-gigi You have changed 95 files for this PR! Please, discard unnecessary changes and try to focus on the main goal of the Issue. |
I did optimize imports in the IDE to find all unused imports. Apparently it also sorted imports in other files. Are you opposed to sorted imports? This will be a one time thing. |
The order is following a lint rule. I need to check how to share it, to be on the same page. |
@the-gigi Have you verified if those providers support the vision feature? As you should know, the Chat Completion service also provides the capacity to recognize images and you can ask the model about image details. If you haven't, please verify and comment the results. Check details here: https://platform.openai.com/docs/guides/vision |
@sashirestela here is the next iteration of the PR for your review. With the BaseSimpleOpenAI being just an interface as you requested there is a lot of duplication in all 3 implementations (lots of fields and similar logic in constructors). All this duplication can be pushed down to the base class as I did in the previous iteration. This is cleaner IMO, but in the end it's your decision. I fixed an issue that surfaces only with Azure OpenAI in the ChatRequest class - If tools are provided, but tools choice is null it fails on Azure OpenAI. I changed it so, if tools are provided, but no tools choice it set tools choice to AUTO. If you have special formatting or linting rules you may consider sharing them as a pre-commit git hook. Or even better have github actions that to perform them automatically on PRs. This can save a lot of unnecessary work. The chat completion with streaming doesn't work in AzureOpenAI (Not calling it in the AzureChatServiceDemo). I didn't test with images. I'll test and let you know the status. |
First, thanks for reducing the number of modified files on your PR. I'll take the task to improve the linting rules. Regarding the main change, I understand your concerns and we could do a slight variation in order to reduce the repeated code: Let's turn abstract class BaseSimpleOpenAI {
protected static final END_OF_STREAM = "[DONE]";
@Getter
protected String apiKey;
@Getter
protected String baseUrl;
@Getter
protected HttpClient httpClient;
@Getter
@Setter
protected CleverClient cleverClient;
protected Map<String, String> headers;
protected UnaryOperator<HttpRequestData> requestInterceptor;
protected OpenAI.Audios audioService;
protected OpenAI.ChatCompletions chatCompletionService;
protected OpenAI.Completions completionService;
// etc.
@SuperBuilder
public BaseSimpleOpenaAI(String apiKey, String baseUrl, HttpClient httpClient) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient());
this.headers = new HashMap<String, String>();
this.requestInterceptor = null;
}
protected void postContructor() {
customSetup();
this.cleverClient = CleverClient.builder()
.httpClient(this.httpClient)
.baseUrl(this.baseUrl)
.headers(headers)
.endOfStream(END_OF_STREAM)
.requestInterceptor(requestInterceptor)
.build();
}
abstract protected void customSetup();
// Common service calls the cleverClient.create() method. Don't include this comment.
public OpenAI.ChatCompletions chatCompletions() {
if (chatCompletionService == null) {
chatCompletionService = cleverClient.create(OpenAI.ChatCompletions.class);
}
return chatCompletionService;
}
// No common service throws an exception. Don't include this comment.
public OpenAI.Audios audios() {
throws new UnimplementedException();
}
// No common service throws an exception. Don't include this comment.
public OpenAI.Completions completions() {
throws new UnimplementedException();
}
// etc.
} Now, SimpleOpenAI can add the specific parameters and overwrite the No-common services: public class SimpleOpenAI extends BaseSimpleOpenaAI {
@Getter
private String organizationId;
@SuperBuilder
public SimpleOpenaAI(@NonNull String apiKey, String baseUrl, HttpClient httpClient, String organizatonId) {
super(apiKey, baseUrl, httpClient);
this.organizationId = organizationId;
postConstructor();
}
protected void customSetup() {
baseUrl = Optional.ofNullable(baseUrl).orElse(OPENAI_BASE_URL);
headers.put(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION + apiKey);
if (organizationId != null) {
headers.put(ORGANIZATION_HEADER, organizationId);
}
}
// Don't need to implement common services (ie: chatCompletions), but no-common, do.
public OpenAI.Audios audios() {
if (audioService == null) {
audioService = cleverClient.create(OpenAI.Audios.class);
}
return audioService;
}
public OpenAI.Completions completions() {
if (completionService == null) {
completionService = cleverClient.create(OpenAI.Completions.class);
}
return completionService;
}
// etc.
} Now, SimpleOpenAIAzure can add the specific parameters and doesn't need overwrite any services: public class SimpleOpenAIAzure extends BaseSimpleOpenaAI {
@Getter
private String apiVersion;
@SuperBuilder
public SimpleOpenaAIAzure(@NonNull String apiKey, @NonNull String baseUrl, HttpClient httpClient, @NonNull String apiVersion) {
super(apiKey, baseUrl, httpClient);
this.apiVersion = apiVersion;
postConstructor();
}
protected void customSetup() {
headers.put(APIKEY_HEADER, apiKey);
requestInterceptor = request -> {
// modify the url to add the property 'this.apiVersion' as query param
// modify the url to remove /v1
// modify the body to remove the model property
return request;
}
}
// Don't need to implement any service.
} |
Regarding your change in the The right place for your change is on the OpenAI interface on the default methods. Specifically, you should do your change here for blocking chats and here for streaming chats. Take in account that we are working with immutable objects, so, you need to use the Lombok's |
I see a small improvement in the url for SimpleOpenAIAzure: public class SimpleOpenAIAzure extends BaseSimpleOpenaAI {
@Getter
private String apiVersion;
@SuperBuilder
public SimpleOpenaAIAzure(@NonNull String apiKey, String baseUrl, HttpClient httpClient, @NonNull String resourceName, @NonNull String deploymentId, @NonNull String apiVersion) {
super(apiKey, "https://"+resourceName+Optional.ofNullable(baseUrl).orElse(AZURE_BASE_URL)+deploymentId, httpClient);
this.apiVersion = apiVersion;
postConstructor();
}
...
} |
@sashirestela this all sounds good. I'm not clear about the value that postConstructor() and customSetup() methods provide. in your sample code BaseSimpleOpenAI creates the cleverclient in the postConstructor, but it doesn't call postConstructor() at the end of the BaseSimpleOpenaAI constructor. This means that sub-classes are now responsible for calling postConstructor() in order for the base class to have a cleverclient. Also, postConstructor is protected, which means sub-classes can override it and do anything they want instead of creating a cleverclient.
is it possible you meant to call postConstructor() here and make it private while the protected customSetup()is where sub-classes do their customization? something like:
However in this case, the postConstructor logic can just be collapsed into the constructor as in:
This workflow allows the sub-class to modify the fields of the base class used to build the clever client. However, this has some problems. First, when looking at the constructor it's not clear what parameters are needed. it divides the parameters into 3 groups: mandatory constructor arguments.(apiKey and baseUrl), optional constructor arguments (httpClient) and "hidden" optional arguments that may be set in customSetup() like the headers and the request interceptor. Another problem is that different parameters can be set in 3 different places now. For example, the httpClient can be passed as constructor argument or as null and then a default HttpClient is created. But, it can also be overridden in customSetup() later. Similarly, baseUrl can be passed as constructor argument, be set later in customSetup() and also be modified later in requestInterceptor. This can be very confusing when trying to troubleshoot some misbehaving sub-class. What do you think about this alternative? BaseSimpleOpenAi has a constructor that takes a single argument BaseSimpleOpenAiArgs.
It is very clean now nad has a single filed for cleverClient (with a setter because of the tests)
It also has per your suggestion the implementation of chat services and throw the
BaseSimpleOpenAiArgs has all the arguments needed to build a clever client and a builder
Note that it doesn't have an api key because the different providers will already Finally, the different providers are very simple too. Their constructor is just one line Here SimpleOpenAI
There is no need even for @SuperBuilder because the base class doesn't have a builder. SimpleOpenAI also implements all the endpoints except chatServices that the base already implements:
With this design we can also get rid in OpenSimpleAI from all the @Getter(AccessLevel.NONE) SimpleOpenAIAnyscale is the simplest. Here is literally the entire class:
SimpleOpenAiAzure is similar except it sets a request interceptor too in its prepare method:
|
.build(); | ||
assertTrue(openAI.getCleverClient().getHeaders().containsValue(openAI.getOrganizationId())); | ||
} | ||
// @Test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these tests check for set properties. they use getters for fields that don't exist anymore. it also seems a little awkward to have a getter only for tests. I think these tests don't provide much value. but, if you think it is useful we can restore them and add the getters again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take in account that we point to have a high test coverage of the code. You could remove it, but you should provide alternative unit tests.
On the other hand, please, provide unit tests for the changes and new classes introduced in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the existing tests pass except for these tests that check for fields that don't exist anymore. the organizationId is embedded into a header in the new design.
with your permission I'll delete the commented out tests.
I'll for the new classes I'll add tests for the prepare functions.
I think what can really increase the testability and the confidence that simple-openai works as expected and changes are safe is to add end-to-end tests. pretty much what the demos are doing, but against a dedicated HTTP test server, not the real providers of course. Each provider will have its own mock service that conforms to its convention and can check the headers and base url passed in and the request schema. Obviously , this is a bug change so not for today. WDYT?
Your proposal is not bad. Let's move in that direction. I'm going to review the code details. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@the-gigi Please, provide changes or answers.
*/ | ||
|
||
|
||
public class BaseSimpleOpenAI { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be an abstract class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. It can be instantiated on its own. For example, if someone wants to access another OpenAI provider that is not one of the officially supported: OpenAI, Azure OpenAI or Anyscale. They can directly proper create BaseSimpleOpenAIArgs and instantiate a BaseSimpleOpenAI object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, turn into an abstract class. Any other provider must extend this abstract class
|
||
@Getter | ||
@Builder | ||
public class BaseSimpleOpenAiArgs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent, change Ai
to AI
in the class name
private OpenAI.Moderations moderationService; | ||
|
||
@Getter(AccessLevel.NONE) | ||
private OpenAI.Assistants assistantService; | ||
|
||
@Getter(AccessLevel.NONE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you keep this Getter for any special reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no. I just missed it somehow.
} | ||
|
||
@Builder | ||
public SimpleOpenAIAnyscale( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the parameters could be in the same line of the constructor name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the change here
@Builder | ||
public SimpleOpenAIAnyscale( | ||
@NonNull String apiKey, | ||
@NonNull String baseUrl, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does Anyscale need a different url every time? Isn't that the same as OpenAI that has a standard base url? If that is the case, you could create a constant ANYSCALE_BASE_URL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They plan to introduce a private endpoints service (more enterprise ready). I'm not sure what the base URL situation is going to be. but, I can make it optional and use the current base URL by default if not provided.
import java.util.Base64; | ||
import java.util.List; | ||
|
||
public class AzureChatServiceDemo extends AbstractDemo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent with the other demos, rename it to ChatAzureServiceDemo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the renaming.
private ChatRequest chatRequest; | ||
|
||
|
||
@SuppressWarnings("unchecked") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary anymore here.
public class AzureChatServiceDemo extends AbstractDemo { | ||
private ChatRequest chatRequest; | ||
|
||
@SuppressWarnings("unchecked") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary anymore here.
import java.io.InputStream; | ||
import java.util.EnumSet; | ||
import java.util.List; | ||
import java.util.Optional; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this unused import
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you have bypassed many observations on my first revision. Please, review carefully
*/ | ||
|
||
|
||
public class BaseSimpleOpenAI { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, turn into an abstract class. Any other provider must extend this abstract class
import io.github.sashirestela.openai.function.FunctionExecutor; | ||
import java.util.ArrayList; | ||
|
||
public class AnyscaleChatServiceDemo extends AbstractDemo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the renaming
import java.util.Base64; | ||
import java.util.List; | ||
|
||
public class AzureChatServiceDemo extends AbstractDemo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the renaming.
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NonNull; | ||
import lombok.Setter; | ||
|
||
/** | ||
* The factory that generates implementations of the {@link OpenAI OpenAI} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update the comment according to my previous observation.
import lombok.NonNull; | ||
|
||
/** | ||
* The factory that generates implementations of the {@link OpenAI OpenAI} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update the comment according to my previous observation.
@@ -13,6 +15,7 @@ | |||
import io.github.sashirestela.openai.domain.chat.tool.ChatTool; | |||
import io.github.sashirestela.openai.domain.chat.tool.ChatToolChoice; | |||
import io.github.sashirestela.openai.domain.chat.tool.ChatToolChoiceType; | |||
import java.util.Optional; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the change here.
@@ -29,7 +32,7 @@ public class ChatRequest { | |||
private ChatRespFmt responseFormat; | |||
private Integer seed; | |||
private List<ChatTool> tools; | |||
private Object toolChoice; | |||
@With private Object toolChoice; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any unit test for this?
HttpClient httpClient) { | ||
|
||
if (isNullOrEmpty(baseUrl)) { | ||
baseUrl = DEFAULT_BASE_URL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change this for Optional
|
||
class SimpleOpenAIAnyscaleTest { | ||
@Test | ||
void shouldPrepareBaseOpenSimpleAIArgsCorrectly() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could check that the default url is taken when you no pass any one and the same for the httpClient
.organizationId("orgId") | ||
.build(); | ||
assertTrue(openAI.getCleverClient().getHeaders().containsValue(openAI.getOrganizationId())); | ||
void shouldPrepareBaseOpenSimpleAIArgsCorrectly() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check when baseUrl ans httpClient are not passed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am approving this, but there are some observations that I need to address on a different PR
@@ -48,12 +46,6 @@ public ChatRequest(@NonNull String model, @NonNull @Singular List<ChatMsg> messa | |||
Integer seed, @Singular List<ChatTool> tools, Object toolChoice, Double temperature, Double topP, Integer n, | |||
Boolean stream, Object stop, Integer maxTokens, Double presencePenalty, Double frequencyPenalty, | |||
Map<String, Integer> logitBias, String user, Boolean logprobs, Integer topLogprobs) { | |||
if (toolChoice != null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@the-gigi Why have you removed this type validation from here?
if (!isNullOrEmpty(chatRequest.getTools())) { | ||
if (toolChoice == null) { | ||
toolChoice = ChatToolChoiceType.AUTO; | ||
} else if (!(toolChoice instanceof ChatToolChoice) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@the-gigi Why have you brought this type validation code? This should keep on the ChatRequest class
See discussion here:
#45
Also added demos for each of the new classes
tested on all providers OpenAI, Azure OpenAI and Anyscale.
Azure OpenAI supports blocking chat completion + functions (fails on streaming)
Anyscale supports streaming, blocking + functions