1つのSSEストリーム、7つのLLMプロバイダー
7つの異なるLLMプロバイダーをサポートするNext.jsアプリを構築しました。
OpenAI、Claude、Gemini、Ollama、Mistral、Groq、そしてAzure。
私は一つの厳格なルールを設けました。「ブラウザは、すべてのプロバイダーに対して全く同じコードパスを使用しなければならない」というルールです。
これは困難な作業です。なぜなら、これらのAPIは同一ではないからです。トランスポート方式が異なり、送信されるデータの形式(shape)も異なります。SSEを使用するものもあれば、NDJSONを使用するものもあります。
もしこれらの違いをそのままUIまで持ち込んでしまうと、コードは「if」文の塊で混乱してしまいます。プロバイダーを追加するたびに、フロントエンドの複雑さが増していくことになります。
私は、単一の「コントラクト(規約)」を作成することでこれを解決しました。すべてのプロバイダーは、ブラウザに対して以下の形式でデータを送信する必要があります。
• data: {"delta":"
ブラウザが理解すべきことは、delta、error、[DONE] の3つだけです。
構築方法は以下の通りです。
非同期ジェネレーター(Async Generators)を使用する 各プロバイダーを、プレーンテキストをyield(生成)するジェネレーターとして扱います。これにより、APIの複雑さを隠蔽できます。
ラッパーパターン
createSSEStreamというラッパー関数を作成しました。このラッパーが通信フォーマットを管理します。また、ストリームが必ず終了するように制御します。プロバイダーが途中で失敗した場合でも、ラッパーがエラーと [DONE] シグナルを送信するため、クライアントがハングアップするのを防げます。類似したAPIのグループ化 OpenAI、Mistral、Groq、Azureはすべて同じ形式(方言)を使用しています。私はこれらすべてに対して一つの実装を書きました。これにより、互換性のある新しいプロバイダーの追加は、わずか1行のコードで済むようになりました。
特異なケースへの対応 AnthropicとOllamaは動作が異なります。Anthropicは特定の型付きイベントを使用し、OllamaはNDJSONを使用します。これらにはカスタムパーサーを書きましたが、どちらも最終的には同じラッパーにテキストをyieldします。ブラウザ側はその違いを意識することはありません。
プライバシーとシンプルさ このアプリは「Bring Your Own Key(独自のAPIキーを持ち込む)」モデルを採用しています。
• ユーザーは自身のAPIキーを貼り付けます。 • キーはローカルストレージに保持されます。 • サーバーは純粋なプロキシとして機能します。 • キーがデータベースに保存されることはありません。
このアプローチにより、複雑な認証やシークレット管理の必要性がなくなります。また、アプリのセルフホストも容易になります。
教訓はシンプルです。各統合(インテグレーション)を、「実際に欲しいもの」を生成するジェネレーターとしてモデル化してください。それを一度ラップします。差異はジェネレーターの中に閉じ込め、メインのロジックをクリーンに保つのです。
オプションの学習コミュニティ: https://t.me/GyaanSetuAi