Linux デスクトップ元年

※ Googlebook OS ではない

パソコン3台あります。mac 1台はともかく、Windows 実機が2台あっても持て余し気味だったので、何らか理由をつけて Linux デスクトップの実機にしたい気持ちが高まりました。思案して、

  • Windows パソコンで 3D ゲームとかやらない
  • Windows 11 は ROCm サポートが貧弱。PyTorch なアプリ動かせない。Linux にして Radeon AI PRO R9700 を使い倒したい
  • Windows 11 では Docker が遅い。やっぱ Linux 実機にして高速にしたい
  • 使ってるアプリの大多数が macOS/Windows/Linux で動作する (Electron製が多い)

納得して自作デスクトップPCの Windows を消し飛ばしました。

Ubuntu を選んだ

イキのいいディストリが揃ってますが、AMD GPU ドライバと ROCm の最新版を利用できるディストリに絞ると、大多数が選択肢から消えてメジャーどころだけ残ります。そこから、、、

  • RedHat 系たとえば Fedora を選ぶ理由が特になかった
    • RedHat系は仕事でも私用でも使うことがないので...
  • Arch 系は小難しそうで、今回モチベーションはないので選ぶ理由が特になかった
    • Omarchy は 試用してみたけど、合う人には合いそう。物事の考えかたは面白い
  • Debian 系で、素直に Ubuntu にしとくか
    • 昔は一時期 Kubuntu, Xubuntu で過ごしたけど結局 Ubuntu に戻った
    • 仕事でも CI 環境の OS が Ubuntu だったり、 FROM:ubuntu なコンテナイメージを触れることは多いし

で Ubuntu 26.04 を選びました。

AMD GPU ドライバ, ROCm をインストールする

AMD GPU ボードを使っていて、ローカルLLMやるなら、AMD公式GPUドライバパッケージ、公式ROCmパッケージをインストールする。

Ubuntu ディストリのバージョン、AMD GPU ドライババージョン、AMD ROCm バージョンは、サポートされてるバージョンがガチッと決められてるので、そのつもりで使う(Ubuntu の LTS 以外のバージョンはサポート外っぽい)。

なお AMD GPU ドライバインストール時に、 Ubuntu の Secure Boot と amdgpu-dkms の組み合わせで必要になる「MOK(Machine Owner Key)の登録」が要求される。良きように進めてください。

AMD GPU ドライバ、initramfs、ストレージ暗号化が絡まって遭難した

Windows なら TPM を利用して BitLocker。macOS なら FileVault。私物といえどストレージ暗号化やっとくかー、Ubuntu でやるには、と、インストール時に TPM によるストレージ暗号化を試みるも、どうやら技術的に困難な組み合わせの問題があって私の手元のパソコンでは無理らしい。

しゃーないのでパスワードによるストレージ暗号化「Encrypt with a passphrase」で我慢しました。

さて Ubuntu インストールを終えて AMD GPU ドライバインストールした後に面倒事に巻き込まれました。前提として Ubuntu 26.04 では initramfs の管理に dracut を用いてます。電源ONしたときブートローダーやら何やかやで、パスワードによるストレージ暗号化を通るためのパスワード入力画面があって、これは dracut が良きようにしてます。ところで AMD GPU ドライバの amdgpu-dkms パッケージは initramfs-tools パッケージに直接依存していて、dracut を削除して initramfs-tools に置き換える形で AMD GPU ドライバイインストールが進みました。initramfs-tools も、電源ON時のパスワードによるストレージ暗号化の解除画面を出してハンドリングできますが、それは追加で cryptsetup-initramfs をインストールしている場合に限ります。という全体像を私は把握しておらず cryptsetup-initramfs を当然ながら入れてない。そして起動不能になりました。

どうしましょうで、

  1. USB メモリで Ubuntu 再インストール。その後に AMD GPU ドライバインストールして、cryptsetup-initramfs もインストールしてから、再起動やる。時間かかるが厄介事はない。
  2. 出たとこ勝負の対策前進でトラブルシュートしてリカバリを目指す。時間は見通せないし厄介事しなかい。

トイレで気張りながら思案して、週末だし道楽だと思って(2)でやってみるか!!した。ひとまずコピペもできやしないので、スマホカメラで画面撮影して、スマホから「チャッピーこれ何なのー」で「このコマンド叩いて結果を知らせてくれ」「あれを叩け」「これを叩け」と指示されて私はミートプロキシして、ひととおり情報収集したら、LiveUSB で Try Ubuntu に入って、1password を入れて Firefox で ChatGPT にログインして、これで Terminal は自在に叩けるしコピペできるので、ふたたび情報収集しながら、暗号化されてる SSD を cryptsetup でパスワード入力して開いてマウントして chroot して情報収集して apt install cryptsetup-initramfs して情報収集して、残ってる怪しげな warn を調べて解消して、なんとか生還しました。

そういう次第なので、パスワードによるストレージ暗号化して、AMD GPU ドライバをインストールしたときに dracut がアンインストールされて initramfs-tools が入ったら、再起動前に sudo apt install cryptsetup-initramfs も忘れずにやりましょう。

amd-smi で、ワット上限は設定できる。ファン制御は上書き不可

Windows では AMD Software で「最大消費電力を何ワットにする」「何度になったらファン回転数いくつにする」を設定できたので、Linux でもやれるのか調べました。結論 AMD Radeon AI PRO R9700 は amd-smi コマンドで、ワット上限は設定できて、ファン制御は上書きできないっぽい。

もしかすると、ちゃんと調べて叩けば何かできるかもしれない。そのうち気が向いたらやるかな。

アプリを入れる

昔は Ansible をローカルに向けてやってたけど、なんかもう面倒臭いのと、そんな繊細にセットアップしなくても足りており、いまのとこ数枚のシェルスクリプトで茶を濁してます。私がインストールしてるアプリは以下に列挙しました。

apt, snap, mise でドカドカ入れる

apt。GUIフロントエンド Synaptic で探してインストール/アンインストールします。

snap。もっぱら proprietary な GUI アプリの管理に使ってます。

mise。「apt, snap で配布されてないか、配布されてるが古い、誰か知らんやつが配布してる」「依存関係でガチャガチャしない」「GitHub repo の Release で、Linux 向け tar.gz が配布されてる」あたりを満たすアプリの管理に使います。

deb パッケージだけ配布されてるアプリを入れる

いくつか配布形態があります。

例1) 独自ドメインで配布してる。たとえば OpenAI ChatGPT デスクトップアプリ。頑張って wget して apt install します。

例2) GitHub repo の Release で配布してる。たとえば OpenLogi。GitHub repo の Release なら mise で扱えそうな気がするのですが、 deb パッケージは扱えないらしいです。

Release から latest で取れたらいいので gh で取ってきて apt install します。

gh release download \
  -R AprilNEA/OpenLogi \
  --pattern 'openlogi-*-linux-amd64.deb' \
  --dir ./

sudo apt install ./openlogi-*-linux-amd64.deb

以上いずれも初回の apt install で source がセットアップされるなら、以降は apt update & apt dist-upgrade でアプデされていきます。そこまで気が利いてないアプリは、しかたないので

curl https://example.com/install | bash で入れる

わりと最終手段。なるべく apt, snap, mise で入れて、インストール/アップデート/アンインストールを綺麗に管理したいですが、そうもいかんこともあるし、しかたない。

PyTorch 利用アプリを入れる

PyTorch は PyTorch 公式 index では配信しておらず AMD の index らしい。

私は uv tool install ... で使いたい派なので、たとえばこのようにします。

sudo apt install python3-dev
uv tool install --force \
  --index https://stable.repo.amd.com/rocm/whl-next/ \
  --with 'rocm[libraries,device-gfx1201]==10.0.0' \
  --with 'torch[device-gfx1201]==2.13.0+rocm10.0.0' \
  --with 'torchvision[device-gfx1201]==0.28.0+rocm10.0.0' \
  'yomitoku==0.15.0'

使うときはこのように。

yomitoku ./9784022732064.pdf \
--outdir ./ --format md --device cuda --ignore_line_break --ignore_meta --ignore_ruby;
printf '%s\n' ./_9784022732064_p*.md | sort -V | xargs cat > ./9784022732064.md

ROCm は手順どおりインストールすると基本的なパッケージだけ入ります。PyTorch で ROCm の使用範囲が広いアプリは、ROCm の追加のライブラリが必要になる場合があります。Synaptic で探して入れたら動くようになります。

uv コマンドでのセットアップ手順を自力で調べていくのは心が折れたので、私はアプリを使いたいのであって開発したいわけではないと割り切って、コーディングエージェントにひたすらセットアップをやらせてます。

アプリランチャー Vicinae を入れる

macOS の Raycast 便利です。Windows ならスタートメニューが簡易なアプリランチャーとして機能します。機能的にアレくらいのものとして、Ubuntu の Gnome には Windows キー押下でパッと出る簡易なアプリランチャーが備わってます。apt, snap で入れた GUI アプリはインデックスされますが、 mise で入れた GUI アプリは射程外のため出てきません。

この厄介の対処方法はいくつか方法はあるようですが、今回アプリランチャー Vicinae を使います。

sudo apt install -y libopengl0
curl -fsSL https://vicinae.com/install | bash
systemctl --user enable --now vicinae.service

インストールできたら、Ubuntuの「設定」-> キーボード -> キーボードショートカット -> ショートカットの表示とカスタマイズ -> カスタムショートカット -> ショートカット追加で、

  • 名前 すきにしろ
  • コマンド /usr/local/bin/vicinae toggle
  • ショートカット すきにしろ

ここまで設定してログアウト/ログインすると、↑のショートカットに反応して Raycast っぽいアレが出てくるようになります。

Vicinae は素のままでは mise でインストールした GUI アプリをインデックスしません。が、いくつか約束事に乗っかることで良きようになります。その約束事に乗せるスクリプト mise-desktop-sync を実行すると良きようになります。何してるかはコードを眺めてください。

mise の postinstall hook に入れると完璧度が高まります。たとえばこのように記述します。

[hooks]
postinstall = [
  { run = "既存の処理", shell = "bash -c" },
  { run = "$HOME/.local/bin/mise-desktop-sync", shell = "bash -c" },
]

mise の hook の詳細は以下。

Vicinae の GNOME 拡張も入れると、GNOME との何かができるようになるらしいです(あまり活用しておらず、よくわかってない)。インストールには GUI アプリ gnome-shell-extension-manager の検索窓で探して入れるのが面倒がなくてよいです。 https://docs.vicinae.com/install/linux ではコマンドラインから入れられそうな風に書いてあるけど、記述が古いのか動作しませんでした。

sudo apt install -y gnome-shell-extension-manager

Docker Engine を入れる

Ubuntu の docker.io でよい。docker 配布の docker-ce でもよい。情報が集めやすいのは「docker 配布の docker-ce」かもしれない。

どちらで入れるにしても post install 作業をやるとよい。特に、普段使いの一般ユーザーに権限付与したほうが便利になります。

Windows/macOS と違って挟まるものがい少ないためか、やはり速い。速いは正義。

縦置き3画面配置、4k2k、150% を Ubuntu の「設定」だけで組めた

これは素直にイイネと思った。「縦置き3画面配置」はこういうことです。

https://x.com/sasasin_net/status/1401072283003674626?s=20

Windows や macOS でマルチディスプレイを構成しようとしたら、OS設定の「ディスプレイ」で、90度ずつ回転させたり、ディスプレイを任意のレイアウトで描画させたり、解像度と拡大率を設定して、といったことが普通にできますね。Ubuntu 26.04 の Ubuntu の「設定」で同じ操作感で構成できて良かった。

昔は Ubuntu ではマルチディスプレイを組む難易度が高く、/etc にある x11 の設定ファイル(?)と、NVIDIA の GPU ボードで NVIDIA の設定アプリで、壊れないよう整合させたら一応ギリ組めはしたが、拡大率設定は、100% と 200% はできるが 150% は beta で挙動が怪しくて Gnome 3 系アプリは対応してるが Gnome 2 系は怪しくて、 KDE/QT 系アプリは何だったか環境変数を設定する必要があって云々とか、まじカオスすぎて、ほんと厳しかった記憶があります。

コーディングエージェントに sudo な操作を任せて楽する

既にあれやこれやと任せまくっているのですが sudo な操作を任せるのは悩ましいものですが、任せたい場面はあります。入り組んだ設定変更とか、挙動不審で原因究明と対処方法を調査させたいとか。

この場合コーディングエージェントに sudo -A を実行させ、パスワード入力だけ GNOME のダイアログで人間が行う方式が良いです。パスワードそのものをエージェントに渡す必要がありません。sudo は -A 指定時、SUDO_ASKPASS で指定された GUI ヘルパーからパスワードを受け取れる仕組みを備えています。

sudo apt install ssh-askpass-gnome
dpkg -L ssh-askpass-gnome | grep askpass # 実際のパスを確認

ChatGPT デスクトップアプリ or Codex なら ~/.codex/config.toml に以下を追加します。

[shell_environment_policy]
set = { SUDO_ASKPASS = "/usr/lib/openssh/gnome-ssh-askpass" }

Claude Code なら ~/.claude/settings.json に以下を追加します。

{
  "env": {
    "SUDO_ASKPASS": "/usr/lib/openssh/gnome-ssh-askpass"
  },
  "permissions": {
    "ask": ["Bash(sudo *)"]
  }
}

サンドボックスを有効にしている場合は、さらに次を足します。

"sandbox": { "excludedCommands": ["sudo *"] }

毎回の指示は ~/.codex/AGENTS.md および ~/.claude/CLAUDE.md に記載します。

# マシン全体に適用されるsudoポリシー

コマンドの実行にsudoが必要な場合:

- `sudo -A` を使用し sudo の認証は設定された askpass ヘルパー経由で行うこと。
- 会話の中で sudo パスワードの入力、貼り付け、タイピング、または開示を私に求めないこと。
- 標準入力(stdin)、コマンドライン引数、環境変数、ファイル、その他エージェントから参照可能な手段を通じて、sudo パスワードを取得しようとしないこと。
- サンドボックスで `no_new_privs` / `NoNewPrivs` が有効になっているために sudo が失敗した場合、その保護を回避したり弱めたりしようとしないこと。代わりに、必要なコマンドをサンドボックス外で実行する許可を求めること。
- 私が明示的に指示しない限り、サンドボックスの無効化、sudoers の変更、NOPASSWD 権限の追加、その他特権境界を弱めるような操作を行わないこと。
- 必要がない場合にはsudoを使用しないこと。

これで何やら壊れたり挙動不審な時に、コーディングエージェントに sudo を要する操作を任せられるようになります。

ローカルLLM

性能特性が変化するかもなので、llama.cpp のパラメータチューニングやりなおしですね。そのうちやる。llama.cpp は ROCm/HIP ビルドより Vulkan ビルドのほうが速いという真偽不詳な話も聞くので、これも試してみたい。推論エンジンは Windows で AMD で簡単で高速動作するから llama.cpp を使ってたけど、ROCm が動くから vLLM とか SGLang とか他も使える。いろいろ手を出してみたい。

Windows 11 Pro のライセンスが余った

自作 PC なんでライセンス買ってます。今回ので余っちゃったな。そのうち何かに使うでしょう。

〆

何をもって Linux デスクトップ元年と称するはか知らんですが、

  • Ubuntu Desktop は便利になってる
  • macOS や Windows で動いてるアプリは Linux にも移植されてる。 snap と mise で簡単にインストールできる
  • いざとなればコーディングエージェントに泣き付けばどうにかできる

あたりに元年を感じました。良い。

13年も放置してた Java 1.7 アプリを立て直した話

13年ほど放置されてた廃墟、Java 1.7 アプリの SReader です。

github.com

2011年頃に開発開始し、公開で無料な RSS については Google Reader で購読し、有償で会員制サイトの RSS に SReader を用いて購読していました。当時はバックエンドの MySQL に会員制サイトのログインID & パスワードを平文で保管して(ひどいw)、もうちょっとどうにかできてただろうとも思いますが、自宅サーバーで稼動させて私だけが利用するものだったし、まあもうええでしょう。当時お仕事で Java で書いてたこともあり練習がてらに始めたものでもあります。が、そうして購読してた有償なサイトがひとつ終わり、ふたつ消え、、、ついに全滅し、動かすことがなくなり、打ち捨てられていました。

Google Reader は2013年夏にサ終して、私は Feedly に転がりこんで数年ほど居座って、それから機能的に充実してる inoreader に引っ越して今に至ってます。

inoreader があるのに再び SReader を引っぱりだしてきたのは、使途としては以下記事の v3 のとおりで、使途は大きく変化し「有償で会員制サイトの RSS 購読」のためではなく「公開で無料な RSS 購読し記事本文をテキストファイルに保管したい」のためです。

sasasin.hatenablog.com

コーディングエージェント

実際的な使途の他にも、2026年2月頃に Claude Code に触れていたものの、1枚ペラの難読シェルスクリプトとか、1枚ペラの Python スクリプトとか、そんなカスみたいな分量の操作しかやらせておらず、もうちょっと仕事を感じられるボリュームとシナリオで使いこんでみたい気持ちが湧いていました。なるほどと、本件でやってみることにしました。

sasasin.hatenablog.com

どこから手を付けるか会議

眺めはじめた時点では、Java 1.7, Hibernate 4.2.7, MySQL 5.1 で、アプリケーションフレームワークは用いておらず、手書きした public static void main() をシェルスクリプトで起動させるというソースコードでした。

まず手元にある Windows 11 24H2 自作PCと M1 MacBook Air どちらも Java 1.7 と MySQL 5.1 を動かすのは相当難儀しそうなことが想像できました。絶対に不可能なわけではなさそうでしたが、いまさら労力をかけてやりたくなかったw。

何段階か強引にやれば、やってやれなくはないものの、調べものが大変だし、鶏か卵かみたいな、どこから始めるにしても大変そうで、いや!めんどうくさい!考えるのやめた!とりあえず ChatGPT に SReader の GitHub URL を渡して「こいつをどうにかして動くようにしたい。どっから着手したら動くところまで持っていけるか案をいくつか出してほしい」などと雑に投げつけて、あーだこーだと議論をして、

Java ソースコード改修を最小限のまま、 Docker 化して Docker Compose で起動するところまで、何段階かで持っていこう。その際に、特に Hibernate バージョンを上げるとソースコード改修範囲が広がりすぎて収集がつかない。Hibernate バージョンそのままに、足回りを整えていこう。

として、いくつか PR を作ってマージしていきました。

docker compose up -d で13年ぶりに起動した様子は感慨深いものでした。

なお出してくれた案のひとつに「要するに欲しい機能の要件だけをもとに、まるごとバッサリ新規設計実装で最初からやってしまうのも選択肢です」と出してくれましたが、それはそうだけど、そうじゃない、厄介な状況からどうにか何段階かで突破する協働の経験をしたいんだ。

不要な機能の削除

今回の立て直しにあたって、後に無駄にリライトや動作確認の手間を減らしたいので、もはや不要な機能とデータ構造を破棄しました。

特に、HTTPアクセスしてチマチマと認証情報を埋めてログインする仕組みなどは、まったく不要なのでバッサリ破棄しました。認証情報を暗号化保存するとか、Playwright でサイトごとにログインさせるとかを考慮不要にできて、大幅にスッキリしました。

eft_rules

SReader では記事本文抽出に LDRFullFeed にて JSON 形式で配布されていた XPath ルール終を eft_rules テーブルに収納して用いていたのですが、当時は存在していた http://wedata.net/databases/LDRFullFeed/ が現在は消滅していると知って、詳しい経緯は知らないのですが、当時は大変にお世話になりました。

どうにかサルベージして公開された JSON が今でも公開されてるようです。

このこともあって後述の Readability を用いることにしたのですが、いちおう Readability でも空振りすることはあるので、XPath ルールのルックアップテーブルとして残してはあり、ただまあ、そのうち eft_rules 自体を削除するかもしれません。

MySQL から PostgreSQL に変更

これは深い意味のない変更でしたが、このくらいデカい変更でも任せられるものなのか、試したくてやらせたタスクでした。また、自宅サーバー内では既に PostgreSQL を稼動させており、そこに相乗りさせたかったのもあって、話がこじれる前に PostgreSQL に移行しておきたかった。

Spring Boot & jOOQ でリラリト

ここまでで、エージェントとの協働の感触がつかめてきたので、今後の機能実装の足回りを固めるため、がばっとリライトに取り組みます。お仕事ならテストカバレッジを高めに高めてからなんですが、まあ、私だけが使うものだし、いいっしょで突き進みます。

Spring Boot も jOOQ も、他いくつか選択肢を比較検討して、私の趣味に合うものとして両者を選びました。

Renovate が出してきた PR

Renovate は早い時期から稼動させてた気がします。Renovate 稼動を始めて早々に、Spring Boot や jOOQ をはじめとして、わりと何もかもに Renovate PR が出てきました。マイナーアプデもあったし、メジャーアプデもあって、おお、エージェント任せで作らせると、なるほど知識カットオフは新しくとも、けっこう相当に古いバージョンを選んんでくるんだなと体験しました。

この時点ではテストカバレッジはカスみたいなもんですが一応はありませいた。ただそもそも CI がほぼ無いので、ひとつずつ gh pr checkout して、エージェントに「この変更マージするにあたって破壊的変更な箇所をアプデ対象ライブラリのマイグレガイドを調べてほしい。破壊的変更があれば、SReaderのアプリケーションコードも改修してほしい」などとやらせて、ちまちまマージしていきました。

機能の実装

あれが欲しい、これが欲しい、をひたすら設計して実装させます。

  • RSS/Atom feed 登録に先立って、全文取得手法の事前確認のための probe コマンド
  • RSS/Atom feed 登録機能 を toml による巡回先のエクスポートとインポートとして実装
  • 全文取得手法の実装
    • feed, http, readability, playwright, infy scroll の組み合わせ
  • 全文取得した後の出力方法
    • いくつか用意するかと思いきや、テキストファイルでのエクスポートのみとした

infy scroll は一応は作ってみたものの、それを使いそうなウェブサイトを購読しておらず、あまり深追いした実装になってません。私が使うことがあれば本気で設計実装する、、かもしれません。

実装してから「feed」と「playwright_readability」をもっぱら使っていましたが、playwright は Chromium が起動しマシンリソースを多めに食ってスケールし難いな、うーむ、、と考えて、「http_readability」でイケるんじゃねえのと試したら、ほぼ全部の巡回先がこれで十分でした。今になってみると、けっきょく使ってるのは「feed」と「http_readability」どちらかだけで、「playwright_readability」まで要するウェブサイトには遭遇してません。

TTS の実装

当初は SReader で TTS も実現させて、mp3 ファイルを出させようと模索してました。しかし日本語TTSをいくつか試して、自宅サーバーのショボいCPUでは、実用的な速度でTTSは無理っぽいとの結論に至り、いったん棚上げしてます。

棚上げして模索して結果、Android アプリ @Voice に至ってます。

もし @Voice に不満が募ってきたら、本気で設計実装する、、かもしれません。

play.google.com

CI 整備

ウェブアプリと違ってヘッドレスなアプリなので、何かあるとしたら「ここ数日ずっと新記事が届かないけど何だろな」みたいな気付き方しかできなさそうだし、ひとまずデプロイ前にいろいろと気付けるようにアプリケーションコード、インフラコード、両面で CI を整備した。

  • テストカバレッジレポート取得して GitHub Page で公開した。ゆるふわユニットテストだったので現状理解すべく。
  • ユニットテストを整備しまくった。あまりに酷かったので。
  • テストカバレッジの閾値で失敗させた
  • Renovate でバージョン番号を片っ端から拾って反応させるようにした
    • pom.xml、コンテナイメージ、k3s バージョン
  • renovate.json が凝った記述になるので GitHub Action で lint させた。
  • k3s バージョンが上がったら GHA 環境内に k3d を起動して k8s マニフェストを apply などして deprecation 確認させるようにした

CD 整備

ここまで docker compose で手元で動作確認だけやってましたが、本命は自宅サーバーで常時稼動させることです。

「こうで、あーで、こんな構成の自宅サーバーにデプロイさせるにあたり、今のソースコードのまま、デプロイ手順を考えてほしい」したら、とんでもなく複雑な作業手順を出してきて、いやはや、こんな手順を毎回なんて無理ぞと、デプロイ手順の簡素化のための整理整頓をはじめます。

  • jOOQ 生成コードを、それまでは実環境で生成させていたのを、生成したコードを Git 管理するよう方針転換
  • 自宅サーバーで稼動させるときは、docker compose ではなく、k8s マニフェストでデプロイするよう改めた
    • すでに k3s に、いくつか namespace を区切って OSS アプリを稼動させていたので、SReader も k3s 稼動させるつもりだった
    • これら k8s マニフェストも CI でチェックさせるようにした
  • コンテナイメージを、それまでは実環境でビルドしてたのを、GitHub Actiosn でビルドして GitHub Package にアップして、実環境では pull するだけに改めた

この記事を書いてる時点では、以上までで、最新のコンテナイメージを自動で自宅サーバーにデプロイする機構までは整備してない。そのうちやるかもしれない。

リファクタリング(?)

いわゆるビジネスロジックが集中してるあたりが魔境となりつつあって、以上までで大枠の欲しかった機能やデプロイまわりを整備して動くようになったところで、これまた雑にリポジトリURLをチャッピーと Claude Opus に渡して、「可読性の観点でどうにかできるものなのか講評して改善策を出してほしい」して、納得いったものは採択してリファクタリングを進めました。結果として部分的にマシになり、部分的にちょっとやりすぎたかも?みたいな感じになってます。

〆

数ヶ月前は諦観していたのですが、コーディングエージェントの練習がてらに使ってみたら、最初の無茶苦茶に大変なところをワッと飛び越えられて、その協働の感触をもとに、けっこう人力ではやりたくない大規模なリライトや、あれこれワガママ放題の機能実装、心が折れるようなテストコード整備、面倒臭い CI/CD 整備、しまいにリファクタリング(?)までやれました。

何事もやってみるものですね。

サ終した Pocket の「RSS の全文取得して、次々と読み上げで聞いて、聞き終えたらアーカイブしていく」体験を再現できた

RSS/Atom で購読してるウェブサイトの記事を、目視で読んでたら時間がいくらあっても足らないので、私は耳で聴くことにしてます。ラジオのように聴きたいので、きっちり記事全文を取得して、連続で読み上げてほしい。

v1: inoreader & Pocket

Pocket というウェブサービスと、そのモバイルアプリは、これを高いレベルで実現していたのですが、サービス終了してしまいました。

inoreader で特定のフォルダに登録した RSS/Atom について、新規記事を検知する都度、inoreader が Pocket に URL 登録してくれます。Pocket は URL にアクセスして記事本文取得を頑張る。RSS の全文取得して、次々と読み上げを聞いて、聞き終えたらアーカイブしていきます。記事本文は、取れることもあれば空振りしてることもある。空振りしたものは Chrome などで開いて読み上げて聴く。2025年07月08日にサービス終了。残念。

詳しくは以下記事を眺めてください。

sasasin.hatenablog.com

v2: wallabag-importer & wallabag

Pocket からの引っ越し先として、複数サービスを試して wallabag をしばらく使っていました。wallabag 自身が RSS リーダー機能を有していることになっています。そこで、inoreader で特定のフォルダの公開URLを発行します。公開URLは RSS 形式です。この URL を wallabag の RSS リーダーで購読する。inoreader で特定のフォルダには200個くらいのブログ等のRSS購読してます。これが1個のURLとして公開されるので、200個を wallabag にチマチマ登録やらなくて済むのです。

歓喜して使っていたのですが、数ヶ月すると粗に遭遇するようになり、特に「RSSリーダー機能が空振りする(というか一切動作しなくなる)」「読み上げ終えて、次の記事に進む機能が空振りする」の難から徐々に使わなくなっていました。

「RSSリーダー機能が空振りする」部分は自宅サーバーで wallabag-importer を cron 実行することでカバーしてました。 wallabag 側に同一記事が無限回数登録される事象など遭遇して、ちょっとこれはなあ、厳しいなあと感じて、wallabag.it のサブスク解約しました。残念。

github.com

他クラウドサービスはどうなのか

wallabag から離れて他も模索しましたが、月額課金の有償サービス(wallabag より高額)だったり、読み上げの音声がイマイチなくせに有償サービス(Android の読み上げエンジン & 音声よりも、音質ガビガビで鈍りがキツい)だったり、機能的に wallabag と大差ないとかで、うーーん、、、として v3 の構成にふりきりました。

v3: SReader & @Voice

自作の RSS/Atom リーダー SReader で記事本文を1記事1個のテキストファイルに出力し、pCloud に保存し、 @Voice で複数個のテキストファイルをリスト登録して連続で読み上げて聴きます。

自作の RSS/Atom リーダー SReader を、もう13年間くらい使用せず打ち捨てていたのを引っぱりだして、OpenAI Codex と xAI Grok Build で再建しました。これを自宅サーバーで常駐稼動させています。

github.com

@Voice はスタンドアロンの Android アプリで、「URL, PDF, EPUB, テキストファイルを再生リストに登録すると連続して読み上げる。URL は全文取得して Readablity 的手法で本文だけ取得する。読み上げ音声は Android のアクセシビリティ機能のエンジンと音声を使う」というものです。URL を渡せることから、期せずして「後で読む」も @Voice を使うことにしてます。

play.google.com

自宅サーバーの SReader が出したテキストファイルを、Android スマホ内の @Voice に渡すにあたり、受け渡し場所として pCloud を用いてます。自宅サーバーは rclone で pCloud ドライブ内の所定のフォルダをマウントさせます。Android スマホでは pCloud アプリで所定のフォルダに置かれたテキストファイルをスマホ内にダウンロードします。これをファイルエクスプローラー的アプリで全選択して @Voice に共有で「リストに登録」することで @Voice の再生リストに登録されます。あとは @Voice で再生リストの上から順に再生させます。

間に pCloud を挟んでいるのは、私が pCloud アカウントを持っていて、かつ、 ubuntu で rclone でマウントできたからで、rclone が対応してるクラウドドライブなら何でも代替できると思います。

rclone.org

〆

v3 は人力操作の手数が多いのですが、自分なりに十分実用と感じられる程度には再現できました。これ以上どうにかやろうとすると、もう Android アプリ自作するしかなくて、ちょっとそこまでは尻込みしています。

Inspiron 14 のストレージを 256GB から 1TB に換装した

Windows ノートパソコン Dell Inspiron 14 のストレージ 256GB のうち残 30GB パツパツなことに気付き、削除できるものは削除を試みたものの全然で、そういや間違って購入したシリコンパワーの NVMe 2280 SSD 1TB が余っていたのを思い出し発掘して換装しました。clonezilla でコピって、Cドライブパーティションを拡張したら終わりっしょ楽勝よ、と思っていたのですが「回復パーティション」が並んでいたのと、回復パーティションを残す決断をしたので、なかなか厄介なオペレーションでした。

sasasin.hatenablog.com

用意するもの

コピー元となる 256GB SSD の BitLocker を解除する

※ Windows Terminal で PowerShell を「管理者として開く」で作業します。以下、特に言及がない場合は「管理者として開く」で作業してるものとして読んでください。

一時解除ではなく完全に復号化。いろいろ厄介が省けます。

現在の暗号化ステータスを知るには;

manage-bde -status

完全に復号化するには;

manage-bde -off C:

で復号化完了するまでしばらく待つ。

Clonezilla でパーティションレイアウト等々まるごとコピーする

USB スティックメモリに Clonezilla を Rufs で焼いて、起動時に F2 で BIOS/UEFI でブート優先度を Clonezilla が最優先で起動するよう改める。

clonezilla.org

rufus.ie

Clonezilla が起動したら、表示されてる案内に従って、初心者メニューを選択して(カッコつけて上級者メニューを選ぶと死ぬ)、コピー元ストレージ、コピー先ストレージを誤らず選択して進めていく。最後に「終わったらどうするか選べ」は好きなのを選んだらいい。これでまた暫く待つ。文字を読めばわかる。読まないと死ぬ。

SSD 換装

Clonezilla が完了したら、Inspiron 14 を電源通電停止する「サービスモード」にしてから、バラして SSD を換装します。サービスモードさえ間違わなければ何も困るとこはないはずです。

www.dell.com

1TB で起動。パーティションレイアウト調整

Insipiron 14 の電源ONして起動する。ここまでノーミスなら何も問題なく起動してくれるはずです。

ストレージはハードウェアとしては 1TB あるが、パーティションは 256GB のクローンであるため、256GB しか切り分けていない。なのでエクスプローラーとかで見ても 256GB のうち空xxGB のようにしか見えない。「ディスクの管理」アプリでは、パーティションレイアウト、未割り当て領域も全部見れます。

ディスクの管理でパーティションレイアウトを見る

ここから先で「715.41GB 未割り当て」をどうにかします。

どうにかする方法はいくつかあって、

  1. 今の見えてるレイアウトを触れずに、「715.41GB 未割り当て」を D ドライブとして割り当てる。
  2. 正常(回復パーティション)の3個を、削除する。空いたところを C ドライブの拡張として割り当てる。
  3. 正常(回復パーティション)の3個を、どうにかしてストレージの後ろにズラして、空いたところを C ドライブの拡張として割り当てる。

今回は 3 を選びました。今後この Inspiron を手放すことがあった場合に、回復パーティションが無いと、厄介なことになった場合の復旧手段が減ってしまうので、そういう意味で残しておきたい。

作業前の現状確認

reagentc /info
diskpart
list disk
select disk 0
list partition

各 recovery パーティションを select partition N → detail partition すると、Type の GUID で WinRE 用かどうか判別できます。

B-1. WinRE を退避・無効化

reagentc /info        # 現状 partition4 を確認
reagentc /disable
reagentc /info        # Disabled になったことを確認

WinRE は P4 を消しても C:...\Recovery\Winre.wim に残るので、P4 自体は「もう中身を保全しなくていい」状態になります。つまり保全が必要なのは P5(Image)と P6(DELLSUPPORT)の 2 つだけです。

B-2. P5・P6 の中身をイメージ化

diskpart で一時的にドライブレターを割り当て、DISM で取り出します。回復パーティションにレターを割り当て、Dism /Capture-Image /ImageFile:... /CaptureDir:... /Name:"..." で中身をイメージ化できます。

diskpart
select disk 0
select partition 5
assign letter=Y
select partition 6
assign letter=Z
exit
Dism /Capture-Image /ImageFile:C:\dell-image.wim /CaptureDir:Y:\ /Name:"DellImage"
Dism /Capture-Image /ImageFile:C:\dell-support.wim /CaptureDir:Z:\ /Name:"DellSupport"

B-3. P4・P5・P6 を削除

レターを外してから削除します。毎回 list partition で番号確認:

diskpart
select disk 0
select partition 4
delete partition override
list partition
select partition 5      # 番号が繰り上がっていないか確認の上
remove                  # レター解除
delete partition override
list partition
select partition 6
remove
delete partition override
list partition

これで C: 直後から末尾まで未割り当てが連続(約 731GB)。

B-4. C: を拡張(末尾に 3 パーティション分 + 余白を残す)

退避先として末尾に WinRE 1.2GB + Image 13GB + DELLSUPPORT 1.5GB ≒ 16GB 必要です。余裕を見て 約 17GB(17,408MB)残すのが安全。残量は list disk の「空き」で確認し、そこから 17,408 を引いた量を MB 指定で:

select volume c
extend size=<残量MB - 17408>

size 無指定は厳禁(全部食う)。これで C: が ~931GB、末尾に ~17GB の未割り当てが残ります。

B-5. 末尾に 3 パーティションを再作成

末尾の未割り当てに、WinRE → Image → DELLSUPPORT の順で作ります。

(a) WinRE パーティション

create partition primary size=1212 id=de94bba4-06d1-4d40-a16a-bfd50179d6ac
gpt attributes=0x8000000000000001
format quick fs=ntfs label="Windows RE tools"

(b) Dell Image パーティション

create partition primary size=13312 id=de94bba4-06d1-4d40-a16a-bfd50179d6ac
gpt attributes=0x8000000000000001
assign letter=Y
format quick fs=ntfs label="Image"

(c) DELLSUPPORT パーティション(残り全部)

create partition primary id=de94bba4-06d1-4d40-a16a-bfd50179d6ac
gpt attributes=0x8000000000000001
assign letter=Z
format quick fs=ntfs label="DELLSUPPORT"
exit

サイズは元の値(P4=1212MB, P5≒13GB, P6=1457MB)に合わせています。元と完全一致である必要はないので、Image は 13312MB あたりで十分。属性 0x8000000000000001 はドライブを隠し、必須パーティションとしてフラグを立てる GPT 属性です。

B-6. Dell の中身を復元

Dism /Apply-Image /ImageFile:C:\dell-image.wim /Index:1 /ApplyDir:Y:\
Dism /Apply-Image /ImageFile:C:\dell-support.wim /Index:1 /ApplyDir:Z:\

復元後、レターを外します(回復系はレター無しが正しい状態):

diskpart
select disk 0
select volume Y
remove
select volume Z
remove
exit

B-7. WinRE を新パーティションで再有効化

reagentc /enable
reagentc /info     # Enabled かつ location が新しい partition を指すこと

万一 /enable がエラーになる場合、C:\Windows\System32\Recovery の Reagent.xml / Reagent_merged.xml を削除してから reagentc /enable すると新しい .xml が書かれて通ることがあります。

Windows 再起動

ここまでで目標としているパーティションレイアウトに到達しています。掃除などする前に、いったん再起動して起動できることを確認します。

整理整頓後のパーティションレイアウト

B-8. 後片付け

戻ってこれたら掃除します。

del C:\dell-image.wim
del C:\dell-support.wim

BitLocker 有効化

Windows 11 Home ですが、設定 → プライバシーとセキュリティ → デバイスの暗号化でトグルを オンにすると、BitLocker が有効になります。

Windows 11 Home だけど BitLocker できる

進行状況は以下で確認できます。

manage-bde -status

BitLocker 回復キー

48桁の回復キーを必ず保存します。

manage-bde -protectors -get C:

Microsoft アカウント保管しているなら回復キーを以下URLで確認できます。

https://account.microsoft.com/devices/recoverykey

〆

以上で作業完了です。パーティションレイアウトが邪魔くさい感じに並んでいたせいで、余分な作業が発生して厄介でした。ひろびろしたストレージで快適に作業できます。

CPU は 12th Gen Core i5-1235U で、Claude Code を動かすとか、 Zed でテキスト作業なら実用十分です。メインメモリ 64GB とストレージ 1TB は、ノートパソコンにしては無闇に強いスペックになっちゃったな。イマドキのノートPCは、こういう交換ができない作りになっていることが多いようで、残念です

Grok Build も使ったらいい

ChatGPT に設計相談して作業指示文書を作成させています。こんなかんじで。こうしてるのは、 ChatGPT は利用制限が寛容なのと、 Codex と ChatGPT は利用制限枠が別々管理になっているから。

  1. ChatGPT に GitHub リポジトリURL を渡して、こういう機能改修やりたいんだが、こんな要素技術を使った設計実装を考えており、どうしたもんかね、などと設計相談する。
  2. ChatGPT と何度かラリーする。
  3. 納得したところで ChatGPT に指示文書を作成させる。
    • 「これ Codex にやらせたいので指示文書を作成して。ここまでの議論を Codex は見れないから、うまく文脈を盛り込んで」的に。

例として、できあがった指示文書がコレ。

github.com

これを抱えて意気揚々 Codex に持っていったら、ちょうど別の作業で Codex が 5 時間制限を別の作業で使い切ってしまっていた。まあ待っててもよかったんですが休日は短い。待っておれぬ。うーーーんと使えるものを記憶から探す。

Grok Build を利用できることを思い出し、ものの試しにやらせてみた。 Grok Build 。イーロンマスクのとこの出してるコーディングエージェント。ツイッターの X Premium+ に課金していると利用できる。正直 X Premium+ は月額料金の割に経済価値を引き出せてなくて、どうしたもんかと思っていたところなので、使い倒すつもりで試してみた。

x.ai

Zed の External Agent で Grok Build の ACP 接続をセットアップした。コーディングエージェントが増えても邪魔なのでね。セットアップ初回でコソッと Grok Build CLI 現物もインストールされてるはずだけど、PATH の通ってるとこには入ってない(探せばあるはず)。利用初回にチャット入力欄に出ている Authenticate Grok 的なボタンを押すとウェブブラウザ起動して OAuth でログインして利用できるようになる。Grok Build の利用は API従量課金ではなく、 X Premium+ サブスク料金で利用できる。Claude Code とは異なり Zed の ACP 接続であっても、 X Premium+ サブスク料金で利用できる(すくなくとも今のところは)。

zed.dev

Grok Build も他のコーディングエージェント同様に利用枠上限は存在していて、今のところは「月間利用枠」のみ設定されている。これは鷹揚なのかケチなのか、ちょっとわかりにくい気はした。今回作業で 1% くらい使ったらしい(別の作業もやらせたので合わせて 2% と表示されてる)。

Grok の利用枠の利用状況の確認画面

確認画面リンクは以下。

grok.com

さて先程の指示文書を「この文書に従って作業してください」で Grok Build に渡して、できあがった PR がこれ。「 docker コマンド実行していーい?」を何度も聞かれたが、それ以外に作業中に私に質問してくることもなくブツブツと進めて、きちんと完走して GitHub PR を出してきた。

github.com

作業指示文が ChatGPT によってギチギチに組まれたものだったことを割り引いたとしても、仕事するんだなと感心した。

とはいえ信じておらず、私が眺めて、さらに Codex にも PR レビューさせたら「ちょっとテストコードこの観点だけ手薄なので追加したら満点ですね、追加します」だけで済んでいた。なので最後1コミットだけ Codex が積んだ。

Grok Build なかなかやるじゃないか。Grok Build のために X Premium+ 課金してまでとは思わないけど、別の用事で課金してるなら、ついでにたまに Claude Code や Codex の使用枠を節約するためでも、 Grok Build も使ったらいいと思った。

Zed よさそう

ふと試した Windows 版 Zed が私の手に馴染むように感じたので、本腰入れて整備した。

OpenCode フロントエンドは複数の選択肢がある

いろいろ試してみるが、どうもしっくりこない(社会性フィルター)。

そういや Zed が Agent コーディングがどうと宣っていたような。ダメ元で「OpenCode のフロントエンドとして Zed を使う」観点で試したら、これがなかなか良かった。

Zed のインストール

winget でのインストールが公式。が、winget は、インストールは大丈夫でも、アップデートがコケるのを他のアプリで多々体験しており、あまり良い印象がない。

非公式だが scoop で管理できる。scoop でインストール/アップデートやるとしよう。

Zed のフォント、フォントサイズ、カラーテーマ

インストール初日は、いろいろ触ってみたものの勝手がわからず、気持ちが萎えてアンインストールを考えた。日を置いて立て直して、フォント、フォントサイズ、カラーテーマを設定して気持ちを高めた。これはGUIの設定パネル等々でポチポチやったら意図したとおり設定できた。

カラーテーマは拡張機能の一種としてインストールできる。 Nightfox Theme という眩しくないテーマを入れた。

  "ui_font_family": "UDEV Gothic NFLG",
  "buffer_font_family": "UDEV Gothic NFLG",
  "ui_font_size": 16.0,
  "buffer_font_size": 16.0,
  "agent_ui_font_size": 16.0,
  "agent_buffer_font_size": 16.0,
  "theme": {
    "mode": "dark",
    "light": "Dayfox - opaque",
    "dark": "Nordfox - opaque",
  },
  "icon_theme": {
    "mode": "system",
    "light": "Zed (Default)",
    "dark": "Zed (Default)",
  },

zed.dev

Zed の Terminal

Windows デフォは PowerShell のところ、 Git Bash が起動するようにした。これはGUIの設定パネルでポチポチやったら意図したとおり設定できた。

"C:\\Users\\sasas\\scoop\\apps\\git\\current\\bin\\bash.exe" では zed が bash.exe の発見失敗していた。

"C:\\Users\\sasas\\scoop\\apps\\git\\current\\usr\\bin\\bash.exe" なら動いた。よくわからない。

  "terminal": {
    "scrollbar": {
      "show": "always",
    },
    "shell": {
      "with_arguments": {
        "program": "C:\\Users\\sasas\\scoop\\apps\\git\\current\\usr\\bin\\bash.exe",
        "args": ["-l", "-i"],
      },
    },
  },

zed.dev

Zed の External Agent

エージェントコーディング用のパネルが用意されている。デフォは zed.dev に接続する Zed Agent で、ACP 接続で他も利用できる。

zed.dev

他のひとつとして OpenCode に向けるようにして、Zed の Agent Panel で OpenCode とのチャットを開通させた。仕組み的には ACP で OpenCode に接続して云々らしい。これはGUIの設定パネルでポチポチやったら意図したとおり設定できた。

  "agent_servers": {
    "opencode": {
      "default_config_options": {
        "effort": "high",
        "mode": "build",
      },
      "type": "registry",
    },
  },

zed.dev

Zed の LLM Provider

Zed の Git Panel で git commit したときのコミットメッセージ生成、等々(いろいろ使われてるらしい)で LLM Provider を用いる。ローカル起動してる llama-server に向けるようにした。これはGUIの設定パネルでは意味不明だったので、ドキュメントを眺めつつ、チャッピーに素案を作らせて、 settings.json に追記した。

  "language_models": {
    "openai_compatible": {
      "llama-cpp": {
        "api_url": "http://127.0.0.1:8080/v1",
        "available_models": [
          {
            "name": "local-llama",
            "display_name": "llama.cpp local",
            "max_tokens": 262144,
            "max_output_tokens": 8192,
            "max_completion_tokens": 2048,
            "capabilities": {
              "tools": false,
              "images": false,
              "parallel_tool_calls": false,
              "prompt_cache_key": false,
              "chat_completions": true,
              "interleaved_reasoning": false,
            },
          },
        ],
      },
    },
  },
  "agent": {
    "default_model": {
      "provider": "llama-cpp",
      "model": "local-llama",
    },
    "inline_assistant_model": {
      "provider": "llama-cpp",
      "model": "local-llama",
    },
  },

zed.dev

Zed のコード補完

Ctrl+Enter したときコード補完が発動する。デフォ設定では zed が運営してるサービスを利用するよう構成されており、一定回数の無償枠が付いてる。これも llama-server に向けるようにした。いちおう設定はしたものの、正直いまさら使うかというと、うーん、、、。 "prompt_format": "qwen", としているのは Qwen3.6 35B A3B を使っているから。

  "edit_predictions": {
    "provider": "open_ai_compatible_api",
    "open_ai_compatible_api": {
      "api_url": "http://127.0.0.1:8080/v1/completions",
      "model": "local-model",
      "prompt_format": "qwen",
      "max_output_tokens": 1024,
    },
  },

zed.dev

他

設定パネル全部を上から下まで見ていくが、読んでも何がどうなるのかサッパリわからない項目が超多数で、理解するのは諦めた。使っていくうちに「これがソレなのね」と体感していくだろうから。

macOS 版 Zed

homebrew でインストールできる。

brew install zed

これも試してみたものの、 MacSKK のキーバインドとの噛み合わせに問題があるようで、現段階では常用は難しいので、いったん見送ることにした。そもそも OpenCode を使うのは Radeon AI PRO R9700 を挿していてローカルLLMやってる Windows だからで、それ以外の Mac や Linux では Zed を使う必然性に乏しいのだった。

〆

Windows 版 Zed だいたいイイ感じに整ったんじゃないかな。しばらく使ってみる。

OpenCode でモード毎に thinkgin ON/OFF 指定するにはクセ強な記述が必要っぽい

これは何

OpenCode では Agent という機能により、計画相談の「Plan mode」、実装を進める「Build mode」を表現しています。これを ~/.config/opencode/opencode.jsonc に定義することで任意に Agent を増やせます。

さて LLM プロバイダーとしてローカル起動させた llama.cpp を用いて、 Thinking の ON/OFF をリクエスト毎にスイッチできる LLM を用いる場合は、 OpenCode 側から適宜にリクエストを送ることで、「これは plan mode なので thinking ON で返答して」「これは build mode なので thinking OFF で返答して」といった挙動を実現できます。

となると ~/.config/opencode/opencode.jsonc の agent のところに、どうにか記述したら定義できそうなものです。

が、どういうパラメータを渡せば thinking ON/OFF となるかは LLM ごとに千差万別であるため、ここに、こう書いたら、どんなモデルでも thiking ON/OFF を実現できます!となっていないようです。

たとえばで Gemma4 は llama-server 起動時オプションで以下のようにすることで thinking OFF をデフォとして動作させられます。これとは別で、 opencode.jsonc 側でどうにか記述することで、 enable_thinking: ture とか false を送って Thinking ON/OFF をスイッチできる、、はず!

llama-server --chat_template_kwargs='{ "enable_thinking": false }'

ということで、こんな指示を投げて、

さてそれでは、あなたは [ (0) proxy.py を起動する。 (1)opencode.jsonc の enable_thinking に関する記述を変更する。 (2) opencode run .... -agent plan および -agent build を実行する。 (3) proxy.py が出力したログを観察する。 ] を何度か繰り返し、 proxy.py 無しで enable_thinking の ON/OFF を正確に伝える opencode.jsonc を模索してください。1 周ごとに、試したこと、結果、次に試すことをレポートしてください。まず4周してみてください。4周までに終われば、その時点で完了してください。4周を終えても成功しなければ、次の指示を仰いでください。

4周で成功せず、ダメ元で以下指示

options.body.chat_template_kwargs のところを options.chat_template_kwargs と記述したパターンで検証してみてください。なんかダメそうな気はしますが...

で試してもらったのが成功しました。

Claude によるまとめ

試したパターンと結果

# opencode.jsonc の記述 リクエストへの影響 結果
1 agent.plan.options.body.chat_template_kwargs (当初) body.body.chat_template_kwargs にネスト llama-server に無視される
2 agent.plan.providerOptions.llamacpp.chat_template_kwargs 何も乗らない 無効
3 agent.plan.providerOptions.chat_template_kwargs 何も乗らない 無効
4 agent.plan.providerOptions.enableThinking 何も乗らない 無効(Anthropic SDK 専用経路)
5 agent.plan.options.chat_template_kwargs トップレベルに chat_template_kwargs が展開される ✅ 正解

解決策:options.chat_template_kwargs

"agent": {
  "plan": {
    "model": "llamacpp/gemma-4-e4b",
    "options": {
      "chat_template_kwargs": { "enable_thinking": true }
    }
  },
  "build": {
    "model": "llamacpp/gemma-4-e4b",
    "options": {
      "chat_template_kwargs": { "enable_thinking": false }
    }
  }
}

llama-server に届くリクエスト:

{
  "model": "...",
  "messages": [...],
  "chat_template_kwargs": { "enable_thinking": true }
}

なぜこれで動くか

@ai-sdk/openai-compatible の options 配下では body キーだけが特別扱いされ、その中身が body というキー名ごとリクエストボディに埋め込まれる。body 以外のキー(chat_template_kwargs など)はトップレベルにそのまま展開される。この挙動を利用することで、プロキシなしで chat_template_kwargs をトップレベルに送ることができる。

最終結論

agent.*.options.chat_template_kwargs を使えば、OpenCode 1.15.13 + llama-server b9437 の組み合わせで plan/build モードの thinking ON/OFF 切り替えがプロキシなしで動作する。

文中の proxy とは?

下図のようにポートなど整合するように起動させます。

OpenCode <--> proxy.py <--> llama-server

proxy.py は当初は上記「試したパターン(1)」にもとづいてHTTPパケットを書き換えて右に左に流すものでした。しかしこんなもん毎度毎度起動させてらんねーので、どうにかこれをナシにできないか。そこで OpenCode と llama-server の間に置いて、 opencode.jsonc の記述を変化させ、opencode からプロンプトを投げさせて、llama-server にどのように届いてるか、 proxy.py で観察して、追い込んでいきました。

後述「参考」のところに Gist としてアップしました。Claude がガッと作ったものなので、わざわざ読むほどのものでもないと思います。

正直、本当なの?

とりあえずこれで、OpenCode で操作していて、 plan のときは Thinking... が出て、 build のときは Thinking が出ずスパッと返答してくるので、まあこれでいいんだろうなあとは理解しています。しかし、なんかもっと簡単に書けそうな気もしてしまいますが、いったん私が欲しい答えは得られたので、ここまでにしておきます。

参考

opencode.jsonc のサンプル。例として Gemma4。

https://gist.github.com/sasasin/0ace5f3d1ff3ad3f672284e292231b22#file-opencode-jsonc

文中の proxy.py 。Claude にガッと作ってもらったものです。

https://gist.github.com/sasasin/0ace5f3d1ff3ad3f672284e292231b22#file-proxy-py