240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
# 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)。
|