Gemini API 無料枠を運用する|枠表示とモデルの生存は別物だった — 自作ゲートウェイ3か月の記録

Gemini API の無料枠は、枠が表示されていてもモデルが生きているとは限らず、枠の値は予告なく変わる前提で追随の手順が要り、Embeddings ではモデルを混ぜられなかった。llm-gateway では、生存確認をスクリプトにし、枠は config の書き換えと再起動で追い、Embeddings はモデルを混ぜない(1 alias = 1 モデル)と決めた。

llm-gateway は Ollama を優先し Gemini API の無料枠を予備に使う OpenAI 互換ゲートウェイで、設計判断は前稿で扱った。2026-07-22 に NAS へ配備し、09-22 に v0.2.0 を反映し、10-11 まで config を保守した。稼働を始めて 3 か月になるが、利用はゲートウェイ単体の疎通と運用確認で、既存プロジェクトの載せ替えはまだ行っていない。1〜4 章は設計では見えなかったこと、5 章は進め方の振り返りである。

1. 稼働先を NAS にした理由と、Docker 配備で詰まった 3 点

稼働先は、GPU ワークステーション EdgeXpert GB10 に同居させる A 案を捨て、Synology NAS の Container Manager で動かす B 案を採った(ADR-0001、2026-07-19)。決め手は制御面(ゲートウェイ)と推論面(Ollama)の分離である。EdgeXpert は vLLM・モデル・ドライバの入れ替えで再起動する実験機で、Gemini 経路や劣化運転が要るのはまさにその停止中なのに、同居させるとゲートウェイも止まる。A 案の LAN 1 ホップの節約は推論時間に比べて誤差と見た。判断は可逆とし、compose ごと EdgeXpert や Pi 5 へ移せる状態を残した。

配備の前には、コンテナを作り直すと DB が消えるバグを直した(コミット 22fa53c)。

# compose.yaml から抜粋
services:
  llm-gateway:
    # …(build・image・ports などは省略)
    environment:
      # …(API キーの行は省略)
      # DB の永続化先。未設定だと app.py の既定値 "gateway.db"(カレントディレクトリ =
      # /app 相対)に書かれ、volumes の /data マウントと噛み合わずコンテナ再作成で
      # DB が失われる(Phase 4 修正: 2026-07-21 タスク仕様)。
      - LLM_GATEWAY_DB_PATH=/data/gateway.db
    volumes:
      # 要置換: NAS の実パスに合わせる(DB を SMB/NFS で他ホストへ共有しないこと)
      - /volume1/docker/llm-gateway/config.yaml:/app/config.yaml:ro
      - /volume1/docker/llm-gateway/data:/data

運営者が実機に配備したとき(2026-07-22)は 3 点で詰まった。Container Manager のウィザードは compose.yaml という名前を検出しないので、docker-compose.yml にコピーするか YAML を直接貼り付ける。SSH から docker compose を使うには、/var/run/docker.sock が root 専用なので sudo が要る。Windows PowerShell の curl は Invoke-WebRequest の別名なので、curl.exe と書く。前の 2 点は README に記録した。配備は最終的に Ollama 経由の実応答まで疎通を確かめて終えた。

2. 枠が表示されていてもモデルは生きていない

Phase 0(2026-07-19)の疎通確認で、gemini-2.5-flash と gemini-2.5-flash-lite は、AI Studio の rate-limit ページに枠が表示されたまま、公式が当時示していた提供終了予定日より前に 404 を返した。2 つを config から外し、「枠の表示 ≠ モデルの生存」をプロジェクトのルールに書いた。

Embeddings では同じ罠を両方向で踏んだ(2026-09-21 実測)。rate-limit ページ表記の gemini-embedding-1.0 は実 API で 404 になり(実 ID は gemini-embedding-001)、旧世代の text-embedding-004 は退役済みだった。逆に gemini-embedding-2 は ListModels に現れて POST も 200 を返したが、公式の models ページには無く、公式が挙げるのは gemini-embedding-2-preview だった。そこで README の手順に、採用は「実 API で応答し、かつ公式に掲載されている」ID に限ると書いた。

生存確認は手順書でなく scripts/probe_models.py にした。config が参照するモデルを 1 リクエストずつ叩き、404 なら終了コード 1 を返す。1 プローブが 1 RPM・1 RPD を消費するので --dry-run で対象だけを確かめられ、--list は枠を消費しない ListModels を引く。要は 404 と 429 を分けることだ。

# scripts/probe_models.py から抜粋
def _classify(status: int) -> tuple[Verdict, str]:
    """HTTP ステータスから生存判定を行う。

    404 のみを「退役」とする。429 は枠を使い切っているだけでモデルは生きており、
    5xx も一時障害の可能性があるため退役とは区別する(config から消してはいけない)。
    """
    if status == 200:
        return "alive", "200"
    if status == 404:
        return "dead", "404(このモデル ID は存在しない。退役または ID 表記の誤り)"
    if status == 429:
        return "throttled", "429(枠切れ。モデル自体は生存)"
    if status in (401, 403):
        return "unknown", f"{status}(認証エラー。API キーを確認)"
    return "unknown", str(status)

公式ページとの突き合わせはスクリプトに入れず、手作業で行う。リポジトリに記録した 2026-10-11 時点の情報では、auto と polish が使う gemini-3.5-flash が公式のモデル一覧から外れ、公式の廃止予定ページでの推奨移行先は gemini-3.6-flash だった(2026-10-23 に原文で再確認。エンドポイントは止まらず gemini-3.6-flash へ自動で振り向けるとの注記あり)。

3. 枠の値は config だけで追随させる

config の再読込は再起動だけで対応すると決め、ホットリロード(SIGHUP や /admin/reload)は非目標に置いて作らなかった(OQ-2、2026-07-19 決定)。枠の上限は DB に持たず config を正にした(前稿の 2 章)ので、改定されたら quotas.gemini.<model> を書き換えて再起動すれば済む。

詰まったのは値の読み方だった。rate-limit ページは「使用量 / 上限」の形で表示し(2026-09-21 時点)、config に書くのは上限の側である。同日に運営者が使用量を枠の改定と読み違え、6 分後に TPM の行だったと訂正したので、README に注記を足した。09-22 に embed を gemini-embedding-001 単独に改めたとき(4 章、コミット c8304d1)、実機の config からも gemini-embedding-2-preview の行を消して再起動するよう指示したが、18 日後の 10-11 に「残っていた」と報告があり、その後の結果は記録から確かめられない。同日の管理画面の要望は、Web UI が非目標なので FastAPI 既定の /docs と /admin/quota で代替した。

4. Embeddings を後から足して、翌日に一部を撤回した

2 か月ぶりに再開した 2026-09-21、運営者は Gemini Embedding を使いたいと依頼した。embeddings は非目標だったので依頼を未決事項 OQ-3 の決定として扱い、AskUserQuestion(選択式の質問)で確かめた。単位(Phase 5)と名前空間(embedding_routes の新設)は推奨どおりだったが、ローカル優先は、推奨した「貫く」ではなく運営者が「Gemini 専用」を選んだ。帰結は、(a) sensitive な embedding は起動時に拒否、(b) 退避先が無く枠が尽きたら 429、(c) chain は「Gemini 内のプール」として機能する、の 3 つと示した。

実キーの疎通(2026-09-21)では、Gemini の OpenAI 互換 /embeddings が usage を返さず、data[].index も 0 のときは省いた。openai SDK(2.46.0、同日時点)は両方を必須とするので、index は配列の位置で補い、usage は枠の予約に使う入力の見積から合成した(実測ではないと README に明記)。chat の見積は max_tokens の既定値 2048 を足すので、TPM が桁違いに小さい embedding では入力だけで見積もる。

翌 09-22、運営者にローカルの bge-m3 との使い分けを聞かれた直後に、(c) が成り立たないと分かった。chat のプールは出力が文章なのでモデルが混ざっても成立したが、embedding はモデルが違えばベクトル空間が別物になる。router の chain 走査は変えず、config の書き方の制約で対応した(コミット c8304d1)。

# config.example.yaml から抜粋
# …(Gemini 専用であることと、ollama・sensitive を拒否する注記は省略)
# **1 alias = 1 モデル**にすること。chat と違い、embedding はモデルが違うと同じ文章でも
# ベクトル空間がまったく別物になり、次元を揃えても比較できない。chain で別モデルへ
# フォールバックさせると、枠切れ(分単位の rate_limited を含む)を境に同じ alias から
# 別空間のベクトルが返り、呼び出し側のベクトルインデックスが混在して壊れる。
# さらにキャッシュは alias 単位のため、混在したベクトルが TTL の間残り続ける。
# 枠切れ時は切り替えずに 429 を返すのが正しい(docs/design.md 8章 OQ-3)。
embedding_routes:
  embed:
    privacy: normal
    cache: true
    chain:
      - { backend: gemini, model: "gemini-embedding-001" }

この非互換は「ローカル優先」の選択肢の説明には書いてあったが、選択の後に示した帰結 (c) には付いていなかった。機密テキストの embedding は Ollama を直接呼び、インデックスも分ける。2 章と同じ記録(原文で再確認 2026-10-23)では、gemini-embedding-001 は 2028-05-14 に提供終了予定で、推奨移行先は preview の付かない gemini-embedding-2 である。09-21 に公式未掲載として見送った判断は見直す余地があり、移行するときはインデックスを全件埋め込み直す計画が要る。

5. 進め方で効いたことと、次回変えること

効いたことは 3 つある。フェーズの境界を守り、Phase 1 のゲートで先行実装を指摘されたとき、運営者は例外承認でなく削る方を選び、以後、実機に依存しない Phase 2・3・5 はどれも着手から半日以内でゲートに届いた(フェーズゲート)。生成と検証を分け、別に立てた gate-reviewer が privacy guard の fail-open や会計のリークを拾った(委譲、レビュー)。引き継ぎも文書にし、Phase 5 の引き継ぎプロンプトに契約変更は承認済みであることなどを書いた結果、新しいセッションは聞き直さずに着手できた。

次回は 3 つ変える。1 つ目は、resume したセッションでは実際の日付を確かめること。OQ-3 の決定は 09-21 だが、4 つの文書と契約 2 ファイルの説明文、コミット本文が「2026-07-23」と書いており、2026-10-22 に文書側を訂正した。原因は特定していない。2 つ目は、ドメイン固有の不変条件を選択肢の段階で確かめること。ベクトルの非互換は選択肢の説明に書いてあったのに、選んだ後の帰結には付いていなかった(4 章)。3 つ目は、実機に依存する DoD に期限か代替条件を付けること。Phase 4 の実機 DoD 3 項目は 3 か月間チェックされていない。

まとめ

ゲートウェイは推論機と分けて NAS に置き、無料枠の運用では、生存確認をスクリプトに、枠の追随を config と再起動に、Embeddings を 1 alias = 1 モデルに寄せた。設計判断は前稿にある。

--- 参照: llm-gateway v0.2.0(HEAD c63dd42、2026-10-23 確認)の compose.yaml・scripts/probe_models.py・config.example.yaml・README.md・docs/ の ADR-0001・design.md・phases.md。Gemini API のレート制限: https://ai.google.dev/gemini-api/docs/rate-limits / モデル一覧: https://ai.google.dev/gemini-api/docs/models / 廃止予定: https://ai.google.dev/gemini-api/docs/deprecations (いずれも 2026-10-24 確認)。