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;    }}