🚀

Ollamaをチームで使うためのOSS、Ollama4Teamを公開しました

2024/05/28に公開

TLDR

Ollamaをチームで使えるソフトウェアをOSSで公開しました。
機能として、

  • OllamaのAPIを呼ぶ際の簡易的な認証
  • ウェブから
    • メンバー管理
    • 統計情報の表示
    • モデルの追加・削除

を提供しています。
こちらからインストールできます。
https://github.com/fjm2u/ollama4team
ぜひスターお願いします(スター乞食)。

背景

Llama等のオープンなモデルは外部にデータを公開せずに利用できる等のメリットがあります。しかし計算資源的に、70B等の大きなパラメータのモデルを手元の環境で動かすことは難しくなります。

そこで、チーム(中小規模の企業、大学の研究室等)が所有する共有の計算資源でローカルモデル用のサーバを立てたくなります。ローカルモデルを手軽に動かすのに便利なソフトウェアとしてOllamaがありますが、Ollamaはチームで利用するために必要な機能が不足しています。

チームで共有してローカルのモデルを利用する場合、

  • サーバを建てるので、利用者は認証したい
  • 誰がいつどの程度利用しているのかを知りたい

という要望があります。
その要望に応えるのがOllama4Teamです。

Ollama4Team


JupyterHubに着想を得てます。

インストール

詳細はリポジトリにありますが、

  1. Git clone
  2. envを編集
  3. Docker-compose up

でOllamaを含めて起動できます。

Generate or embeddings

Generate(https://OLLAMA4TEAM_URL/api/generate)とEmbeddings(https://OLLAMA4TEAM_URL/api/embeddings)があります。Streamにも対応しています。

curl -X POST -H "Content-Type: application/json" -H "Authorization: Basic YOUR_PASSWORD" -d '{"model": "YOUR_MODEL", "prompt": "YOUR_PROMPT"}' "https://OLLAMA4TEAM_URL/api/embeddings"

で呼べます。

OllamaのAPIに準拠していて、Authorization: Basic YOUR_PASSWORDを付け加えます。

  • Generate a completion
  • Generate Embeddings

Langchainから使う方法はTipsに記述してます。

メンバー管理

下図のようにメンバーを管理できます。adminは基本的に全ての操作を、memberは自分のパスワード変更やアカウント追加が許可されています(今後、権限はenvで詳細に設定できるようにしていきます)。

モデルの追加・削除

統計情報の表示


統計情報のダッシュボードは充足予定です。

Tips

Langchainでの利用

以下コードをコピペして、

コピペコード
from typing import Any, Dict, Iterator, List, Optional

import requests
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings

class Ollama4Team(Ollama):
    password: str

    def _create_stream(
        self,
        api_url: str,
        payload: Any,
        stop: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> Iterator[str]:
        if self.stop is not None and stop is not None:
            raise ValueError("`stop` found in both the input and default params.")
        elif self.stop is not None:
            stop = self.stop

        params = self._default_params

        for key in self._default_params:
            if key in kwargs:
                params[key] = kwargs[key]

        if "options" in kwargs:
            params["options"] = kwargs["options"]
        else:
            params["options"] = {
                **params["options"],
                "stop": stop,
                **{k: v for k, v in kwargs.items() if k not in self._default_params},
            }

        if payload.get("messages"):
            request_payload = {"messages": payload.get("messages", []), **params}
        else:
            request_payload = {
                "prompt": payload.get("prompt"),
                "images": payload.get("images", []),
                **params,
            }

        response = requests.post(
            url=api_url,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Basic {self.password}",
                **(self.headers if isinstance(self.headers, dict) else {}),
            },
            json=request_payload,
            stream=True,
            timeout=self.timeout,
        )
        response.encoding = "utf-8"
        if response.status_code != 200:
            if response.status_code == 404:
                raise self.OllamaEndpointNotFoundError(
                    "Ollama call failed with status code 404. "
                    "Maybe your model is not found "
                    f"and you should pull the model with `ollama pull {self.model}`."
                )
            else:
                optional_detail = response.text
                raise ValueError(
                    f"Ollama call failed with status code {response.status_code}."
                    f" Details: {optional_detail}"
                )
        return response.iter_lines(decode_unicode=True)

    @property
    def _identifying_params(self) -> Dict[str, Any]:
        """Return a dictionary of identifying parameters."""
        return {
            # The model name allows users to specify custom token counting
            # rules in LLM monitoring applications (e.g., in LangSmith users
            # can provide per token pricing for their model and monitor
            # costs for the given LLM.)
            "model_name": self.model,
        }

    @property
    def _llm_type(self) -> str:
        """Get the type of language model used by this chat model. Used for logging purposes only."""
        return "Ollama4Team"


class Ollama4TeamEmbeddings(OllamaEmbeddings):
    password: str

    def _process_emb_response(self, input: str) -> List[float]:
        """Process a response from the API.

        Args:
            response: The response from the API.

        Returns:
            The response as a dictionary.
        """
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Basic {self.password}",
            **(self.headers or {}),
        }

        try:
            res = requests.post(
                f"{self.base_url}/api/embeddings",
                headers=headers,
                json={"model": self.model, "prompt": input, **self._default_params},
            )
        except requests.exceptions.RequestException as e:
            raise ValueError(f"Error raised by inference endpoint: {e}")

        if res.status_code != 200:
            raise ValueError(
                "Error raised by inference API HTTP code: %s, %s"
                % (res.status_code, res.text)
            )
        try:
            t = res.json()
            return t["embedding"]
        except requests.exceptions.JSONDecodeError as e:
            raise ValueError(
                f"Error raised by inference API: {e}.\nResponse: {res.text}"
            )

以下のように呼び出せます。

llm = Ollama4Team(model="llama3", password="password", base_url="https://localhost:3000")
for chunk in llm.stream("Write me a 1 verse song about sparkling water."):
    print(chunk, end="|", flush=True)


embeddings = Ollama4TeamEmbeddings(model="llama3", password="password", base_url="https://localhost:3000")
text = "This is a test document."
query_result = embeddings.embed_query(text)

base_urlにはOllama4TeamのURLを指定し、passwordには自分のアカウントのパスワードを指定します。

さいごに

Ollamaをチームで使うためのOSSを作成しました。

共有の計算資源でローカルLLMを立ち上げる必要のある方は、ぜひ触ってみてください!
そしてPRやIssueはウェルカムです!励みになるのでスター☆ポチっとお願いしますー。
https://github.com/fjm2u/ollama4team

Discussion