Open7

OpenHands CLIをLocal LLMで試してみる

vArvAr

https://x.com/neubig/status/1935094481537384589

新しいコーディング用CLIツール、OpenHands CLI を公開しました!

  • 最高性能(Claude Code と同等)
  • 完全オープンソース、MIT ライセンス - モデル非依存:API を使っても自前モデルでもOK
  • インストール&実行が簡単:pip install openhands-aiとopenhandsを実行するのみ(Docker 不要!)

DevinのOSSクローンであるOpenHandsがCLIに対応。(2025/06/18)

これまでOpenHandsを動かすにはコンテナが必要で、動作が重いなどの不満があったようだ。

OpenHands-CLIはClaude Codeのようにターミナルで軽量に動作するとのこと。

vArvAr

いま流行りのAnthropicのClaude CodeやOpenAIのCodexなどはベンダーロックイン(プロバイダモデルしか使えない)の面で課題があった。

センシティブな内容やプライベートなプロジェクトで使いたいときはローカルLLMを使いたくなるが、前述の仕様が障壁になっていた。

OpenHandsならありとあらゆるLLMエンドポイントに対応しており、vllmやLiteLLMなどのLocalLLMも使用することができる。

追記:
と思ったのだが、Codex CLIはOpenAI以外のLLMにも対応しているらしい。Ollamaなど。
https://github.com/openai/codex

vArvAr

実際に動かすところまでやってみた。

https://docs.all-hands.dev/usage/how-to/cli-mode

公式ドキュメントには pip でインストールせよと書いているが、pipでいけるならuvでもいけるだろうと思ってやってみた。

結果、普通にuvでセットアップできた。

$ uv venv --python 3.13
$ uv init
$ uv add openhands-ai
vArvAr

さて本題のLocal LLMの設定だが、自動的に生成される.openhands/settings.jsonを弄ることで呼び出すことができた。

まずは設定ファイルのディレクトリを生成するために一旦起動。

$ uv run openhands

対話的にモデル設定をするように誘導されるので、ここは適当にエンターを押していき、

  1. "Save new settings?"で"yes"を選択する。
  2. "Do you wish to continue?"で"Yes, proceed"を選択する。

途中でAPI KEYも聞かれるが、まずはでたらめな値を入力してOK。すると、

OS: Ubuntu24.04 LTS
の場合、/home/ubuntu/.openhands/settings.jsonファイルが生成される。

settings.json
{"language":null,"agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":null,"llm_model":"anthropic/claude-sonnet-4-20250514","llm_api_key":"hoge","llm_base_url":null,"remote_runtime_resource_factor":null,"secrets_store":{"provider_tokens":{}},"enable_default_condenser":true,"enable_sound_notifications":false,"enable_proactive_conversation_starters":true,"user_consents_to_analytics":null,"sandbox_base_container_image":null,"sandbox_runtime_container_image":null,"mcp_config":null,"search_api_key":null,"email":null,"email_verified":null}

ここまで来たら一旦 Ctrl + C で中断する。

同じディレクトリ内にconfig.tomlファイルも生成されるが、こちらの設定を変えてもエラーになってしまい、モデル呼び出し先を変更できなかったので一旦は空にした。

変更すべき箇所は、下記のkey3つ。

  • "llm_model"
  • "llm_api_key"
  • "llm_base_url"

注意点として、"llm_model"にはprefixを付ける必要がある。

https://docs.all-hands.dev/usage/llms/local-llms

今回私はvllmを使って同じサーバ内にOpenAI API互換のhttp推論サーバとしてLocal LLM(Qwen3)をホスティングしている。

OpenHands CLIでOpenAI API互換のエンドポイントを使う場合は、モデル名のprefixにopenai/を付与する必要がある(たとえそれがOpenAIのモデルでなくても)。

よって、設定値は下記のようになる(一例)。

  • "llm_model" : "openai/my-Qwen3" # 設定しているモデル名にprefixを付けたもの
  • "llm_api_key" : "xxxxxxx" # vllmの推論サーバ起動時に--api-keyで設定した値(必須)
  • "llm_base_url" : http://localhost:8001/v1 # vllmのエンドポイントURL
settings.json
{"language":null,"agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":null,"llm_model":"<モデル名>","llm_api_key":"<設定したAPI KEY>","llm_base_url":"<設定したエンドポイントURL>","remote_runtime_resource_factor":null,"secrets_store":{"provider_tokens":{}},"enable_default_condenser":true,"enable_sound_notifications":false,"enable_proactive_conversation_starters":true,"user_consents_to_analytics":null,"sandbox_base_container_image":null,"sandbox_runtime_container_image":null,"mcp_config":null,"search_api_key":null,"email":null,"email_verified":null}
vArvAr

ここまで設定したところで、起動してみた。

$ uv run openhands
     ___                    _   _                 _
    /  _ \ _ __   ___ _ __ | | | | __ _ _ __   __| |___
    | | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
    | |_| | |_) |  __/ | | |  _  | (_| | | | | (_| \__ \
    \___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
          |_|
    
OpenHands CLI v0.44.0

Let's start building!

What do you want to build? Type /help for help

> あなたは何ができますか?

Agent running... (Press Ctrl-P to pause)

私はさまざまなタスクをサポートできます。たとえば:

- ファイルの作成・編集・削除
- プログラムの実行やサーバーの起動
- コードのバグ修正やリファクタリング
- テストの実行やテストコードの追加
- 必要なパッケージや依存関係のインストール
- ディレクトリやファイルの内容の確認
- Git操作(コミット、プルリクエスト作成など)
- Webページの自動操作や表示確認

具体的にやりたいことを教えていただければ、最適な方法でサポートします。どんなことをお手伝いしましょうか?

Agent is waiting for your input...

>

無事Local LLMを呼び出すことができた。

Ctrl + C で終了してみる。

Terminate session?

> Yes, proceed
  No, dismiss
 
Closing current conversation...

┌───────────────────────────────────────────────────────────| Usage Metrics |───────────────────────────────────────────────────────────┐
│   Total Cost (USD):    $0.013854                                                                                                      │
│                                                                                                                                       │
│   Total Input Tokens:  6,247                                                                                                          │
│      Cache Hits:       0                                                                                                              │
│      Cache Writes:     0                                                                                                              │
│   Total Output Tokens: 170                                                                                                            │
│                                                                                                                                       │
│   Total Tokens:        6,417                                                                                                          │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Conversation duration: 0h 0m 39s
vArvAr

このまま$HOME/直下の.openhands/ディレクトリ内の設定ファイルを見に行くままだと、設定値がグローバルに全プロジェクト共通になってしまう。

現実的な運用を考えると、プロジェクトごとにモデル設定を変えたくなるはず。

そこでプロジェクトルートに.openhands/ディレクトリを作成して、それを読み込むようにできないか調査してみた。

結論として、それは現時点ではできなさそうなことが分かった。

試したこと

  • 環境変数OPENHANDS_HOME=$PWD/.openhandsを設定
    • 機能せず(起動するとモデル設定から)
  • 環境変数LLM_MODEL, LLM_API_KEY, LLM_BASE_URLをセット
    • 機能せず(起動するとモデル設定から)
    • 前者2オプションはDockerでコンテナとして実行するときには使えると公式ドキュメントには書いてあるのだが、LLM_BASE_URLは見当たらないので無いのかもしれない。
  • 起動オプション--config-file <config.tomlのパス>を付与

まだそのオプションは提供されていないようだった。

開発中らしき痕跡も発見。今後に期待。
https://github.com/All-Hands-AI/OpenHands/issues/9077

vArvAr

Streamlitで簡単なじゃんけんアプリを作ってもらうことにした。

> pythonのstreamlitでじゃんけんをするアプリを作ってほしいです。

┌──────────────────────────────────────────────────────────────| Command |──────────────────────────────────────────────────────────────┐
│$                                                                                                                                      │
│ls -l                                                                                                                                  │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

まずは、作業ディレクトリの内容を確認し、既存のファイルやStreamlitのセットアップ状況を把握します。  
その後、じゃんけんアプリの作成に進みます。

`main.py`は現在、シンプルなPythonスクリプトになっています。  
ここにStreamlitを使ったじゃんけんアプリを実装します。

既存の内容をStreamlitアプリに置き換え、ユーザーがボタンで手を選び、結果が表示されるようにします。

┌─────────────────────────────────────────────────────────────| File Edit |─────────────────────────────────────────────────────────────┐
│(no changes detected. Please make sure your edits change the content of the existing file.)                                            │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

お待たせしました。  
`old_str`に現在の`main.py`の内容を正確に指定し、`new_str`にStreamlitのじゃんけんアプリのコードを記述します。

old_str:
```
def main():
    print("Hello from test-openhands!")


if __name__ == "__main__":
    main()
```

new_str:
```
import streamlit as st
import random

st.title("じゃんけんアプリ")

hands = ["グー", "チョキ", "パー"]
user_hand = st.radio("あなたの手を選んでください", hands)
if st.button("勝負!"):
    cpu_hand = random.choice(hands)
    st.write(f"あなた: {user_hand}")
    st.write(f"コンピュータ: {cpu_hand}")

    if user_hand == cpu_hand:
        st.success("あいこです!")
    elif (user_hand == "グー" and cpu_hand == "チョキ") or \
         (user_hand == "チョキ" and cpu_hand == "パー") or \
         (user_hand == "パー" and cpu_hand == "グー"):
        st.success("あなたの勝ち!")
    else:
        st.error("あなたの負け!")
```

これで置き換えます。

┌─────────────────────────────────────────────────────────────| File Edit |─────────────────────────────────────────────────────────────┐
│(no changes detected. Please make sure your edits change the content of the existing file.)                                            │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

失礼しました。`old_str``new_str`を明示的に指定します。

┌─────────────────────────────────────────────────────────────| File Edit |─────────────────────────────────────────────────────────────┐
│(no changes detected. Please make sure your edits change the content of the existing file.)                                            │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

繰り返し失礼しました。  
`old_str``new_str`を明示的に指定して、ファイル全体を置き換えます。

┌─────────────────────────────────────────────────────────────| File Edit |─────────────────────────────────────────────────────────────┐
│(no changes detected. Please make sure your edits change the content of the existing file.)                                            │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

滑り出しは良かったものの、既存コードを新規コードに上書きするツール使用がうまくいっておらず、何度も繰り返している模様。

LLMをGPT-4.1に変えても同様だった。

こちらの指示が悪いのかもしれないので、割り込んで指示を与えてみた。

Agent paused... (Enter /resume to continue)

> あなたは繰り返し失敗しています。
現在の環境が"uv"というツールで作られたPython仮想環境であることを認識すべきです。
CPythonの実行が必要なすべてのコマンドは`uv run`をprefixにして実行すべきです。
例えば`uv run streamlit run main.py`のようにします。
さて、引き続きじゃんけんアプリの作成を続けてください。
あなたが混乱しないように、まずはtask.mdというファイルを作り、どんな順番でアプリを作成していくかを計画して、随時このタスクファイルを見ながら作業を進めてください。

Agent running... (Press Ctrl-P to pause)

ご指摘ありがとうございます。  
まずは、`task.md`ファイルを作成し、じゃんけんアプリ作成の計画を明記します。その後、このファイルを参照しながら作業を進めます。

計画ファイル(task.md)の作成が完了しました。  
次は計画2「Streamlitでじゃんけんアプリのコードをmain.pyに実装する」に進みます。  
既存のmain.pyは短いので、内容をStreamlitアプリに置き換えます。

タスクファイルは問題なく作成された。Clineのようにエディタ上で差分が表示されることもなくヌルっと生成されていた。

task.md

# じゃんけんアプリ作成計画

## 手順
1. 既存ファイル(main.py等)の内容を確認する
2. Streamlitでじゃんけんアプリのコードをmain.pyに実装する
3. 必要に応じて依存パッケージ(streamlit)のインストール状況を確認する
4. `uv run streamlit run main.py`でアプリを起動し、動作確認する
5. 必要に応じてUIやロジックを修正する
6. 完了したらtask.mdに進捗を記録する

## メモ
- uv環境下では、Python実行コマンドは`uv run`をprefixに付ける
- Streamlitアプリはmain.pyで管理する

しかしここからまた無限ループ発生。何が悪いのだろうか……。

失礼しました。  
`str_replace`ではなく、`main.py`の内容を一度確認した上で、`insert`コマンドで全置換を行うのが適切です。  
まず、`main.py`の全内容を消去し、次にStreamlitアプリのコードを挿入します。

まず、`main.py`の全内容を空にします。

┌─────────────────────────────────────────────────────────────| File Edit |─────────────────────────────────────────────────────────────┐
│(no changes detected. Please make sure your edits change the content of the existing file.)                                            │
│                                                                                                                                       │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

繰り返し失礼しました。  
`str_replace`ではなく、`main.py`の内容を直接上書きするには`insert`コマンドではなく、`create`コマンドも使えません。  
この場合、`main.py`の内容を全て新しいStreamlitアプリの内容で上書きするには、`old_str`に現在の全内容を指定し、`new_str`に新しい内容を指定する必要があります。

トークンの無駄なので、一旦終了。

Agent paused... (Enter /resume to continue)

Terminate session?

> Yes, proceed
  No, dismiss
 
Closing current conversation...

┌───────────────────────────────────────────────────────────| Usage Metrics |───────────────────────────────────────────────────────────┐
│   Total Cost (USD):    $0.276242                                                                                                      │
│                                                                                                                                       │
│   Total Input Tokens:  254,641                                                                                                        │
│      Cache Hits:       206,080                                                                                                        │
│      Cache Writes:     0                                                                                                              │
│   Total Output Tokens: 9,510                                                                                                          │
│                                                                                                                                       │
│   Total Tokens:        264,151                                                                                                        │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Conversation duration: 0h 5m 9s