アーキテクチャ

SchneeAI Gateway のフェイルオーバー設計

レジリエンスの3層 — same-provider retry (出荷済み)、cross-provider failover (モデル化済み・未統合)、canary failover (将来)。プロバイダー障害時に実際何が起きるか、あえて抑制した設計判断とともに。

SchneeAI2026-07-1616 分読了

「OpenAI が障害を起こしたとき何が起きるか?」は、design partner が最初に聞く質問の一つです。正当な質問で、世の “AI Gateway” ランディングページの多くは1行の約束 — シームレスなプロバイダー横断フェイルオーバー — と、プロバイダーのロゴ間に矢印を引いた図で答えます。

本稿はその長い版です。プロバイダーのレジリエンスを3層に分け、どの層が今日出荷されていて、どの層が domain code にはあるが request path に未統合で、どの層がロードマップ上かを明示します。その上で、ほとんどのベンダーが day one で出荷すると主張する部分を何故あえて抑制したかを説明します。

短い版: same-provider retry on 5xx / 429 は本番で動いています。cross-provider failover は internal/core/routing/ に domain model として実装されていますが、chat completion path からはまだ呼ばれていません。理由は技術ではなく (統合は数日の作業です)、audit / billing / cost predictability に関わることであり、請求で誰も驚かない形で出したいからです。

1. レジリエンスの3層

「フェイルオーバー」がチームで意味するものを3つに分けると分かりやすいです:

  • プロバイダー内 retry — 上流が 5xx / 429 を返した、または network が drop した。同じプロバイダーの同じ model に対して同じ呼び出しを、exponential backoff 付きで再送する。現実の transient 障害の大半はこれで拾えます。
  • プロバイダー横断 failover — primary プロバイダーが劣化している (5xx 率上昇、地域障害、発表済みインシデント)。リクエストを別プロバイダーの同等 model へ再ルーティングする。呼び出し元は回答を得るが、model は異なりうる。
  • カナリア / model migration failover — 新 model がリリースされ、挙動が変わり、code deploy 無しに一部を即前版へ戻したい。provider レジリエンスより release engineering に近いですが、同じ会話に登場します。

これらは積層します。本稿は前の2層に集中し、3層目は完全性のために言及します (さらに先の構想です)。

2. 今日出荷されているもの

全プロバイダー呼び出しは LiteLLM を通り、Go layer が coreerrors.Retryable() で呼び出しを wrap します。retry 規則は単純です:

  • HTTP status >= 500 または 429 で retry。
  • Exponential backoff + jitter (base 1秒、上限 60秒、jitter 約25%)。
  • プロバイダーが Retry-After を送れば従う。
  • 同じプロバイダー、同じ model — retry は同じ endpoint への新規 POST。
  • retry 予算はデフォルトで最大5回。

retry 予算を使い切ると、Gateway は呼び出し元へ構造化エラーを返します: 502 と code: provider_failed (全試行でプロバイダーがエラーを返した) または 504 と code: provider_timeout (timeout 予算を超えた)。両者とも、retry semantics を含めて Error Handling に文書化されています。

これで一般的なケース — プロバイダーの一時的な 503、短い rate-limit の flicker、network の half-open connection — は処理できます。処理できないのは、プロバイダーが1時間完全 down するようなケースで、そのためには第2層が必要です。

ここでの原則は、全故障モードに適用するものと同じ: 明示的であること。最終的に retry が静かに成功するのは問題ないですが、静かに諦めて部分 response を 200 で返すのは問題です。リクエストを serve できないなら、分岐できる code で伝えます。

3. 構築済みだが未統合の domain model

internal/core/routing/routing.go は、第2層のための完全な domain model です:

type RoutingPolicy struct {
    PrimaryProvider  string   // 例: "openai"
    PrimaryModel     string   // 例: "gpt-4o-mini"
    FallbackProvider string   // 例: "anthropic"
    FallbackModel    string   // 例: "claude-haiku-4-5"
    Strategy         Strategy // priority | cost | latency
}

type ProviderHealth struct {
    Provider     string
    Healthy      bool
    LatencyMs    int64
    CostMicroUSD int64
}

ResolveModel は policy を歩きます: primary が healthy なら primary を、そうでなければ secondary へ fallback、いずれも不可なら strategy (priority 順、cost 順、latency 順) で healthy なものから最良を選ぶ。3つの strategy (StrategyPriority / StrategyCost / StrategyLatency) が domain に出荷されています。

今日起きていないこと: chat completion handler (internal/delivery/http/ginserver/handler/openai_compat.go:193) は h.catUC.ResolveModel(ctx, req.Model) を呼び、これは catalog.ResolveModel — model catalog への slug lookup です。単一の model を返します。RoutingUsecase は DI container で instantiate されます (internal/app/gateway/app.go:197) が、OpenAICompatHandler には渡されていません。domain code は存在し、request path はそれを呼んでいません。

何故 domain を先に作り、統合を後にしたか? chat handler が RoutingUsecase を呼ぶようになると、caller visible な全挙動 — resp.model、audit record、billing、PII scan 結果 — を routing 決定と一致させなければならないからです。先に domain を作ることで、caller へ露出する前に不変条件を確定できました。統合は簡単な部で、caller との契約が難しい部です。

4. 何故抑制したか

プロバイダー横断 failover は、多くの gateway ベンダーが過剰に主張し、不足して届ける部分です。我々が出荷していない正直な理由:

ヘルスチェック。 プロバイダーが unhealthy と知ること自体が設計問題です。能動 probe (定期 synthesize request) はプロバイダーへ負荷を加え、実際の customer traffic を反映しない可能性があります。受動観測 (実際の request の error rate と latency の EWMA) はより正確ですが遅れ — EWMA が閾値を越える頃には、実際の顧客は既に失敗を見ています。両方が必要で、能動 signal で circuit breaker を開き、受動 signal で閉じる形になります。

Audit の含意。 リクエストが claude-haiku-4-5 から gpt-4o-mini へ failover するとき、2つのプロバイダーの data policy が同じ論理リクエストに適用されます。data residency の理由で Anthropic を選んだ EU の顧客は、単一の failover リクエストであっても prompt が OpenAI のインフラを通ることを望まないかもしれません。failover は policy を考慮する必要があり、routing policy はどのテナントがどのプロバイダーへ failover できるかを知る必要があります。

PII scanning。 pre-call の PII scan は primary プロバイダーの policy に対して走ります。call が別プロバイダーへ failover した場合、fallback 側の policy で再 scan するのか? 今日 scanner は1セットの finding を生成しますが、failover は request と共に移動する cache 済み結果か、新しい境界での再 scan のいずれかを必要とします。

Cost predictability。 Gemini Flash から Claude Opus への黙示的 failover は、token 単価で概ね 50× の cost surprise です。design partner は部分的には予測可能な請求を求めて SchneeAI を評価しています。一時的な 5xx のたびに自動的に model tier を上げる failover は、1回の billing cycle でその信頼を焼き尽くします。

Idempotency。 クライアントが Idempotency-Key: abc123 を送ります。primary プロバイダーが stream 中に失敗。fallback へ切り替えて成功。これは課金対象 interaction が1つか2つか? 今日の答えは単純 — 1 interaction、1 provider。failover は答えを興味深くし、興味深い billing は悪い billing です。

至った結論: retry を先に出荷する (same provider、same model、same cost なので安全)。failover を次に、routing policy による明示的 opt-in、fallback が primary の N× を超えたら拒否する cost ceiling、caller に failover を観測させる response extension 付きで。

5. 出荷のために必要なもの

依存順に並べた具体チェックリスト:

  1. chat handler へ RoutingUsecase を統合。 catalogUCroutingUC で置き換えるか、chain する (catalog が alias を解決し、routing が policy と health を元に provider を決定)。
  2. provider health infrastructure。 プロバイダー毎の in-process circuit breaker。実際の request 成果による受動 EWMA で駆動し、設定可能な schedule での能動 probe を option で。
  3. policy を考慮した failover eligibility。 routing policy は、テナントがどのプロバイダーへ failover できるかを知る必要がある (data residency、PII policy、契約上の除外)。
  4. cost ceiling。 routing policy の field (max_fallback_cost_multiplier: 5 等) で、fallback の token 単価が primary の5倍を超えたら拒否。拒否は構造化エラーで、primary への黙示的 retry ではない。
  5. response extension。 failover 発火時、response は schneeai.fallback_reasonschneeai.fallback_from_modelschneeai.fallback_to_model を携え、caller が観測・audit 可能に。resp.model は今日 resolved な model を示しており、failover は format を拡張します。
  6. audit record の拡張。 audit log は request された model だけでなく、実際に serve した provider を記録。
  7. idempotency 規則。 同じ Idempotency-Key は、どの provider が serve しても1つの課金 interaction。reconciliation worker は既に retry に由来する二重 reservation ケースを扱っており、failover はその仕組みを再利用します。

いずれも研究問題ではなく、engineering work です — 数週間、feature flag の後ろ、少数の design partner が canary として opt-in する形で。

6. 出荷時に何が見えるか

caller の視点では、failover は opt-in で観測可能です。default 挙動は変わらず: routing policy で fallback を設定しなければ、call は primary プロバイダーへ行き、そこに留まり、一時的な失敗に対しては今日と同じように retry します。

opt-in した場合、response shape が何が起きたかを伝えます:

{
  "model": "auto→gpt-4o-mini (fallback: claude-haiku-4-5)",
  "schneeai": {
    "fallback_reason": "primary_provider_5xx",
    "fallback_from": "claude-haiku-4-5",
    "fallback_to": "gpt-4o-mini"
  },
  "request_id": "req_01HQ..."
}

model field は既存の semantics を保ちます — 実際に出力を生成した model を伝えます。extension object は failover を programmatic に検出可能にし、dashboard や alerting で追跡できます。

audit log は request された provider/model だけでなく実際のものを記録します。cost dashboard は service / feature 毎に fallback 分布を分離し、routing policy が想定より頻繁に発火していないかを確認できます。

7. ロードマップと準備

failover 向けの公開 phase spec はまだありません — domain model は存在しますが、統合作業は他の優先事項 ("model": "auto" router 本体、prompt-management UX、GA 準備) と競合しています。design partner の皆様から cost ceiling と policy eligibility の default について input を頂いた上で、将来の phase で出荷する予定です。

今日できる準備:

  • 評価期間中は model を固定。 SchneeAI を direct provider 呼び出しと比較 benchmark するなら、両側で同じ model を pin して apples-to-apples で。router は今日まだ面白いことをしていません。
  • provider_failed / provider_timeout rate を監視。 プロバイダーが今日どれだけ hard-fail しているかを示します。その数値が、failover が実際に catch する上限です。
  • 制約を教えてください。 data residency で failover 先が制限される、予算天井で fallback の费用上限を設けたい等、まさにその input が policy field の default を正しく設定するために必要です。[email protected] まで。

正直なサマリ

短い版を求める読者へ:

  • 今日出荷済み: same-provider retry on HTTP 5xx / 429、exponential backoff + Retry-After honor。retry 予算を使い切ったときの明示的失敗モード (provider_timeoutprovider_failed)。
  • モデル化済み・未統合: primary/fallback の provider と model を持つ RoutingPolicyProviderHealth tracking、3つの routing strategy (prioritycostlatency)。domain code は完全だが、chat completion path は未呼び出し。
  • ロードマップ上: cross-provider failover の統合。明示的 opt-in、cost ceiling、policy を考慮した eligibility、response extension、audit 拡張付き。
  • v1 では計画外: 自動 provider-quality scoring、ML-based routing。今日の router は policy 駆動で、opaque にする計画はありません。

ベンダーが「プロバイダー障害をシームレスに扱う」と言ったら、failover 発火時の response shape、audit log が実際の provider を記録するか、cost ceiling があるかを聞いてください。答えは marketing copy より大抵興味深いです。

retry と routing が request path のどこに位置するかの全体は、SchneeAI Gateway の中身 を。本稿が参照する error code は、エラーハンドリング を。build-vs-buy 判断との関係は、SchneeAI vs LiteLLM を。そして設計 — とくに cost ceiling の default — へ input いただける場合は、メール まで。