Gemini CLIをローカルLLM(Ollama)専用クライアントに改造する
はじめに
GoogleのGemini CLIは、コマンドラインからGemini AIモデルと対話できる優れたツールです。今回は、このCLIを改造して、認証不要でローカルLLM(Ollama)専用のクライアントとして動作させる方法を紹介します。
gemma3nが公開されてできないかなと思ってやってみたら
思った以上に容易にできたので書きました。
開発環境:Claude CodeとDeepWiki MCP
今回の改造はClaude Codeを使用して実装しました。Claude Codeは、AIアシスタントがファイルの読み書きやコマンド実行を直接行えるため、実際のコード改造を効率的に進められます。
さらに、DeepWiki MCPを活用することで、Gemini CLIのリポジトリ構造やアーキテクチャを素早く理解できました。DeepWiki MCPは、GitHubリポジトリのドキュメントや構造を解析し、質問に答えてくれるツールです。
DeepWiki MCPを使うことで、以下の情報を効率的に収集できました:
- リポジトリ全体の構造とパッケージ構成
- ContentGeneratorインターフェースの役割と実装
- 認証システムのアーキテクチャ
- ライセンス情報(Apache-2.0)
この組み合わせにより、コードベースの理解から実装、テストまでをスムーズに進めることができました。
ライセンスについて
Gemini CLIはApache-2.0ライセンスで公開されています。このライセンスは、商用利用、修正、配布、私的利用を許可する寛容なライセンスです。今回の改造や、その内容の共有も問題ありません。
改造の概要
主な変更点:
- 認証機能を完全に削除し、ローカルLLM専用に
- OllamaのOpenAI互換APIを使用
- 起動ロゴをGEMINIからGEMMAに変更(絶対いらない)
ファイル構造と変更内容
1. ContentGenerator インターフェースの理解
/packages/core/src/core/contentGenerator.ts
このファイルがAIプロバイダーの抽象化の中心です。ContentGenerator
インターフェースを実装することで、異なるAIプロバイダーをサポートできます。
export interface ContentGenerator {
generateContent(
request: GenerateContentParameters,
): Promise<GenerateContentResponse>;
generateContentStream(
request: GenerateContentParameters,
): AsyncIterable<GenerateContentResponse>;
// ... その他のメソッド
}
2. OpenAI互換ContentGeneratorの実装
/packages/core/src/core/openAICompatibleContentGenerator.ts
(新規作成)
export class OpenAICompatibleContentGenerator implements ContentGenerator {
private endpoint: string;
private model: string;
constructor(config: { endpoint: string; model: string }) {
this.endpoint = config.endpoint;
this.model = config.model;
}
async generateContent(
request: GenerateContentParameters,
): Promise<GenerateContentResponse> {
const messages = this.convertToOpenAIMessages(request.contents);
const response = await fetch(`${this.endpoint}/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.model,
messages,
temperature: request.generationConfig?.temperature,
max_tokens: request.generationConfig?.maxOutputTokens,
}),
});
// レスポンス変換処理
return new OpenAIGenerateContentResponse(data);
}
}
3. createContentGeneratorの改造
/packages/core/src/core/contentGenerator.ts
export async function createContentGenerator(
config: ContentGeneratorConfig,
): Promise<ContentGenerator> {
// 常にローカルLLM(Ollama)を使用
return new OpenAICompatibleContentGenerator({
endpoint: process.env.LOCAL_LLM_ENDPOINT || 'http://localhost:11434/v1',
model: process.env.LOCAL_LLM_MODEL || 'gemma3n:latest',
});
}
4. 認証タイプの追加
/packages/core/src/core/contentGenerator.ts
export enum AuthType {
OAuthPersonal = 'oauth-personal',
OAuthServiceAccount = 'oauth-service-account',
GeminiApiKey = 'gemini-api-key',
USE_LOCAL_LLM = 'use-local-llm', // 新規追加
}
5. 認証ダイアログの更新
/packages/cli/src/ui/components/AuthDialog.tsx
const items = [
{ label: 'Local LLM (Ollama)', value: AuthType.USE_LOCAL_LLM },
// 他の認証オプション(表示はするが使わない)
];
6. 認証検証の更新
/packages/cli/src/config/auth.ts
export async function validateAuthMethod(
authMethod: AuthType | undefined,
): Promise<string | null> {
if (authMethod === AuthType.USE_LOCAL_LLM) {
return null; // ローカルLLMは認証不要
}
// 他の認証方法の検証
}
7. ロゴの変更
/packages/cli/src/ui/components/AsciiArt.ts
ASCIIアートをGEMINIからGEMMAに変更:
export const shortAsciiLogo = `
█████████ ██████████ ██████ ██████ ██████ ██████ █████
███░░░░░███░░███░░░░░█░░██████ ██████ ░░██████ ██████ ███░░░███
███ ░░░ ░███ █ ░ ░███░█████░███ ░███░█████░███ ░███ ░███
░███ ░██████ ░███░░███ ░███ ░███░░███ ░███ ░███████████
░███ █████ ░███░░█ ░███ ░░░ ░███ ░███ ░░░ ░███ ░███░░░░░███
░░███ ░░███ ░███ ░ █ ░███ ░███ ░███ ░███ ░███ ░███
░░█████████ ██████████ █████ █████ █████ █████░███ █████
░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░░
`;
実装時の注意点
TypeScriptの型互換性
GeminiのレスポンスフォーマットとOpenAIのフォーマットは異なるため、変換処理が必要
class OpenAIGenerateContentResponse {
constructor(private data: any) {}
get candidates() {
return [{
content: { parts: [{ text: this.data.choices[0].message.content }] },
finishReason: this.mapFinishReason(this.data.choices[0].finish_reason),
}];
}
}
設定ファイルの優先順位
~/.gemini/settings.json
が存在する場合、環境変数より優先されることがあります。完全にローカルLLM専用にする場合は、このファイルを削除するか、適切に設定する必要があります。
使い方
- Ollamaをインストールし、起動
- 必要なモデルをpull:
ollama pull gemma3n:latest
- 改造したGemini CLIを起動
- 認証画面で"Local LLM (Ollama)"を選択
Gemini CLIのアーキテクチャ分析:なぜ改造が容易だったのか
今回の改造がスムーズに実装できた理由は、Gemini CLIが疎結合で拡張を前提とした設計になっているからです。
優れた疎結合設計の特徴
1. インターフェースによる抽象化
// AIプロバイダーを完全に抽象化
export interface ContentGenerator {
generateContent(request: GenerateContentParameters): Promise<GenerateContentResponse>;
generateContentStream(request: GenerateContentParameters): AsyncIterable<GenerateContentResponse>;
countTokens(request: CountTokensParameters): Promise<CountTokensResponse>;
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
}
この設計により、新しいAIプロバイダー(Ollama)の追加が既存コードを変更せずに可能でした。
2. 依存性注入パターン
- Configクラスを通じた一貫した依存性管理
- コンストラクタベースの注入により、テスタブルで柔軟な設計
3. パッケージの明確な分離
packages/
├── core/ # ビジネスロジック、AI連携
└── cli/ # UI、ユーザーインタラクション
- ESLintルールで相対的なクロスパッケージインポートを禁止
- 各パッケージが独立して機能
4. プラグインシステム
- ツールシステム: Tool/BaseToolインターフェースによる統一的な契約
- MCP(Model Context Protocol): 外部ツールの動的な追加が可能
- 認証システム: AuthTypeの列挙により新しい認証方法の追加が容易
5. 設計パターンの活用
-
ファクトリーパターン:
createContentGenerator()
で実装の詳細を隠蔽 - ストラテジーパターン: 異なるAIプロバイダーを同じインターフェースで扱う
- レジストリパターン: ToolRegistryによる動的なツール管理
改造時に恩恵を受けた点
-
ContentGeneratorの追加
- インターフェースを実装するだけで新しいプロバイダーを追加
- 既存のGoogleGenAIContentGeneratorと同じ契約を守るだけ
-
認証方法の追加
- AuthType enumに新しい値を追加
- validateAuthMethodに条件分岐を追加
- 既存の認証フローを壊さない
-
設定の拡張
- 環境変数による設定の上書きが可能
- 設定ファイルの階層的な管理
アーキテクチャの強み
- Open/Closed原則: 拡張に対して開いており、修正に対して閉じている
- 単一責任の原則: 各モジュールが明確な責任を持つ
- 依存関係逆転の原則: 具象クラスではなくインターフェースに依存
まとめ
Gemini CLIの優れたアーキテクチャにより、比較的簡単にローカルLLM対応を追加できました。ContentGeneratorインターフェースによる抽象化と、疎結合な設計が、この種の拡張を容易にしています。
このような設計は、オープンソースプロジェクトとして理想的です。コミュニティが独自の拡張を作りやすく、本体のコードを変更せずに新機能を追加できるからです。
Apache-2.0ライセンスのおかげで、このような改造を自由に行い、共有することができます。ローカルLLMを使いたいが、使いやすいCLIインターフェースが欲しい場合に、この方法は有効な選択肢となるでしょう。
参考リンク
リポジトリ
Discussion