skip to content
ainoya.dev

Revisiting: Generating OpenAI API Client with Kiota from OpenAPI Spec

/ 4 min read

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/generated

The 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: true

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