1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LiteLLMでPerplexity API+OpenHands CLIの非互換性を回避する

Last updated at Posted at 2025-08-11

概要

2025/8/12時点で「OpenHands CLI」と「Perprexity API」の組み合わせには非互換性の問題がある。「LiteLLM Proxy Server」を挟み込んで回避を試みたが、最終的にカスタムハンドラを実装するハメになったのでその手順を残すものである。試したのはWindowsのみだがカスタムハンドラは共通と思われる。

OpenHands CLIとは?

OpenHands CLIは、オープンソース(MITライセンス)のコマンドラインベースAI開発エージェント。一言で言うと「Claude Codeみたいなもの」だが、好みのローカルLLMやAPIを自由に組み合わせて使用することができる点でより理想的と言える。

image.png
コマンドプロンプトはもちろんVisualStudio Codeのターミナルでも動く

インストールから起動までをおさらいしておく。

インストール

Pythonのpipコマンドでインストールできる。Pythonの環境管理にminiforgeを使用しているのでcondaコマンドで仮想環境を作成及び有効化しているが、環境管理の手段は何でも良い。

Miniforge Prompt
conda create -n openhands-cli python=3.12.11
conda activate openhands-cli
conda install pip
pip install openhands-ai

起動

起動用のバッチファイルを作成して実行する。

openhands-cli.bat
@echo off
call C:\Users\(ユーザー名)\miniforge3\Scripts\activate.bat
call conda activate openhands-cli
openhands

PowerShell「7.x」がインストールされていない場合、『15:28:35 - openhands:ERROR: windows_bash.py:93 - Failed to load PowerShell SDK components. Details: ~ ERROR:root:: name 'DotNetMissingError' is not defined』の様なエラーがでるのでこちらからダウンロードしてインストールする。

PerplexityのAPIを組み合わせた時の問題

ローカルLLMでは思うように動いてくれるモデルがなかなか見つからず(Function callingが伴う操作のハードルが高い)、Perplexity Proのオマケで$5/月のクレジットが付与されるAPIを試してみることにした。ところが期待に反して全く動かない。

  1. 「litellm.UnsupportedParamsError: perplexity does not support parameters: ['stop']」というエラーが発生する
  2. 「PerplexityException - After the (optional) system message(s), user or tool message(s) should alternate with assistant message(s).」というエラーが発生する

1.は、PerplexityがOpenAI互換の「stop」パラメータをサポートしないため。2.は、chat形式のメッセージリストにuserやtool、assistantなどのroleが交互に並んでいないため。PerplexityのAPIはこの辺りが他より厳格だそうな。「データを渡す側は厳格に」「データを受け取る側は寛大に」というI/F実装の基本とは真逆となってしまった悪い例である。

LiteLLM Proxy Serverを導入する

LiteLLM Proxy Serverは、OpenAI形式のAPIで様々なLLMへのリクエストを一元管理できるプロキシサーバーである。フォールバック機能やコスト管理機能などを備えるが、個人の範疇ではアプリケーション毎にURLやAPIキーを設定して回らなくて良いというメリットが大きい。

まずはインストールから起動までをおさらい。

インストール

LiteLLMはPythonのpipコマンドでインストールできる。Pythonの環境管理にminiforgeを使用しているので(以下略)

Miniforge Prompt
conda create -n litellm-proxy
conda activate litellm-proxy
conda install pip
pip install litellm[proxy]

設定

適当なフォルダに設定ファイル「config.yaml」を作成する。

config.yaml
model_list:
  - model_name: perplexity-sonar-pro
    litellm_params:
      model: perplexity/sonar-pro
      api_key: os.environ/PERPLEXITY_API_KEY

  - model_name: openai-gpt-4o
    litellm_params:
      model: openai/gpt-4o
      api_key: os.environ/OPENAI_API_KEY

  - model_name: ollama-deepseek-r1:14b
    litellm_params:
      model: ollama/deepseek-r1:14b
      api_base: http://localhost:11434

general_settings:
  master_key: sk-1234567890

Perplexity APIの他にOpenAIとollamaの例も含めている。最後の「sk-1234567890」がアプリケーションがLiteLLM Proxyに接続する際に使用するAPIキーの指定だ。

起動

起動用バッチファイルを作成して実行する。

litellm.bat
REM @echo off
call C:\Users\(ユーザー名)\miniforge3\Scripts\activate.bat
call conda activate litellm-proxy

X:
cd X:\Tools\litellm

set PERPLEXITY_API_KEY=(Perprexity APIのAPIキー)
set OPENAI_API_KEY=(OpenAIのAPIキー)

litellm --config config.yaml --port 40404

上記は「config.yaml」が「X:\Tools\litellm」に置いてある前提なのでパスは適宜変更する(設定ファイルはフルパスでの指定も可能だが、後述のカスタムハンドラも同じ場所に置くためcdする段取りとしている)。

OpenHands CLIとの連携

「C:\Users\(ユーザー名)\.openhands」に「settings.json」を作成する。

settings.json
{
	"llm_model" : "litellm_proxy/perplexity-sonar-pro",
	"llm_api_key" : "sk-1234567890",
	"llm_base_url" : "http://localhost:40404/v1"
}

以上でLiteLLMの基本的な導入は完了である。

「stop」パラメータ問題の回避

前述の「stop」パラメータの問題についてはLiteLLMの「config.yaml」に以下を記述することで回避できた。

config.yaml
model_list:
  - model_name: perplexity-sonar-pro
  - litellm_params:
        ...
        drop_params: true

「roleの順番」問題の回避

同じく「メッセージリストのroleの順番」の問題は設定の範囲では対処できず、LiteLLMのカスタムハンドラを実装する羽目になった。

まずカスタムハンドラの実装。やっていることはサポート外パラメータの削除とmessage配列の整形だ。コードの主要部分はPerplexityに書いてもらったが、私は普段Pythonを書かないので悪いコードかもしれない点に留意されたし。

custom_handler.py
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.caching.caching import DualCache
from typing import Literal

class MyCustomHandler(CustomLogger):
    def __init__(self):
        pass

    # Perplexity仕様 messages整形例
    async def async_pre_call_hook(self, user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: dict, call_type: Literal[
        "completion", "text_completion", "embeddings", "image_generation", "moderation", "audio_transcription"
    ]):
        # 例: サポート外パラメータ除去
        for unsupported in ["stop", "web_search_options"]:
            data.pop(unsupported, None)

        # messages配列の交互整形例
        def enforce_order(messages):
            fixed = []
            prev = None
            for m in messages:
                if prev in ("user", "tool") and m["role"] in ("user", "tool"):
                    fixed.append({"role": "assistant", "content": ""})
                fixed.append(m)
                prev = m["role"]
            return fixed

        if "messages" in data:
            data["messages"] = enforce_order(data["messages"])
        return data  # 修正payload返却

proxy_handler_instance = MyCustomHandler()

この「custom_handler.py」ファイルはLiteLLM起動時のカレントディレクトリ(前述の起動バッチの例なら「X:\Tools\litellm」)に置いたが、モジュール検索パスに含まれるディレクトリならどこでも良いとのこと。

次にカスタムハンドラの有効化。「config.yaml」に以下を追記する。

config.yaml
litellm_settings:
  callbacks: custom_handler.proxy_handler_instance

「custom_handler」が.pyファイルの名称、「proxy_handler_instance」がMyCustomHandlerクラスのインスタンスの変数名である。以上でカスタムハンドラが働くようになる。

Perplexity APIでOpenHands CLIが動いた!

「Web画面のおみくじアプリを作って。全てのコードを1つのHTMLファイルに記述して。a.htmlというファイルで保存して。」の指示でHTMLファイルを生成できるようになった。

{0F48DFA2-DFD7-4954-A373-DCB6B7D807BD}.png

さらに「a.htmlをブラウザで開いて。」の指示でStart-Processしてくれた。

{E632FE08-9080-4872-B88B-28559C034C89}.png

なかなか高機能な「おみくじアプリ」だった。「Hello!」の問いかけで例外を吐いていたヤツがここまで出来たなら上等だろう。めでたしめでたし。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?