CLOVER🍀

That was when it all began.

OpenAIのJavaライブラリーからOpenAI API互換のサーバーへアクセスしてみる

これは、なにをしたくて書いたもの?

これまでよくOpenAI API互換のサーバーにOpenAI Python APIライブラリーからアクセスして試していたのですが、1度Javaからも
アクセスしてみようかなと思いまして。

アクセス先としては、llama-cpp-pythonを使うことにします。

OpenAI API向けのJavaクライアント

OpenAI APIのJavaクライアントにアクセスするためのライブラリーの話に…入る前に、ちょっと自分がここをやろうとしている理由について
少し書いておきます。

自分はよくOpenAI API互換のサーバーをローカルで動かしていますが、OpenAIに限らずローカルLLMを使いたい場合はPythonからの
アクセスが基本になると思います。直接実行する場合もあると思いますが。

それはそれでよいのですが、テキスト埋め込みはベクトル検索でも使うのでこれ単体で勉強しようと思った時にはベクトル化の手段を
持っておく必要があります。で、Javaでやる場合はどうしようかな?と。

ここで、著名なJavaライブラリーがどのような方法を取っているか確認してみます。

Spring AIはOpenAI、Ollama、Azure OpenAI、PostgresML、Google VertexAI PaLM2、Amazon Bedrock、Transformers(ONNX)、
Mistral AIといった感じで外部サービスに頼る形態になっています。

Embeddings API :: Spring AI Reference

LangChain4jではどうでしょうか。

こちらはインプロセス(ONNX)、Amazon Bedrock、Azure OpenAI、DashScope、Google Vertex AI、HuggingFace、LocalAI、Mistral AI、
Nomic、Ollama、OpenAI、Qianfan、ZhipuAIといった感じでやっぱり外部サービスに頼る感じになっていますね。

Embedding Models | LangChain4j

やっぱりこの分野はJava内では完結しませんよね。

というわけで、なにか別のプロセスやAPIを使う手段を押さえておくとよさそうです。

となると、OpenAI API互換のサーバーにアクセスするのが自分としては今後もやりやすいのかなと。

では、OpenAIのドキュメントで紹介されているJavaクライアントを見ておきましょう。

Libraries

オフィシャルクライアントというものはありません。コミュニティが開発しているOpenAI-Javaというライブラリーが挙げられています。

GitHub - TheoKanning/openai-java: OpenAI Api Client in Java

なお、2024年になってからは更新されていないみたいです…。リポジトリーの状態もちょっと微妙です…。

What is the status of this library❓❓ · Issue #491 · TheoKanning/openai-java · GitHub

他のライブラリーもこちらを使っているのでしょうか?

Spring AIは自分で実装しているようです。

https://github.com/spring-projects/spring-ai/tree/v0.8.1/models/spring-ai-openai/src/main/java/org/springframework/ai/openai

LangChain4jでは別のライブラリーを使っているようです。

https://github.com/langchain4j/langchain4j/blob/0.30.0/langchain4j-open-ai/pom.xml#L19-L22

こちらのJava client library for OpenAI APIですね。リポジトリー名を見るとOpenAI4jと呼んでもよさそうですが。

GitHub - ai-for-java/openai4j: Java client library for OpenAI API

こちらもそんなに開発が活発というわけではなさそうです…。

なお、どちらのライブラリーもRetrofitを使っています(OpenAI-Javaのapiモジュールはまた別ですが)。

Retrofit

Spring AIが自前な理由がなんとなくわからないでもないですね。

自分でOpenAI APIのOpenAPIから自動生成してもいいのではないかという気もしますが。

GitHub - openai/openai-openapi: OpenAPI specification for the OpenAI API

せっかくならOpenAI APIを使えるようにと調べてみましたが、テキスト埋め込みをしたいだけだったらちょっと別の方法を考えた方が
いいかもですね。

どうするか迷いましたが、今回はいったんこのままOpenAI API路線で突き進むことにして、Java client library for OpenAI APIを
使うことにします。

GitHub - ai-for-java/openai4j: Java client library for OpenAI API

README.mdに使い方も書かれていますし。

OpenAI API互換のサーバーはllama-cpp-pythonとして、Java client library for OpenAI APIからチャットモデルにアクセスしてみることに
します。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.2 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-105-generic", arch: "amd64", family: "unix"

llama-cpp-pythonを動作させる環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

llama-cpp-pythonでOpenAI API互換のサーバーを起動する

まずはアクセス先のOpenAI API互換のサーバーを起動します。

llama-cpp-pythonのインストール。

$ pip3 install llama-cpp-python[server]

依存関係など。

$ pip3 list
Package           Version
----------------- -------
annotated-types   0.6.0
anyio             4.3.0
click             8.1.7
diskcache         5.6.3
exceptiongroup    1.2.1
fastapi           0.110.2
h11               0.14.0
idna              3.7
Jinja2            3.1.3
llama_cpp_python  0.2.65
MarkupSafe        2.1.5
numpy             1.26.4
pip               22.0.2
pydantic          2.7.1
pydantic_core     2.18.2
pydantic-settings 2.2.1
python-dotenv     1.0.1
PyYAML            6.0.1
setuptools        59.6.0
sniffio           1.3.1
sse-starlette     2.1.0
starlette         0.37.2
starlette-context 0.3.6
typing_extensions 4.11.0
uvicorn           0.29.0

モデルはGemmaの量子化済み2Bのものにします。

mmnga/gemma-2b-it-gguf · Hugging Face

ダウンロード。

$ curl -L https://huggingface.co/mmnga/gemma-2b-it-gguf/resolve/main/gemma-2b-it-q4_K_M.gguf?download=true -o gemma-2b-it-q4_K_M.gguf

起動。

$ python3 -m llama_cpp.server --model gemma-2b-it-q4_K_M.gguf --chat_format gemma

これでllama-cpp-pythonの準備は完了です。

Java client library for OpenAI API(OpenAI4j)からOpenAI API互換のサーバーへアクセスする

それでは、Java client library for OpenAI API(OpenAI4j)からOpenAI API互換のサーバーへアクセスしてみます。

まずはMaven依存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>dev.ai4j</groupId>
            <artifactId>openai4j</artifactId>
            <version>0.17.0</version>
        </dependency>
    </dependencies>

現時点の最新版は0.17.0のようなのですが、リポジトリーにタグがありません…。

ソースコードの雛形はこんな感じで用意。

src/main/java/org/littlewings/openai/openai4j/OpenAi4jExample.java

package org.littlewings.openai.openai4j;

import dev.ai4j.openai4j.OpenAiClient;
import dev.ai4j.openai4j.chat.ChatCompletionModel;
import dev.ai4j.openai4j.chat.ChatCompletionRequest;
import dev.ai4j.openai4j.chat.ChatCompletionResponse;

public class OpenAi4jExample {
    public static void main(String... args) {
        // ここにOpenAI APIを使うコードを書く
    }
}

まずはOpenAIへアクセスするためのクライアントのインスタンスを作成します。

        OpenAiClient client =
                OpenAiClient.builder()
                        .openAiApiKey("dummy-api-key")
                        .baseUrl("http://localhost:8000/v1")
                        .build();

baseUrlには、llama-cpp-pythonのエンドポイントを指定するのですが。/v1まで指定しないとNot Foundになりました…。

Java client library for OpenAI API / Code examples / Create an OpenAI Client

Chat Completions APIへのアクセス。

Java client library for OpenAI API / Code examples / Chat Completions

どのAPIもそうなのですが、同期、非同期(コールバック)、ストリーミングの3種類の呼び出し方があります。

今回は同期で使います。

        ChatCompletionRequest request =
                ChatCompletionRequest.builder()
                        .model(ChatCompletionModel.GPT_3_5_TURBO)
                        .addUserMessage("Could you introduce yourself?")
                        .temperature(0.0)
                        .build();

        ChatCompletionResponse response = client.chatCompletion(request).execute();
        System.out.printf("request -> response, message = %n%s%n", response.choices().getFirst().message().content());

Chat Completions APIでの例ですが、OpenAiClient#chatCompletionの戻り値はSyncOrAsyncOrStreamingという型になっており、
この後にexecuteを呼び出すと同期、onResponseを呼び出すと非同期、onPartialResponseを呼び出すとストリーミングというように
分岐します。

今回はexecuteを呼び出しているので同期ですね。

リクエストとレスポンスの型は、OpenAI API OpenAPIの定義が元になっているので他の言語のクライアントライブラリーと内容に
大差はありません。

ちなみに、OpenAiClient#chatCompletionでユーザーのメッセージを渡すだけの簡単な使い方もあるみたいです。この時の戻り値は
メッセージのみになるようです。

        System.out.printf(
                "simple request -> simple response, message = %n%s%n",
                client.chatCompletion("Could you introduce yourself?").execute()
        );

実装を見るとこんな感じになっていました。

    @Override
    public SyncOrAsyncOrStreaming<String> chatCompletion(String userMessage) {
        ChatCompletionRequest request = ChatCompletionRequest.builder().addUserMessage(userMessage).build();

        ChatCompletionRequest syncRequest = ChatCompletionRequest.builder().from(request).stream(null).build();

        return new RequestExecutor<>(
                openAiApi.chatCompletions(syncRequest, apiVersion),
                ChatCompletionResponse::content,
                okHttpClient,
                formatUrl("chat/completions"),
                () -> ChatCompletionRequest.builder().from(request).stream(true).build(),
                ChatCompletionResponse.class,
                r -> r.choices().get(0).delta().content(),
                logStreamingResponses
        );
    }

最後にシャットダウン。

        client.shutdown();

では実行してみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.openai.openai4j.OpenAi4jExample

結果。

request -> response, message =
Hello! I am a large language model, trained by Google. I am a conversational AI that can assist you with a wide range of tasks, including answering questions, generating text, and translating languages.

Is there anything specific I can help you with today?

simple request -> simple response, message =
Greetings! I'm a large language model, trained by Google. I'm here to assist you with a wide range of text and language tasks. How can I help you today?

OKそうですね。

簡単ですが、今回はこんな感じでおわりにしましょう。

おわりに

OpenAIのJavaライブラリーから、OpenAI API互換のサーバーへアクセスしてみるということでJava client library for OpenAI API(OpenAI4j)を
試してみました。

今回はこのライブラリーの使い方というより、JavaでのOpenAI周辺のライブラリーの状況がなんとなくわかったことの方が大きいかもですね。
自分は基本的にローカルLLMを使うので、アクセス方法や使うLLM自体ももう少し考えた方がよさそうだな、と思いました。