コンテンツにスキップ
OmniRoute source

open-sse Architecture (日本語)

ワークスペースパッケージを分離する理由

Section titled “ワークスペースパッケージを分離する理由”

open-sse/ は、以下の理由により OmniRoute モノレポ内の独立したワークスペースとなっています。

  1. 再利用性 — open-sse は npm で @omniroute/open-sse として公開されているため、他のプロジェクトでも独立して使用できます
  2. 明確な境界 — ストリーミングエンジンが OmniRoute 固有の UI/DB レイヤーから分離されています
  3. パフォーマンス — エンジンは Next.js に依存しないため、CLI/サーバーレス環境でのコールドスタートが高速になります
  4. バージョニング — open-sse は独自のサイクルでリリースできます
package.json
"workspaces": ["open-sse"]

open-sse/
├── index.ts # 公開エントリーポイント
├── types.d.ts # 公開型エクスポート
├── package.json # @omniroute/open-sse
├── config/ # プロバイダー設定、定数、レジストリ
├── executors/ # プロバイダー別 HTTP エグゼキューター(67個 + base.ts/index.ts)
├── handlers/ # リクエストハンドラー(chatCore、responses など)
├── lib/ # 内部ユーティリティ
├── mcp-server/ # Model Context Protocol サーバー
├── services/ # 約298個のサービスモジュール
├── transformer/ # Responses API 形式トランスフォーマー
├── translator/ # 形式変換(OpenAI ↔ Claude ↔ Gemini)
└── utils/ # 共有ユーティリティ(ロギング、エラー、ストリームなど)

| ディレクトリ | ファイル数 | 目的 | | executors/ | 167 | プロバイダー別 HTTP エグゼキューター(DefaultExecutor ファクトリーにより統一) | | handlers/ | 157 | リクエストのエントリーポイント(chatCore、responses、embeddings) | | services/ | 約536 | ルーティング、キャッシュ、レート制限、更新など | | translator/ | 56 | 形式変換(OpenAI ↔ Claude ↔ Gemini) | | mcp-server/ | 44 | MCP ツールおよびトランスポート | | utils/ | 約108 | 横断的ユーティリティ(ロギング、エラー、ストリーム) | | config/ | 約339 | プロバイダー設定、定数、レジストリ |


すべての LLM リクエストは、5段階のパイプラインを通過します。

┌──────────────┐
HTTP リクエスト │ 1. ルーティング │ コンボ解決、モデル選択
(Next.js ルート) └──────┬───────┘
│
▼
┌──────────────┐
│ 2. 変換 │ 形式変換(OpenAI ↔ Claude ↔ Gemini)
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. 実行 │ プロバイダーエグゼキューター、HTTP、再試行、ブレーカー
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. ストリーム │ SSE 変換、バックプレッシャー
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. 記録 │ 使用量追跡、呼び出しログ、エラー分類
└──────┬───────┘
│
▼
HTTP レスポンス(SSE または JSON)

ステージ1: ルーティング(services/combo.ts)

Section titled “ステージ1: ルーティング(services/combo.ts)”

エントリーポイント: services/combo.ts の handleComboChat()

リクエストを具体的な (provider, model, account, credentials) タプルへ解決します。

  • ID でコンボを検索します(または auto/* モデル用の仮想コンボを構築します)
  • ルーティング戦略(優先度、重み付け、ラウンドロビンなど)を適用します
  • 正常でないプロバイダーを除外します(サーキットブレーカー)
  • 次に利用可能なターゲットを選択します

auto/* モデルの場合、このステージではさらに以下を行います。

  • 16要素スコアリングアルゴリズム(services/autoCombo/)を実行します
  • 正常性、コスト、レイテンシなどに基づいて provider+model の組み合わせを選択します

送信元の形式(例: OpenAI)が送信先の形式(例: Claude)と異なる場合、リクエストは変換されます。

  • システムプロンプト → システムメッセージ
  • ツール定義 → プロバイダー固有のツール形式
  • 推論/思考パラメーター → プロバイダー固有の同等パラメーター
  • メッセージロールの正規化(OpenAI 以外では developer → system)

translator/index.ts は以下を公開します。

translateRequest(body, sourceFormat, targetFormat): TranslatedRequest
needsTranslation(source, target): boolean

エントリーポイント: getExecutor(providerId).execute(request, options)

すべてのプロバイダーは、getExecutor() ファクトリーのフォールバックを介して DefaultExecutor(executors/default.ts)を使用します。エグゼキューターは以下を行います。

  • アップストリーム URL を構築します(buildUrl())
  • プロバイダー固有のヘッダーを追加します(buildHeaders())
  • リクエストボディを変換します(transformRequest())
  • 再試行と指数バックオフを使用して HTTP リクエストを送信します
  • 必要に応じて認証を更新します(OAuth プロバイダー)

すべてのエグゼキューターは BaseExecutor(executors/base.ts、1170 LOC)を継承し、以下の機能を利用します。

  • 共通の再試行ロジック
  • プロキシ連携
  • サーキットブレーカー連携
  • 使用量記録フック

ステージ4: ストリーム(utils/stream.ts)

Section titled “ステージ4: ストリーム(utils/stream.ts)”

ストリーミングレスポンスの場合、エグゼキューターは ReadableStream を返します。ハンドラーは以下を行います。

  • SSE トランスフォーム(createSSETransformStreamWithLogger)を通します
  • 切断された接続を検出するためにハートビート ping を適用します
  • クライアントの切断を適切に処理します(pipeWithDisconnect)
  • 非ストリーミングクライアント向けに SSE → JSON 変換を行います

非ストリーミングレスポンスの場合、エグゼキューターは解析済みの JSON オブジェクトを返し、そのまま渡されます。

ステージ5: 記録(services/usage.ts)

Section titled “ステージ5: 記録(services/usage.ts)”

レスポンス後(成功・失敗を問わず)、使用状況が記録されます。

  • レスポンスから取得した prompt_tokens、completion_tokens、cached_tokens
  • 料金データに基づいて算出された cost_usd
  • 失敗した場合の latency_ms、status、error_class
  • usage_history テーブルに永続化

コールログのアーティファクト(有効な場合)は ${DATA_DIR}/call_logs/ に書き込まれます。


メインのリクエストハンドラーです。非常に大きなファイルですが、構造は明確です。

// chatCore.tsの擬似構造
export async function handleChat(request: NextRequest) {
// 1. 認証 + CORS
await authenticateRequest(request);
applyCorsHeaders(response);
// 2. ボディの検証
const body = await parseRequestBody(request);
// 3. フォーマットの検出 + 変換
const sourceFormat = detectFormat(request);
const targetFormat = getTargetFormat(providerId);
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}
// 4. コンボルーティング
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
const result = await executeOnTarget(target, body);
await recordUsage(result);
return result;
} catch (err) {
// 次のターゲットへ進む
}
}
// 5. 緊急フォールバック
return await emergencyFallback(body);
}

1つの巨大な関数であるにもかかわらず、5段階のパイプラインに対応するコメント付きのセクションに整理されています。

コンボを順序付きターゲットへ解決するルーティングエンジンです。

services/combo.ts
export async function handleComboChat(body, comboId): Promise<ChatResult> {
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
return await handleSingleModel(target, body);
} catch (err) {
log.warn("target failed, trying next", { target, err });
}
}
throw new ComboExhaustedError("All targets failed");
}

19種類のルーティング戦略をサポートしています(src/shared/constants/routingStrategies.tsを参照)。

戦略 動作
priority 先頭ターゲットを優先する順序付きリスト
weighted ターゲットごとの重みに基づく確率的選択
round-robin ターゲットを順番に巡回
context-relay ターゲット間でコンテキストを引き継ぐ
fill-first 次へ移る前にクォータを使い切る
p2c 2択のうち良い方を選択
random 一様ランダム
least-used 最近の使用回数が最も少ないものを選択
cost-optimized 正常なターゲットのうち最も安価なものを優先
reset-aware プロバイダーのリセット期間を考慮
reset-window リセット期間に基づくルーティング
headroom 残りのクォータ余裕が最も大きいものを優先
strict-random 完全な一様ランダム(品質による重み付けなし)
auto 16要素のスコアリングを使用(autoCombo/)
lkgp 最後に正常動作した既知のプロバイダーを優先
context-optimized 長いコンテキストのリクエストに最適なものを選択
fusion 複数のパネルへ並列に展開し、審判役を通じて統合(fusion.ts)

107個すべてのエグゼキューターが継承する抽象エグゼキューターです。以下が含まれます。

  • buildUrl() — デフォルトのURL構築(カスタム動作が必要な場合はサブクラスでオーバーライド)
  • buildHeaders() — デフォルトのヘッダー(認証、content-type)
  • transformRequest() — デフォルトではそのまま渡す
  • execute() — 再試行、バックオフ、ブレーカーを備えたメインのHTTPループ
open-sse/executors/default.ts
export class DefaultExecutor extends BaseExecutor {
// OpenAI/Anthropic互換のすべてのプロバイダーを処理
// プロバイダーは設定(URL、認証、ヘッダー)を登録するが、エグゼキューターのロジックは共有する
}

プロバイダー固有の動作(認証ヘッダー、ベースURL、バージョンヘッダー)は、個別のエグゼキュータークラスではなく、プロバイダーレジストリを介して設定されます。

---
## サービス(117モジュール)
サービスは、ハンドラーによって構成される、**特定の目的に特化した単一用途のモジュール**です。主なカテゴリーは次のとおりです。
### ルーティングとコンボ
- `combo.ts` — コンボルーティングされたリクエストのエントリーポイント
- `services/autoCombo/` — 16要素のスコアリング、8つの自動ルーティング戦略
- `wildcardRouter.ts` — ワイルドカードルート(`gpt-*`)の照合
- `modelFamilyFallback.ts` — T5ファミリー内のフォールバック
### レート制限とクォータ
- `rateLimitManager.ts` — キーとプロバイダーごとのトークンバケット
- `usage.ts` — 使用量の記録
- `quotaCache.ts` — インメモリのクォータスナップショット
### アカウントとトークン
- `tokenRefresh.ts` — 401発生時のOAuth更新
- `accountFallback.ts` — 代替アカウントへの切り替え
- `sessionManager.ts` — 複数ターンのセッション状態
### インテリジェンス
- `intentClassifier.ts` — リクエストの意図を分類
- `taskAwareRouter.ts` — タスクタイプ別にルーティング
- `thinkingBudget.ts` — 思考トークンを割り当て
- `contextManager.ts` — ルーティングコンテキストを注入
### レジリエンス
- `resilience.ts` — 再試行、バックオフ、サーキットブレーカーのオーケストレーション
- `emergencyFallback.ts` — 最終手段のフォールバック
- `modelDeprecation.ts` — 後継モデルへの自動ルーティング
### 状態
- `signatureCache.ts` — リクエストシグネチャによる重複排除
- `volumeDetector.ts` — 負荷制限
- `contextHandoff.ts` — セッションのシリアライズ
### 圧縮
- `compression/`(サブディレクトリ)— 完全な圧縮パイプライン
- エンジン、ルールパック、アダプターを網羅する39ファイル
### スキル
- ([SKILLS.md](./SKILLS.md)で説明)
### メモリ
- ([MEMORY.md](./MEMORY.md)で説明)
---
## エグゼキューター(75以上のファイル)
プロバイダーごとに1ファイルあります。すべてが`BaseExecutor`を拡張し、異なる部分をオーバーライドします。
### 共通パターン
プロバイダーは`getExecutor(providerId)`を介して解決され、設定済みのエグゼキューターが返されます。OpenAI/Anthropic互換プロバイダーは`DefaultExecutor`(`executors/default.ts`)を使用します。プロバイダー固有の動作(ベースURL、認証ヘッダー、APIバージョン)は`open-sse/config/providers/`で設定され、リクエストボディの変換は`open-sse/translator/`で処理されます。
**カスタムURL**はプロバイダー設定で指定します。
```ts
// open-sse/config/providers/内のプロバイダー設定
export default {
id: "together",
baseURL: "https://api.together.xyz/v1/chat/completions",
}

カスタム認証は、プロバイダーレジストリの認証設定(APIキー、OAuth、ヘッダープロファイル)を通じて処理されます。

カスタムリクエストボディの変換(たとえば、Anthropicによるsystemとmessagesの分離)は、open-sse/translator/でプロバイダーごとに登録されます。

### エグゼキューターファクトリ
`executors/index.ts`は`getExecutor(providerId)`をエクスポートします。
```ts
import { getExecutor } from "@omniroute/open-sse/executors";
const executor = getExecutor("anthropic");
const result = await executor.execute({
model: "claude-sonnet-4-5",
messages: [...],
});

解決はExecutorRegistry(executors/registry.ts)を介して行われます。特殊化された各エグゼキューターは、executors/index.tsの組み込みテーブルで宣言され、モジュールのロード時にregisterExecutor(alias, instance)を介して登録されます。getExecutor()はレジストリを参照し、特殊化されたエントリが存在しないプロバイダーについては、メモ化されたDefaultExecutorへフォールバックします。alias → executorの完全なマッピングは、ゴールデンテストtests/unit/executor-map-golden.test.tsで規定されています。


3つの形式(OpenAI、Anthropic、Gemini)および新しい Responses API の間で変換します。

import { needsTranslation, translateRequest } from "@omniroute/open-sse/translator";
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}

一般的な変換:

  • OpenAI → Anthropic:独立した system フィールド、x-api-key ヘッダー
  • OpenAI → Gemini:messages の代わりに contents、systemInstruction
  • OpenAI → Responses API:input 配列、previous_response_id による状態管理
  • developer ロール → OpenAI 以外では system
  • system ロール → GLM/ERNIE では最初のユーザーメッセージに統合
  • json_schema → Gemini の responseMimeType + responseSchema
  • tools → プロバイダー固有のツール形式
  • Thinking パラメーター(o1、Claude)→ プロバイダー固有の同等機能

open-sse/mcp-server/ は Model Context Protocol サーバーを実装しています:

  • 110個のツール(プロバイダー管理、コンボ、メモリ、キャッシュ、圧縮、プロキシ、スキル、ゲーミフィケーション、プラグイン、Notion、Obsidian、ローカルコーパス)
  • 3つのトランスポート:stdio、SSE、Streamable HTTP
  • きめ細かな認可のための 33個のスコープ

ツールは open-sse/mcp-server/tools/ 内の独立したファイルとして登録され、それぞれが名前、スキーマ、ハンドラー、スコープをエクスポートします:

open-sse/mcp-server/tools/getHealth.ts
import { z } from "zod";
export default {
name: "omniroute_get_health",
description: "Get system health snapshot",
scope: "read:health",
inputSchema: z.object({}),
handler: async (_args, ctx) => {
return await getSystemHealth();
},
};
// stdio(CLIでの使用)
startMcpStdio(server);
// SSE(HTTPベースのストリーミング)
startMcpSse(server, port);
// Streamable HTTP(最新のMCP)
startMcpStreamable(server, port);

すべてのツール呼び出しはスコープチェック(open-sse/mcp-server/auth/)を通過します:

if (!hasScope(apiKey, "providers:read")) {
throw new Error("Insufficient scope");
}

open-sse/transformer/ は Chat Completions 形式と Responses API 形式の間で変換します。

トランスフォーマーを分離する理由

Section titled “トランスフォーマーを分離する理由”

Responses API は、ステートフルな会話(previous_response_id)に対応した OpenAI の新しい形式です。クライアントが Responses リクエストを送信すると、OmniRoute は次の処理を行います:

  1. 内部で Responses → Chat Completions に変換
  2. プロバイダー(Chat Completions をサポートする任意のプロバイダー)に送信
  3. レスポンスを Responses 形式に再変換
  4. 変換されたレスポンスをクライアントにストリーミング

トランスフォーマー(transformer/responsesTransformer.ts)は以下を提供します:

createResponsesApiTransformStream(): TransformStream

これは以下を処理します:

  • response.output_item.added イベント
  • response.output_text.delta イベント
  • response.completed イベント
  • ツール呼び出しのマッピング(function_call ↔ tool_calls)

open-sse/config/ は設定レイヤーを保持します:

ファイル 目的
providerRegistry.ts 352プロバイダーのカタログを対象としたチャットモデルレジストリ
providerModels.ts モデルエイリアス、形式マッピング
constants.ts タイムアウト、制限、ステータスコード
defaultThinkingSignature.ts デフォルトのClaude Thinkingシグネチャ
modelStrip.ts(services内) プロバイダーごとのフィールド除去

プロバイダーレジストリのスキーマ

Section titled “プロバイダーレジストリのスキーマ”
interface ProviderConfig {
id: string;
name: string;
baseUrl: string;
authType: "bearer" | "api-key" | "oauth" | "cookie";
executorClass: string;
defaultModel: string;
capabilities: ProviderCapabilities;
models: ModelDefinition[];
}

モジュール読み込み時の Zod 検証により、すべてのプロバイダー設定が有効であることが保証されます。


ルーティングエンジンには厳格なパフォーマンス予算があります:

操作 目標 測定条件
コンボ解決 <10ms 50ターゲットの場合
レート制限チェック <1ms インメモリトークンバケット
モデルファミリーのフォールバック <5ms キャッシュ済みのファミリー定義
リクエストルーティングのディスパッチ <2ms ホットパス
ルーティングのホットパスでブロッキングI/Oを行わない — すべて非同期

❌ combo.ts内での同期DB呼び出し — 事前計算してキャッシュする ❌ ハンドラー内の再試行ロジック — レジリエンスサービスのretry()を使用する ❌ プロバイダー設定への直接アクセス — providerRegistryのゲッターを使用する ❌ ハードコードされたフォールバックチェーン — modelFamilyFallback.tsで定義する ❌ 並行リクエスト間での状態変更 — リクエストスコープのコンテキストのみを使用する


  1. 責務を明確に限定したopen-sse/services/[serviceName].tsを作成する
  2. メインハンドラー関数と必要な定数をエクスポートする
  3. tests/unit/services/[serviceName].test.mjsにユニットテストを追加する
  4. ルーティング関連の場合は、handlers/chatCore.tsのリクエストパイプラインに統合する
  5. サービスがターゲット選択に影響する場合は、combo.tsのルーティングロジックを更新する
  6. このファイルに文書化する

新しいエグゼキューターの追加

Section titled “新しいエグゼキューターの追加”
  1. BaseExecutorを継承するopen-sse/executors/[provider].tsを作成する
  2. config/providerRegistry.tsに登録する
  3. executors/index.tsのファクトリーに追加する
  4. エグゼキューターのユニットテストを追加する
  5. docs/architecture/ARCHITECTURE.mdに文書化する
  1. open-sse/mcp-server/tools/[category]Tools.tsを作成または更新する
  2. 入力用のZodスキーマを定義する
  3. mcp-server/index.tsにツールを登録する
  4. mcp-server/auth/のスコープマトリクスに追加する
  5. ユニットテストを追加する


OmniRoute ソースコード (a58000c7685f)

HagiCode

HagiCode は構造化ワークフロー、マルチエージェント実行、Hero Dungeon ビューを備えたエージェント型コーディングワークスペースです。

よりスマートで速く、楽しいエージェント型ワークフローで、使いやすいソフトウェアを形にします。

HagiCode ライトテーマのメイン画面
  • Smart構造化ワークフローは意図をアイデアから変更のリリースまで実行可能な道筋にします。
  • Efficientマルチエージェントのワークフローで調査、実装、レビューを並行して進めます。
  • FunHero Dungeon により長時間のコーディングを視覚的で協力的な体験にします。
HagiCode を見る