Revisiting: Generating OpenAI API Client with Kiota from OpenAPI Spec
/ 4 min read
Table of Contents
In a previous exploration, I utilized the openapi-generator tool to create a Java client for the OpenAI API. Recently, I discovered another generator named Microsoft Kiota, prompting me to investigate whether it could offer a more user-friendly experience for generating API clients.
Just like before, modifications to the original OpenAI OpenAPI specification were necessary, but I successfully generated the client. The resulting work is available at ainoya/openai-kiota-client-java.
Generating the client with Kiota is straightforward, done through the command line. A notable difference from openapi-generator is the reduced number of options required during code generation, which reduces cognitive load—a welcomed change.
docker run -v ${PWD}:/app/output \ -v /${PWD}/openapi.yaml:/app/openapi.yaml \ mcr.microsoft.com/openapi/kiota generate \ -d /app/openapi.yaml \ --language java \ -n dev.ainoya.kiota.openai.generated \ -o /app/output/src/main/java/dev/ainoya/kiota/openai/generatedThe detailed modifications made to the OpenAPI spec can be understood by comparing diffs in the repository, but key changes include adding a discriminator to enable type mapping, which helps eliminate warnings. Since OpenAI’s API server seems to be written in Python, the type handling in responses is generally loose.
ChatCompletionRequestMessage: discriminator: propertyName: role oneOf: - $ref: "#/components/schemas/ChatCompletionRequestSystemMessage" - $ref: "#/components/schemas/ChatCompletionRequestUserMessage"For instances where the API could return either a string or an object, making it challenging to define a discriminator, I opted to comment out the string return type. It’s a workaround due to the API’s loose type constraints on certain parameters, indicating a preference for more strict typing from an API consumer perspective.
@@ -5616,11 +5620,11 @@ components:
`none` is the default when no functions are present. `auto` is the default if functions are present. oneOf:- - type: string- description: >- `none` means the model will not call a function and instead generates a message.- `auto` means the model can pick between generating a message or calling a function.- enum: [none, auto]+# - type: string+# description: >+# `none` means the model will not call a function and instead generates a message.+# `auto` means the model can pick between generating a message or calling a function.+# enum: [none, auto] - $ref: "#/components/schemas/ChatCompletionNamedToolChoice" x-oaiExpandable: trueThe Usability of the Generated Code
Utilizing Kiota brings several benefits, as highlighted in the official documentation, including reduced maintenance cost across different language SDKs, less redundancy in templates, and a consistent feature set across languages. These advantages mainly benefit SDK developers but indirectly enhance the experience for SDK consumers by providing well-maintained tools.
An example of using the generated code for a Chat completion request is straightforward and similar to using code generated by openapi-generator, but with less boilerplate, such as not needing to write setActualInstance methods. This simplicity could be seen as a significant advantage of Kiota.
package dev.ainoya.kiota.openai.example;
import com.microsoft.kiota.ApiException;import com.microsoft.kiota.authentication.AccessTokenProvider;import com.microsoft.kiota.authentication.AllowedHostsValidator;import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;import com.microsoft.kiota.http.OkHttpRequestAdapter;import com.microsoft.kiota.serialization.*;import dev.ainoya.kiota.openai.generated.ApiClient;import dev.ainoya.kiota.openai.generated.models.*;import okhttp3.*;import okhttp3.logging.HttpLoggingInterceptor;import org.jetbrains.annotations.NotNull;import org.jetbrains.annotations.Nullable;
import java.net.URI;import java.util.List;import java.util.Map;
class ExampleBearerTokenProvider implements AccessTokenProvider { // https://learn.microsoft.com/en-us/openapi/kiota/authentication?tabs=java
@NotNull @Override public String getAuthorizationToken(@NotNull URI uri, @Nullable Map<String, Object> additionalAuthenticationContext) { // get token from env variable "OPENAI_API_KEY" return System.getenv("OPENAI_API_KEY"); }
@NotNull @Override public AllowedHostsValidator getAllowedHostsValidator() { return new AllowedHostsValidator( "openai.com" ); }}
public class ExampleApp {
public static void main(String[] args) { final BaseBearerTokenAuthenticationProvider authProvider = new BaseBearerTokenAuthenticationProvider(new ExampleBearerTokenProvider());
HttpLoggingInterceptor logging = new HttpLoggingInterceptor().setLevel( HttpLoggingInterceptor.Level.BASIC // if set level to BODY, kiota client will not work because of the response body is consumed by the interceptor );
Call.Factory httpClient = new OkHttpClient.Builder() .addNetworkInterceptor(logging) .build();
ParseNodeFactory parseNodeFactory = ParseNodeFactoryRegistry.defaultInstance; SerializationWriterFactory serializationWriterFactory = SerializationWriterFactoryRegistry.defaultInstance; final OkHttpRequestAdapter requestAdapter = new OkHttpRequestAdapter(authProvider, null, null, httpClient );
ApiClient client = new ApiClient(requestAdapter);
final CreateChatCompletionRequest request = getCreateChatCompletionRequest();
try { CreateChatCompletionResponse post = client .chat().completions().post(request);
// debug response if (post != null) { var choices = post.getChoices(); if (choices != null) { for (var choice : choices) { if (choice.getMessage() != null) { System.out.println(choice.getMessage().getContent()); } } } else { System.out.println("choices is null"); } } else { System.out.println("post is null"); } } catch (ApiException e) { // handle as ApiException System.out.println(e.getLocalizedMessage()); }
}
@NotNull private static CreateChatCompletionRequest getCreateChatCompletionRequest() { final CreateChatCompletionRequest request = new CreateChatCompletionRequest();
final CreateChatCompletionRequest.CreateChatCompletionRequestModel model = new CreateChatCompletionRequest.CreateChatCompletionRequestModel(); model.setString("gpt-4-turbo-preview");
request.setModel( model );
request.setMaxTokens(100);
ChatCompletionRequestMessage message = new ChatCompletionRequestMessage(); ChatCompletionRequestUserMessage userMessage = new ChatCompletionRequestUserMessage();
ChatCompletionRequestMessageContentPart contentPart = new ChatCompletionRequestMessageContentPart();
ChatCompletionRequestMessageContentPartText partText = new ChatCompletionRequestMessageContentPartText(); partText.setText("What is the meaning of life?"); partText.setType(ChatCompletionRequestMessageContentPartTextType.Text);
contentPart.setChatCompletionRequestMessageContentPartText( partText );
userMessage.setContent(List.of( contentPart ));
userMessage.setRole( ChatCompletionRequestUserMessageRole.User );
message.setChatCompletionRequestUserMessage(userMessage); List<ChatCompletionRequestMessage> messages = List.of( message );
request.setMessages( messages ); return request; }}