初めに
最近、ローカルモデルで agent を回したいと思っていた。
API 利用は楽だが、コスト、遅延、そして「そもそもデータを持ち出したくない案件」がある以上、ローカルに立てられる構成をちゃんと試しておきたかった。
そのタイミングで、DeepSeek が open-source の agent harness「DeepSeek Harness」(コマンド名は dsh)を公開した。
"everything is a plugin" のアーキテクチャで、Cordis を基盤にしているという、少し変わった設計だ。
今回は、インストール → Web UI 起動 → ローカルモデル(OpenAI 互換エンドポイント)の設定まで、実際に進めてみた。
やったことと設定の落とし穴を、短くまとめる。
対象読者は、ローカルモデルや OpenAI 互換ゲートウェイを agent のバックボーンにしたいエンジニアです。
単なる紹介ではなく、設定ファイルまで書いていくので、そのまま自分の環境に落とし込めるはずです。
なお、dsh は現在 developer preview 段階で、公式 README にも COMPATIBILITY-BREAKING CHANGES がある と明記されています。
その点は踏まえて読んでください。
結論
先に結論を書くと、次の5つです。
- dsh は DeepSeek AI が開発したオープンソースの agent harness で、プラグインの積層(Cordis)が本体の設計。Web UI、headless 実行、Python SDK は、すべて同じ core を起動する入口にすぎない
-
npx @deepseek-ai/dsh web一条で Web UI が起動する(既定はhttp://127.0.0.1:3080) - DeepSeek API を使うだけなら、Settings → Models でキーを入力して保存するだけ。キーは write-only で、設定ファイルには秘密そのものではなく参照だけが残る
- ローカルモデル / OpenAI 互換サーバは「custom provider」として普通に扱える。UI の Add a custom provider か
settings.yamlのllm-pi-aiセクションに route を書くだけ - 設定変更はサーバ再起動なしで次回リクエストから有効(hot reload)になる
「ローカルモデル対応」を別物として売らず、provider 設定として普通に扱える設計は、かなり好感が持てた。
1. DeepSeek Harness とは
公式 README の定義をそのまま引用すると、dsh は
DeepSeek AI が開発した open-source agent harness。「everything is a plugin」のアーキテクチャを使用し、Cordis によって動かされる。
というものです。Cordis の設計思想は 論文 "A Programming Paradigm for Spatiotemporal Composability" に書かれています。
「everything is a plugin」が具体的な形になっているのが、リポジトリの packages です。
npm scope は @deepseek-ai/dsh-* で、役割ごとにグループ化されています。
-
core/— セッション、プロンプト、ツール、agent サービス、実行ループの本体 -
llm/— LLM 抽象サービスと provider アダプタ(後述の鍵) -
shell/terminal/code-runtime/— コマンド実行・PTY・コード実行 -
fs/lsp/subagent/jobs/workflow/— ファイル操作、LSP、委譲、バックグラウンドジョブ、workflow -
sandbox/— プロセス隔離(bwrap / Landlock / Seatbelt) -
session/settings/credentials/— 永続化、設定、認証情報の管理 -
sdk/acp/— 外部から呼ぶための SDK / プロトコル
つまり「agent の機能」が全部個別プラグインで、積層の仕方(profile)で構成が変わる、という設計です。
ユーザーが触る入口は、公式に次の3つあります。
-
Web UI —
dsh web。ブラウザから会話・承認・設定を行う -
headless —
dsh --profile headless "タスク"。一回きりのセッションを走らせて最終回答を出力して終了 -
Python SDK —
deepseek-harness-sdkを pip install して自前のプログラムから呼ぶ
動作として、agent は workspace のファイルを読み書きし、コマンドを実行し、仕事を委譲し、計画(plan)を維持できます。
承認が必要な操作には、Web UI がアクティブな権限ポリシーに従って確認を入れます。
ライセンスは MIT。フィードバックは GitHub Discussions や Discord が公式窓口です。
2. インストールと初回起動
npm から(一番簡単)
Node.js をインストールしてから、公式 README が勧めるのはこれです。
npx @deepseek-ai/dsh web
これだけで Web UI が起動し、URL が表示されます。
既定の提供先は http://127.0.0.1:3080 です(ローカルホストのみ。ここは 6 節でまた出ます)。
ソースから
リポジトリを clone して build する流れも公式に載っています。
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
dsh コマンドは profile 起動子
CLI の README にあるとおり、dsh は profile(プラグインバンドルの積層)を起動するローンチャー です。
| コマンド | 用途 |
|---|---|
dsh --profile <name> |
$DSH_HOME/profiles/<name> 以下から name 指定の profile を起動 |
dsh --profile headless "job" |
一回きりの永続セッションを走らせ、最終回答を出力して終了 |
dsh web |
--profile web のエイリアス |
dsh plugin --profile <name> <pnpm 引数> |
profile ディレクトリでプラグイン(pnpm)を管理 |
web と headless profile は初回利用時に同梱テンプレートから自動初期化されます。
これ以外の profile は dsh plugin で作る必要があります。
また、起動したディレクトリが既定の workspace ルートになります。
積まれた設定の確認には、起動せずにツリーを dump できます。
dsh --dump-config # 現在の積層ツリー
dsh --dump-default-config # ユーザー層なしの既定ツリー
3. Web UI で DeepSeek API を使う
サーバが起動している状態で、まずは公式 DeepSeek API を繋いでみます。
-
Settings → Models を開く。DeepSeek のカードに API キー1つを入力して保存
-
Choose workspace で、
dshを起動したプロジェクトディレクトリを追加して選択 -
セッションを開始して、例えば
Summarize this repository and identify its main packages.
を投げる
これで、ファイルの読み書き・コマンド実行・委譲・plan 維持まで一通り触れます。
承認が必要な操作には Web UI が確認を挟みます。
キーの扱い(write-only)
ここが丁寧だなと感じたポイントです。
API キーは write-only として扱われます。保存後、画面が受け取るのはマスク済みの descriptor だけで、文字列そのものは返ってきません。
キー本体は $DSH_HOME/.credentials.yaml に保管され、settings 側にはその credential 参照だけが残ります。
$DSH_HOME は環境変数 DSH_HOME、未設定なら ~/.dsh です。
モデルピッカー側では、設定済みの provider が一覧に出ます。
モデルを選ぶと、それが新規セッションの既定モデルにもなります(すでにリクエストを送ったセッションは、自分のログに記録されたモデルを維持します)。
4. ローカルモデルを config する
ここからが本題です。
手元では vLLM をローカルで立てて http://127.0.0.1:8000/v1 に OpenAI 互換 API を公開しています(モデル ID はエンドポイントの GET /models が返す名前を使います。私の環境では claude-opus-5 というエイリアスになっていました)。
dsh では、こういう自前エンドポイントは custom provider として扱います。
公式 providers ガイドの表現を借りると、対象は「会社のゲートウェイ、self-hosted サーバ、インストール済み catalog に載っていない provider」です。
4.1 UI での追加(Add a custom provider)
Models ページの Add a custom provider から、次の項目を入力します。
- Provider ID — 小文字。永続です(リクエスト、保存済みセッション、既定モデル、credential 参照のすべてがこれを使う)。リネームしたくなったら「新規追加して旧方を削除」が公式の答え
- Display name
-
Base URL — 例:
http://127.0.0.1:8000/v1 -
API protocol — 使う wire プロトコル(
openai-completions/openai-responsesなど) - credential — API キー
- Model — 最低1つ以上
Fetch available models を押すと、フォームの Base URL と credential で GET /models を叩いて候補をくれます(OpenAI 互換の GET /models 形状を使う gateway / self-hosted サーバ向け)。
候補を選んで draft を更新し、保存するまで provider はディスクに書き込まれません。
この UI 操作が、裏では何を生成しているのか——その正体が次節の settings.yaml です。
4.2 settings.yaml での追加(llm-pi-ai route)
dsh の LLM 周りには2つのアダプタプラグインが積まれています。
-
dsh-llm-deepseek— DeepSeek 公式エンドポイント専用の直接アダプタ -
dsh-llm-pi-ai— 複数 provider 対応の汎用アダプタ(pi-ai ベース)
ローカルモデルを含む custom provider は、後者の llm-pi-ai セクションに route を1本追加する という形になります。
公式ドキュメントの例(ゲートウェイ向け)はこういう形です。
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY
api: openai-completions
baseURL: https://gateway.example/v1
models:
- id: legacy-chat
- id: vision-preview
input: [text, image]
私の環境(vLLM)に落とすと、こうなりました。
# ~/.dsh/settings.yaml
llm-pi-ai:
providers:
vllm-local:
displayName: vLLM Local
apiKeyEnv: VLLM_LOCAL_API_KEY
api: openai-responses
baseURL: http://127.0.0.1:8000/v1
models:
- id: claude-opus-5
name: claude-opus-5 (local)
contextWindow: 114688
maxTokens: 16384
reasoningEfforts:
xhigh: xhigh
主要フィールドの意味を、公式 README(dsh-llm-pi-ai)に基づいて整理します。
-
apiKeyEnv— 環境変数名への credential 参照。秘密は settings に書かない(4.3 参照) -
api— wire プロトコル。catalog に載っていない route は、このフィールドで「どのプロトコルを話すか」を宣言する。supportedProtocols()が許すのはopenai-completions/openai-responses/anthropic-messagesの3つ(キー・エンドポイント・ヘッダで完全に表現できるプロトコルのみ、という設計) -
baseURL— その route の全モデルのエンドポイント。/v1付きの prefix として扱われる -
models— route の catalog を置換するリスト(追加・拡張ではない)。idのみで書けば残りは route / catalog の既定値 -
contextWindow/maxTokens— モデルの能力。設定されたmaxTokensはリクエストの既定上限にもなる(未設定なら route 既定のdefaultContextWindow: 262144/defaultMaxTokens: 32768) -
input— 入力モダリティ。手書きしたモデルは既定でテキスト専用になります。画像を受け取りたいならinput: [text, image]を明示する -
reasoningEfforts— レベル名 → wire 上の表記のマップ。4.4 で詳述
models を書き忘れた route は dormant(起動しない) 状態です。
「catalog provider に手動モデルを追加したい」場合は modelOverrides を使うと、catalog の残りはそのまま維持したまま上書きできます(catalog の Anthropic/OpenAI 向けに有用、という公式の位置づけ)。
4.3 credential の仕組み
apiKeyEnv に書くのは環境変数名であり、秘密そのものではないです。
- UI でキーを保存すると、credential は
$DSH_HOME/.credentials.yamlに入り、settings には参照だけが残る(3 節の write-only と同じ経路) -
settings.yamlだけで route を足した場合も、同じルールが機能する。apiKeyEnv名の変数を credential store に置けばよい
私の環境では、この2ファイルの組み合わせになっていました。
# ~/.dsh/settings.yaml(抜粋:秘密は含まない)
llm-pi-ai:
providers:
vllm-local:
apiKeyEnv: VLLM_LOCAL_API_KEY
# ...
# ~/.dsh/.credentials.yaml
VLLM_LOCAL_API_KEY: sk-your-key-here
credential 参照が解決できないと、リクエストは MISSING_CREDENTIAL で失敗します。
つまり「キーがどこに書かれているのか」を settings から追わなくていい、というのが設計思想です。
4.4 reasoningEfforts(思考レベル)
reasoning 系モデルを使うなら、ここも押さえておきたい部分です。
pi-ai 系の provider では、reasoningEfforts のキー(レベル名)が dsh が提示する選択肢になり、値は API に実際に送られる wire 上の表記になります。
models:
- id: some-reasoning-model
reasoningEfforts:
off: "disabled"
high: "high"
xhigh: "xhigh"
- レベルは
off/minimal/low/medium/high/xhigh/maxの内から選べる - 例:
max: "ultra"と書くと、UI のmaxは wire 上でultraとして送られる - 手書きしたモデルはこのマップを明示的に書かなければ reasoning を送らない(既定で思考が効く、とはしない)
route 単位では reasoning でデフォルトレベルを置けます(off 含む)。ここは「その route の全リクエストのデフォルト」です。
4.5 実測:headless でワンショット実行
Web UI 以外で確認したい場合、headless プロファイルが便利です。
dsh --profile headless "请只回复两个字:收到"
私の環境(vLLM + claude-opus-5)では、この通り
收到
が返ってきました。
セッションログ($DSH_HOME/sessions/ 配下)を確認すると、実際に送られたリクエストは provider: vllm-local / model: claude-opus-5 / reasoningEffort: xhigh / maxTokens: 16384 になっていて、settings.yaml に書いた値がそのまま wire に落ちていました。
設定変更後の挙動確認は、この「headless ワンショット + セッションログ」が非常に早いです。
再起動しなくても次回リクエストから反映される、という hot reload の説明もこれで実証できます。
5. その他の実行経路
Python SDK
deepseek-harness-sdk は pip で入ります(Python 3.10 以上。Linux x64/arm64、macOS 14 以上の arm64 向けで、runtime が同梱されているためシステム Node.js は不要、と公式が明記)。
python -m pip install deepseek-harness-sdk
環境変数でエンドポイントとキーを指します。
export DEEPSEEK_API_KEY=sk-your-key-here
# 公式ドキュメントの例は、OpenAI 互換プロキシ向けにこう指している
export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
ローカルエンドポイントで回すための DEEPSEEK_BASE_URL が公式 doc の例に出ている点は、ローカル利用が想定された第一級シナリオであることを示しています。
最小構成の呼び出しはこんな感じです。
from deepseek_harness_sdk import DeepSeekHarness
harness = DeepSeekHarness(code="Hello from the Python SDK.")
session = harness.start_task("hello", content=harness.content_text("Hello from the Python SDK."))
print(session.session_id)
harness.destroy()
ツール呼び出しを許可しないなら、権限を danger-full-access にしない(既定の deny-all)ままにしておく、という注意書きも公式に載っています。
6. 注意点と失敗例
実際に進めてみて、つまずきやすいポイントをまとめます。
-
Web UI は既定で
127.0.0.1:3080のみ。 LAN の別マシンから見るには、ポート転送か reverse proxy が必要です - Provider ID は永続。 後からリネームしたければ「新規 provider を追加して旧方を削除」。セッション・credential・既定モデルの参照が ID に紐づいているため
-
手書き route は
api+baseURL+modelsが必須。 書き忘れた route は dormant(起動しない)のままです -
modelsは置換。 「catalog に1つ足したい」だけならmodelOverrides。リストを書き換えると他は消えます -
Fetch available modelsが 401 になる — キーが解決できない、またはエンドポイントがGET /modelsを持たない、のどちらか。キーは Models ページから保存すると descriptor がpresentになるので確認しやすい。GET /modelsがないサーバは手動入力 -
UNKNOWN_MODEL— その provider に設定済みのモデルを選べていない。未登録のモデルはmodels(またはmodelOverrides)に追加する -
画像を添付して拒否される — 手書きしたモデルは既定でテキスト専用。
input: [text, image]を model / route(defaultInput)/ catalog provider(modelOverrides)のいずれかに書く -
developer preview。 公式 README の「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」は本物。安定性を求める用途ではバージョンを固定し、アップグレード前に
--dump-configで積層を確認しておくのが無難
まとめ
DeepSeek Harness は、LLM を「交換可能なプラグイン」として扱って設計された agent harness でした。
最後に、私の感想を3つに絞って残します。
-
「everything is a plugin」が実装レベルで本物だった点。 agent の機能は
@deepseek-ai/dsh-*の積層で構成され、dshはただの profile 起動子。 Web UI と headless が同じ core を共有する設計は、CLI → Web の二重実装のなやみを最初から消しています -
credential が write-only なのは地味に高評価。 キーは
$DSH_HOME/.credentials.yamlにしかおらず、settings には参照だけ。 設定ファイルをコピペしても秘密が漏れない、という方向性は、agent をチームで使う段階で効いてくる設計です -
ローカルモデルが「特効機能」でなく provider 設定である点。
GET /modelsを叩いてモデルを取り込む UI、llm-pi-aiの route、reasoningEffortsの wire 名マッピング、hot reload—— 自前サーバを繋ぐための部品が全部揃っていて、かつ公式ドキュメントがDEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1を例に出している
今回の記事で紹介した範囲(npm 起動、DeepSeek API、ローカルモデル設定)は、公式ドキュメントに忠実に進めれば数分で通ります。
ただし developer preview であることは忘れないでください。
プラグイン積層という設計がどこまで安定するのか、Discord や Discussions で動きを見ていきたいと思います。
あなたのローカルモデル環境でどう動くか、気になった人は npx @deepseek-ai/dsh web から試してみてください。