Skip to content
Copied!
published on 2026-09-12

4. Bashで実現するストリーミングと移植性

単一スクリプトの構成

myllm-cliの中核は,myllmという1本のBashスクリプトです。このファイルに,設定の読み取り,タスクの解決,翻訳時の言語判定,三つのプロバイダーへの接続,CLIの引数処理が含まれます。Pythonなどの言語ランタイムを追加せず,HTTPをcurl,JSONをjqへ委ねています。

Bash 3.2を基準とするのは,macOS標準の/bin/bashでそのまま動かすためです。連想配列,mapfilereadarrayなど,新しいBashにしかない機能は使用しません。この制約を守ることで,同じスクリプトをmacOS,Linux,WSL2へ配置できます。

設定ファイルはTOMLですが,スクリプトは完全なTOML処理系を内蔵していません。単一行の文字列,真偽値,複数行リテラル文字列,セクションという,設定テンプレートで必要な範囲を行単位で読み取ります。このため,設定は配布されるテンプレートの書式に沿って編集します。

プロバイダーごとのストリーム

三つのAPIは,ストリーミング応答の形式が異なります。myllmは各形式をプロバイダー関数で吸収し,本文だけを共通のstdoutへ流します。

プロバイダーエンドポイント応答形式本文の抽出元
Ollama/api/chat改行区切りJSON.message.content
OpenAI/chat/completionsServer-Sent Events.choices[0].delta.content
Anthropic/messagesServer-Sent Eventscontent_block_delta.delta.text

curl -Nは受信データのバッファリングを抑えます。さらにjq --unbuffered -jを使い,JSONから取り出した断片を余分な改行なしで直ちに出力します。-rではなく-jを使うのは,モデルが生成した改行と,jq自身が各JSONへ付ける改行を混在させないためです。

CLI側でも,生成結果を$(...)へ格納してから表示しません。コマンド置換は処理完了まで表示を待ち,末尾の改行も取り除くためです。プロバイダー関数のstdoutを直接端末へ流すことで,応答中の改行と逐次表示を維持します。ファイルへリダイレクトした場合も,同じstdoutが保存されます。

リクエスト処理の共通化

通常のタスクでプロバイダーへ渡す内容は,次の二要素です。

  1. config.tomlで定義したinstruction
  2. CLIの引数またはstdinから得た入力テキスト

両者を空行で連結し,各APIの単一ユーザーメッセージへ格納します。プロバイダーごとの差は,接続先,認証ヘッダー,JSONの形,ストリーム解析に閉じ込めています。そのため,タスクの指示は接続先を変更しても再利用できます。

ただし,異なるモデルが同じ指示に同じ形式で従うとは限りません。タスクを別のモデルへ切り替える場合は,代表的な入力で出力形式と品質を確認します。myllm info <task>を実行すると,解決後のプロバイダー,モデル,指示を確認できます。

ライブラリとしてsourceする

myllmは直接実行するCLIであると同時に,他のBashスクリプトからsourceできるライブラリです。ファイル末尾では,直接実行された場合にだけmainを呼ぶため,source時には関数の定義だけが読み込まれます。

bash
#!/bin/bash
set -euo pipefail

source "$(command -v myllm)"

myllm_process "polish" "下書きの文章"

翻訳はtranslate <text> <source_lang> <target_lang>として呼び出せます。翻訳元を自動判定し,日本語へ翻訳する場合は,翻訳元に空文字列を渡します。

bash
translate "Hello, world." "" "ja"

このライブラリモードは,後に開発したmacOSメニューバーアプリMy LLMでも利用しています。GUI側でAPI呼び出しを再実装せず,アプリに同梱したmyllmをsourceして同じ処理関数を呼び出します。CLIとGUIでプロンプト構築や改行処理を共有できることが,単一スクリプト構成の利点です。

CLIとしての範囲

myllm-cliは,名前付き指示を保存し,入力を選択したLLMへ送り,結果をストリームで受け取る処理に集中しています。対話履歴を保持するチャットクライアントではなく,知識ファイル,Web検索,ツール呼び出しも管理しません。必要な文脈は入力として渡し,必要な後処理はUnixコマンドや呼び出し元のスクリプトで組み合わせます。

この範囲は制限であると同時に,コマンドラインツールとしての性質を明確にします。モデルやサービスをひとつに固定せず,同じ名前付きタスクをローカルとクラウドで使い分け,結果を通常のテキストストリームとして次の処理へ渡せます。TranslateGemma専用スクリプトから始まった仕組みは,こうして個人用の汎用LLMインターフェースへ発展しました。