# 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)。
