maestro/docs/aao-gateway-overview.md
2026-06-03 05:08:00 +00:00

240 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-<base62-32>` (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<Registry, Map<prefix, GatewayMetrics>>` で 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)。