概要
2025/8/12時点で「OpenHands CLI」と「Perprexity API」の組み合わせには非互換性の問題がある。「LiteLLM Proxy Server」を挟み込んで回避を試みたが、最終的にカスタムハンドラを実装するハメになったのでその手順を残すものである。試したのはWindowsのみだがカスタムハンドラは共通と思われる。
OpenHands CLIとは?
OpenHands CLIは、オープンソース(MITライセンス)のコマンドラインベースAI開発エージェント。一言で言うと「Claude Codeみたいなもの」だが、好みのローカルLLMやAPIを自由に組み合わせて使用することができる点でより理想的と言える。
コマンドプロンプトはもちろんVisualStudio Codeのターミナルでも動く
インストールから起動までをおさらいしておく。
インストール
Pythonのpipコマンドでインストールできる。Pythonの環境管理にminiforgeを使用しているのでcondaコマンドで仮想環境を作成及び有効化しているが、環境管理の手段は何でも良い。
conda create -n openhands-cli python=3.12.11
conda activate openhands-cli
conda install pip
pip install openhands-ai
起動
起動用のバッチファイルを作成して実行する。
@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を試してみることにした。ところが期待に反して全く動かない。
- 「litellm.UnsupportedParamsError: perplexity does not support parameters: ['stop']」というエラーが発生する
- 「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を使用しているので(以下略)
conda create -n litellm-proxy
conda activate litellm-proxy
conda install pip
pip install litellm[proxy]
設定
適当なフォルダに設定ファイル「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キーの指定だ。
起動
起動用バッチファイルを作成して実行する。
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」を作成する。
{
"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」に以下を記述することで回避できた。
model_list:
- model_name: perplexity-sonar-pro
- litellm_params:
...
drop_params: true
「roleの順番」問題の回避
同じく「メッセージリストのroleの順番」の問題は設定の範囲では対処できず、LiteLLMのカスタムハンドラを実装する羽目になった。
まずカスタムハンドラの実装。やっていることはサポート外パラメータの削除とmessage配列の整形だ。コードの主要部分はPerplexityに書いてもらったが、私は普段Pythonを書かないので悪いコードかもしれない点に留意されたし。
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」に以下を追記する。
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ファイルを生成できるようになった。
さらに「a.htmlをブラウザで開いて。」の指示でStart-Processしてくれた。
なかなか高機能な「おみくじアプリ」だった。「Hello!」の問いかけで例外を吐いていたヤツがここまで出来たなら上等だろう。めでたしめでたし。