# AAO Gateway モード — 機能概要 (2026-05-20 時点) このドキュメントは「LiteLLM Proxy 代替として AAO に追加された Gateway 機能」が今回の一連の作業でどう変わったかを日本語で解説します。技術詳細の設計 doc は末尾の関連ドキュメントセクションを参照。 --- ## TL;DR (3 行) - **AAO 自身が OpenAI 互換 LLM Gateway として動けるようになりました。** 他の AAO や任意の OpenAI クライアントから `/v1/chat/completions` を叩けます。 - **設定 UI のトグル 1 つで on/off** (同一プロセス内で起動・停止)。別プロセスで動かす運用も従来通り可能 (advanced)。 - 仮想 API キーごとに **月次 token 予算 + RPM レート制限**、Prometheus でリアルタイム監視。Telemetry 外部送信ゼロ。 --- ## なぜこれを作ったか LiteLLM Proxy には次の懸念がありました: 1. **Telemetry**: デフォルトで匿名利用データが外部送信される (opt-out 可だが要設定)。 2. **License 変更リスク**: MIT → BSL/SSPL への変更前例があり、組織で長期運用するには不確実性が高い。 3. **追加依存**: Python サービス + Redis (任意) + DB を、AAO とは別に運用する必要がある。 これらを **「AAO 単一バイナリ + 既存 SQLite + telemetry 完全ゼロ」** で代替するのが本機能の目的です。LiteLLM 固有の高度機能 (sticky routing / canary / cost USD / OpenTelemetry / Slack アラート / multi-region federation 等) は **意図的にスコープ外**にして、シンプルな in-house ゲートウェイに振り切りました。 --- ## どの機能が、どのタイミングで入ったか 時系列に沿って、追加された機能とユーザーへの影響を解説します。 ### Phase 1: Gateway モードの土台 (PR #326) | 項目 | 内容 | |---|---| | 起動方法 | `AAO_MODE=gateway` を環境変数指定すると、worker / scheduler / UI を起動せず、軽量に gateway サーバだけ立つ | | エンドポイント | `/v1/chat/completions` (SSE ストリーミング) + `/v1/models` + `/health` (LiteLLM 互換 JSON) | | 認証 | Bearer Token (config.yaml に static な virtual keys) | | ルーティング | 複数 backend (llama-server / vLLM など) を **least-busy** (進行中リクエスト数が一番少ない backend) で振り分け | | 安全停止 | SIGTERM 時に進行中 SSE を `shutdown_graceful_sec` (デフォルト 30s) 待って drain、`gateway_shutdown` SSE event でクライアントに retry を促す | ### Phase 2a: 仮想キーを DB で管理 + Admin REST API (PR #330) - API キー形式: `sk-aao-` (32 文字、23 bytes エントロピー、SHA-256 hash で DB 保存) - **raw key は発行直後の 1 回だけ表示**、以降は prefix しか見えない (LiteLLM 同様の安全設計) - Admin REST API で発行 / 一覧 / rotate / soft delete (revoked_at) - config.yaml の static key は起動時に自動で DB に移行 - `team` フィールドでテナント分離 ### Phase 2b: 予算とレート制限 + UI 一式 (PR #332) | 機能 | 実装 | |---|---| | 月次 token 予算 | キーごとに `tokens_budget`。UTC 月初に counter リセット。超過した次のリクエストで 429 を返す (post-hoc enforcement) | | RPM レート制限 | キーごとに `rate_limit_rpm`。sliding 60s window、in-memory カウンタ + 30s 周期で DB flush | | 使用量集計 | `gateway_key_usage` テーブルに (key_id, period_start) 単位で tokens_in / tokens_out / requests を記録 | | UI | Settings → Tools → **"Gateway Keys"** タブ。発行・無効化・rotate・月次グラフ | ### Phase 3a: Polish bundle (PR #333) Phase 1-2b で残った INVESTIGATE 8 件をまとめて解消: 1. Orphan key 検出時の警告ログ 2. `Authorization: Bearer ...` の regex を RFC 6750 厳密に 3. 5 秒 LRU の key auth cache (DB 負荷削減) 4. config 由来 key の drift resync (config.yaml を書き換えた時の DB 整合) 5. PATCH で revoked key を編集しようとしたら 409 conflict 6. Dead field cleanup 7. MAX_TRACKED_KEYS LRU eviction (rate limiter) 8. **SSE error の sentinel 化**: `gateway_shutdown` / `gateway_timeout` / `budget_exhausted` / `rate_limited` の 4 種をクライアントが識別可能に ### Phase 3b: Prometheus exporter (PR #335) - **Gateway 11 metrics**: requests_total / tokens_total / backend_busy_slots / key_cache_size / latency histogram など - **Worker 6 metrics**: jobs_total / piece_runs / queue depth など - 全 metric に **per-team label** を載せて、テナント単位の利用量を分離可視化 - `/metrics` は **デフォルトで localhost 限定** (`127.0.0.1` / `::1` allowlist)、外部 Prometheus 用に bearer token opt-in - cardinality 暴走を防ぐため、label を 1 桁オーダーに抑える discipline ### cleanup (PR #337) - AAO の LLM クライアント (`openai-compat.ts`) で gateway sentinel SSE error を parse ### ops (PR #340) - `scripts/gateway.sh start | stop | status | logs` (`.gateway.pid` 管理、`logs/gateway.log`) - `AAO_CONFIG` env で config.yaml path を override 可能 (両モード共通) - `GATEWAY_PORT` env で listen port を override 可能 - DB 共有設計 (worker と gateway は同一 SQLite を WAL で共有可能) を doc に明記 ### Phase 3c: UI 制御 + 同プロセス default ← **本セッションで完了 (PR #341)** ここが今回の最大の変化です。これまで「gateway 専用プロセスを別 port で起動する」モデルだったのを抜本的に変更: | Before (Phase 1-3b) | After (Phase 3c) | |---|---| | `AAO_MODE=gateway` で gateway 専用プロセスを別 port (例: 9877) に起動 | 通常の worker AAO の **同一プロセス・同一 port** で動作 | | Worker UI と gateway を物理的に分離 | UI のトグル 1 つで gateway を mount / unmount | | 各 AAO に gateway 用の追加デプロイが必要 | 既存 AAO を起動して UI から有効化するだけ | #### 新しい設定 UI (Settings → Tools → "Gateway Server") - **Enable Gateway** トグル: チェックで gateway 起動、外して停止 - **Backends list** (フォーム編集): - Endpoint URL (http/https) - Backend ID (任意の文字列、ルーティング識別子) - Max slots (並列処理上限) - API key (backend 側に必要な場合) - 編集 + Save で **hot reload**: 進行中 SSE は `gateway_shutdown` event で drain、新接続から新 config で動作 - **リアルタイム状態 badge**: `running` / `disabled` / `misconfigured` を 3 秒周期で表示 #### 動作モード - **Gateway 有効時**: `/v1/*` paths が gateway sub-app にルーティングされ、`/health` は LiteLLM 互換 JSON を返す - **Gateway 無効時**: `/v1/*` は 404、`/health` は bridge の `{status:'ok'}` (Docker healthcheck 等の既存利用に影響なし) #### Phase 3c で重要だった 8 件のバグ修正 レビューで critical な問題が見つかり、すべて修正済み: 1. **prom-client メトリクス重複登録クラッシュ**: gateway 設定変更 (= bounce) 1 回ごとに `Counter` を新規登録 → 2 回目で throw して bridge プロセスごと死ぬ問題。`WeakMap>` で memoize して解決 2. **BackendStatusRegistry が worker 用の list を見ていた**: 同プロセス mode で gateway も worker と同じ registry を共有していたため、gateway 専用の backend が status 不明 → routing blind / `/health` 空 / metrics 0。Gateway は自前 registry を `gateway.backends[]` 上に build する設計に 3. **`/health` LiteLLM 互換が壊れていた**: bridge の既存 `/health` (`{status:'ok'}`) が mountGateway より先に登録されていて、LiteLLM 互換 JSON が返らない問題。`classifyGatewayPath` を 3 値 (`gateway-only` / `gateway-when-enabled` / `false`) に拡張し、Express middleware の登録順序を修正 4. **transition 中の config が dropped される**: `state === 'starting' | 'stopping'` 中に新規 config 適用が落ちる問題。`pendingConfig` queue + mutex chain replay で対応 (実用上は mutex serialize で到達不能だが、防御として実装) 5. **stop() が state を misconfigured のまま残す**: 公開 stop API が state を `disabled` にリセットしない問題 6. **configsEquivalent の比較が不安定**: `JSON.stringify` の key 順依存で偽 bounce が起きる問題。stable stringify に 7. **listenPort が env 依存**: 実 listen port と表示 port が乖離する可能性。`CoreServerOptions.listenPort` で配線 8. **`api_key` 入力時に `${VAR}` env 参照が黙って literal 保存される**: amber warning text を form に表示 これらは round-1 / round-2 の adversarial review で発見・修正。テストでは隠蔽されやすいバグ (例: 1 番は `beforeEach` で fresh Registry を使うと検出できない) を含むため、shared-registry pattern の test を追加で書いて検証しています。 --- ## どう使うか (運用ガイド) ### パターン A: 単一 AAO で gateway を有効化 (Phase 3c 以降の推奨) ``` 1. scripts/server.sh start で AAO を通常起動 2. UI に admin としてログイン 3. Settings → Tools → "Gateway Server" を開く 4. "Enable Gateway" にチェック → Backends list を入力 → Save 5. Settings → Tools → "Gateway Keys" で API キーを発行 (raw key は発行直後のみ表示) 6. 他の AAO や OpenAI クライアントから: base_url: http://this-aao:9876/v1 api_key: sk-aao-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ### パターン B: Gateway 専用サーバとしてデプロイ (advanced) GPU サーバが多い、または worker UI を運用したくない場合: ```bash AAO_MODE=gateway scripts/gateway.sh start # AAO_CONFIG=/etc/aao/gateway.yaml で別 config を指定可能 # worker mode と同じ DB を共有可能 (キー管理が両モードで一貫) ``` ### パターン C: 既存 LiteLLM Proxy からの乗り換え 互換性のポイント: - Endpoint path: `/v1/chat/completions` ・ `/v1/models` ・ `/health` 同じ - Response header `x-litellm-model-id` をそのまま発行 (vendor-neutral な `x-aao-backend-id` も同値で同時発行) - Key format: `sk-aao-*` (LiteLLM の `sk-*` から prefix を変更、長さ・エントロピー同等) - `/health` の JSON shape: `{healthy_endpoints, unhealthy_endpoints, healthy_count, unhealthy_count}` で LiteLLM 互換 クライアント側の `parseLiteLLMHealth` 等の既存コードは無修正で動作します。 --- ## 監視 `/metrics` で Prometheus 形式メトリクスを取得: ``` # Gateway 系 aao_gateway_requests_total{backend="llm-a",team="ops",status="ok"} 142 aao_gateway_tokens_total{backend="llm-a",team="ops",direction="out"} 95821 aao_gateway_backend_busy_slots{backend="llm-a"} 2 # Worker 系 (same-process mode 時に同じ endpoint から) aao_worker_jobs_total{piece="chat",status="succeeded"} 38 ``` 外部 Prometheus からスクレイプする場合は config で `provider.metrics.bearer_token` を設定し、IP allowlist を緩和。 --- ## 意図的にやっていない機能 (実需確認まで保留) | 機能 | 保留理由 | |---|---| | Sticky routing (cache hit 最適化) | 1 backend 構成 + cache hit 率 実測無しでは ROI 不明 | | Canary routing (model rollout) | 現状 model A→B 切り替え予定なし | | Token → USD コスト換算 | tokens 数で十分か admin の要望次第 | | Audit log テーブル | compliance 要件無し時点では不要 | | Pre-reserve budget (並列 burst) | 実際に N×max_tokens overshoot が観測されたら検討 | | OpenTelemetry trace | 単一 org deploy では ROI 低い | | Slack / Email アラート | Prometheus Alertmanager で代用想定 | | Multi-AAO federation | Prometheus federation で代用想定 | --- ## 次のステップ - Backend ルーティングの動作 (least-busy の精度) - SSE drain の挙動 (大量同時接続 + graceful shutdown) - Budget / RPM の境界値 (オフバイワン無いか) - Prometheus metrics の cardinality / scrape duration - UI hot reload の race condition - LiteLLM 互換性 (既存 client コードが無修正で動くか) を検証。dogfooding で観測した問題から Phase 4 のスコープを決めます。 --- ## 関連ドキュメント - In-product help: `ui/src/content/help/11-llm-gateway.md` - INVESTIGATE backlog (open follow-up issues): Gitea issue #338 --- ## マージ済み PR 一覧 | Phase | PR | merge commit | 日付 | |---|---|---|---| | 1 | #326 | `178baa9` | 2026-05-18 | | 2a | #330 | `78a796d` | 2026-05-18 | | 2b | #332 | `b0569c7` | 2026-05-19 | | 3a | #333 | `30e9d78` | 2026-05-19 | | 3b | #335 | `9efc60f` | 2026-05-19 | | cleanup | #337 | `bcfdd41` | 2026-05-20 | | ops | #340 | `13bd3cd` | 2026-05-20 | | **3c** | **#341** | **`561ff24`** | **2026-05-20 (本セッション)** | 累計コード追加: 約 12,500 行 (テスト含む)、テスト件数 2720 (ベースライン 2707 から +13)。