[1つのモデルで429制限発生時に、Google Geminiプロバイダー全体にバックオフが適用される問題] - Google Gemini Provider: 429 Rate Limit Scopes to Entire Provider Instead of Specific Model
1つのGoogle Geminiモデルがレート制限(429)に達すると、OpenClawゲートウェイが'google'プロバイダー全体にバックオフを適用し、独立した配额を持つ他の無関係なモデルへのアクセスをブロックします。
🔍 症状
主な症状
特定のGoogle Geminiモデルのクォータが使い果たされると、そのモデルとは独立したクォータ割り当てを持つ他のモデルへのすべてのリクエストも、レート制限エラーで失敗します。
エラーの出力例
直接のAPIレスポンス(Googleからの429):
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
}
バックオフ発動後のOpenClawゲートウェイレスポンス:
{
"error": {
"type": "rate_limit_exceeded",
"provider": "google",
"message": "Provider 'google' is currently in cooldown due to rate limiting. Retry-After: 120s",
"retry_after": 120
}
}
動作上の症状
- モデル分離なし:
gemini-3.1-pro-preview-customtoolsからgemini-3.0-pro-previewへの切り替えでは機能が回復しません。 - 長時間の利用不可:すべての
googleプロバイダへのリクエストが、プロバイダレベルのクールダウンが期限切れるまで失敗します。 - フォールバックパスなし:レート制限イベント発生時、同じプロバイダ内の代替モデルはフォールバックとして機能できません。
- ゲートウェイレベルでの拒否:リクエストはGoogleのAPIに到達する前に、OpenClawゲートウェイレイヤーで拒否される場合があります。
再現シナリオ
# Step 1: Request to rate-limited model
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.1-pro-preview-customtools", "messages": [{"role": "user", "content": "test"}]}'
# Response: 429 from Google API
# Step 2: Immediate fallback to another model
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.0-pro-preview", "messages": [{"role": "user", "content": "test"}]}'
# Expected: Request proceeds to Google API
# Actual: 429 or backoff error from OpenClaw gateway
🧠 原因
アーキテクチャ分析
根本原因は、OpenClawゲートウェイのリトライ/バックオフメカニズム内のプロバイダレベルのレート制限トラッキングの実装にあります。
障害のシーケンス
gemini-3.1-pro-preview-customtoolsへのリクエスト:モデル固有のデプロイメントがGoogleのAPIから429 RESOURCE_EXHAUSTEDを受け取る。- ゲートウェイが429を傍受:OpenClawのエラーハンドリングミドルウェアが429レスポンスをキャッチする。
- プロバイダレベルのバックオフ激活:特定のモデル/デプロイメントではなく、ゲートウェイが
googleプロバイダ識別子にクールダウンタイマーを設定する。 gemini-3.0-pro-previewへの後続のリクエスト:ゲートウェイがgoogleプロバイダがクールダウン中かどうかを確認する。クールダウン中の場合、バックオフエラーでリクエストを先制的に拒否する。- 独立したクォータを持つモデルがブロックされる:
gemini-3.0-pro-previewは完全に別のクォータ割り当てを持っている可能性があるが、アクセスできない。
コードレベルの根本原因
レート制限トラッキングは、以下のようなデータ構造を使用している可能性があります:
// Simplified representation of current behavior
const providerBackoff = {
"google": {
cooldownUntil: 1699999999999, // Unix timestamp
reason: "rate_limit",
retryAfter: 120
}
};
// Backoff check
function shouldReject(provider) {
return providerBackoff[provider]?.cooldownUntil > Date.now();
}
問題点:バックオフがプロバイダ名(“google”)而不是由模型或部署标识符キーになっている。
Google Gemini APIのクォータアーキテクチャ
Google Gemini APIは以下の形式で動作します:
- モデル固有のクォータ:各モデル(例:
gemini-3.1-pro-preview-customtools)は独立したレート制限を持つ。 - プロジェクトレベルのクォータ:すべてのモデルに影響するより広範な制限だが、通常ははるかに高い。
- リージョンエンドポイント:独立した制限を持つ場合がある。
異なるコードパス
| シナリオ | 現在の動作 | 期待される動作 |
|---|---|---|
| モデルAが429を受ける | googleプロバイダ全体がブロック | モデルAのみブロック |
| モデルAのクォータが使い果たされる | モデルBが利用不可 | モデルBはクォータが利用可能なら継続 |
| プロバイダバックオフがアクティブ | ゲートウェイがレイヤー7で拒否 | リクエストがAPIに送信される |
🛠️ 解決手順
オプション1:モデルスコープのレート制限を有効にする(推奨)
OpenClawがモデルごとのレート制限トラッキングをサポートしている場合、ゲートウェイをモデルレベルのバックオフを使用するように設定します:
変更前(openclaw.yaml):
providers:
google:
api_key: "${GOOGLE_API_KEY}"
rate_limit:
strategy: "provider" # Current: blocks entire provider
retry_after: 120
変更後:
providers:
google:
api_key: "${GOOGLE_API_KEY}"
rate_limit:
strategy: "model" # Changed: per-model tracking
retry_after: 120
scope: "deployment" # Granularity: model/deployment level
オプション2:モデル固有のフォールバックを設定
レート制限されたモデルをバイパスするための明示的なフォールバックチェーンを定義します:
変更前:
models:
- name: "gemini-3.1-pro-preview-customtools"
provider: "google"
変更後:
models:
- name: "gemini-3.1-pro-preview-customtools"
provider: "google"
fallback_models:
- "gemini-3.0-pro-preview"
- "gemini-pro"
- name: "gemini-3.0-pro-preview"
provider: "google"
fallback_models:
- "gemini-pro"
オプション3:プロバイダのクールダウン粒度を増加(コード修正)
OpenClawのソースコードへのアクセスがある場合、レート制限トラッキングを修正します:
ステップ1:レート制限ハンドラを特定
429レスポンスを処理するファイルを見つけます。通常は以下の場所にあります:
src/gateway/middleware/rate-limit-handler.ts
src/providers/google/error-handler.ts
ステップ2:バックオフキーをプロバイダからモデルに変更
// BEFORE (provider-level)
providerBackoff[provider] = {
cooldownUntil: Date.now() + retryAfter * 1000,
reason: "rate_limit"
};
// AFTER (model-level)
const modelKey = `${provider}:${model}`;
modelBackoff[modelKey] = {
cooldownUntil: Date.now() + retryAfter * 1000,
reason: "rate_limit",
model: model,
provider: provider
};
ステップ3:拒否チェックを更新
// BEFORE
function shouldReject(request) {
const provider = request.provider;
return providerBackoff[provider]?.cooldownUntil > Date.now();
}
// AFTER
function shouldReject(request) {
const modelKey = `${request.provider}:${request.model}`;
const providerKey = request.provider;
// Check model-specific backoff first
if (modelBackoff[modelKey]?.cooldownUntil > Date.now()) {
return { rejected: true, reason: "model_rate_limited" };
}
// Fallback to provider-level for shared limits only
if (providerBackoff[providerKey]?.cooldownUntil > Date.now()) {
return { rejected: true, reason: "provider_rate_limited" };
}
return { rejected: false };
}
オプション4:複数プロバイダインスタンスによる回避策
独立したクォータを持つモデルのために別々のプロバイダ設定を作成します:
providers:
google-gemini-31:
api_key: "${GOOGLE_API_KEY}"
models:
- "gemini-3.1-pro-preview-customtools"
rate_limit:
retry_after: 60
google-gemini-30:
api_key: "${GOOGLE_API_KEY}"
models:
- "gemini-3.0-pro-preview"
rate_limit:
retry_after: 60
google-gemini-pro:
api_key: "${GOOGLE_API_KEY}"
models:
- "gemini-pro"
rate_limit:
retry_after: 60
🧪 検証
テスト1:修正後のモデルレベル分離の確認
# Step 1: Trigger rate limit on model A
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.1-pro-preview-customtools", "messages": [{"role": "user", "content": "test"}]}'
# Expected: 429 from Google API
# Verify with: echo $? (should be non-zero)
# Step 2: Immediately test model B access
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.0-pro-preview", "messages": [{"role": "user", "content": "test"}]}'
# Expected: 200 OK or valid API response (not gateway backoff error)
テスト2:モデル固有のバックオフ状態の確認
ゲートウェイの内部状態を確認します(管理者エンドポイントで公開されている場合):
GET /admin/rate-limit-status
# Expected response structure:
{
"providers": {
"google": {
"cooldown": false,
"models": {
"gemini-3.1-pro-preview-customtools": {
"cooldown": true,
"retry_after": 120,
"expires_at": "2024-01-15T10:30:00Z"
},
"gemini-3.0-pro-preview": {
"cooldown": false
}
}
}
}
}
テスト3:同時モデル可用性テスト
# Run concurrent requests to different models
for model in "gemini-3.1-pro-preview-customtools" "gemini-3.0-pro-preview" "gemini-pro"; do
echo "Testing: $model"
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\": \"$model\", \"messages\": [{\"role\": \"user\", \"content\": \"test\"}]}"
done
# Expected:
# gemini-3.1-pro-preview-customtools: 429 (rate limited)
# gemini-3.0-pro-preview: 200 (independent quota)
# gemini-pro: 200 (independent quota)
テスト4:バックオフ期限切れの確認
# Wait for cooldown to expire
echo "Waiting for model cooldown expiration..."
sleep 130 # retry_after + buffer
# Verify previously rate-limited model is accessible
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.1-pro-preview-customtools", "messages": [{"role": "user", "content": "test"}]}'
# Expected: 200 OK
成功基準
- ✅
gemini-3.1-pro-preview-customtoolsでレート制限後でも、他のgoogleモデルはアクセス可能なまま。 - ✅ モデル固有のバックオフ状態が正しくトラッキングされ、独立して期限切れになる。
- ✅ ゲートウェイがレート制限されていないモデルへのリクエストを先制的に拒否しない。
- ✅ プライマリモデルが利用不可の場合、フォールバックチェーンが正しく機能する。
⚠️ よくある落とし穴
環境固有のトラップ
Dockerコンテナのキャッシュ
# Pitfall: Container filesystem may cache rate limit state
# Restarting containers may not reset state if persistence is enabled
docker-compose down
docker volume prune openclaw-cache # Clear cached state
docker-compose up -d
Kubernetesボリュームマウント
レート制限トラッキングに永続ボリュームを使用している場合:
# Verify PVC is not stale after config changes
kubectl get pvc | grep openclaw
kubectl describe pvc openclaw-cache
# May need to delete and recreate if schema changed
kubectl delete pvc openclaw-cache
# Then restart deployments
macOS開発環境
# Pitfall: Local rate limit state may persist across terminal sessions
# Clear any local state files
rm -rf ~/.openclaw/cache/*
rm -rf .openclaw/state.json
設定ミス
フォールバックチェーンの不適切なプロバイダ名
# WRONG: Typos in provider name cause silent failures
models:
- name: "gemini-3.0-pro-preview"
provider: "googel" # Typo - will not match actual provider
# CORRECT:
models:
- name: "gemini-3.0-pro-preview"
provider: "google"
重複するモデル宣言
# WRONG: Same model declared multiple times
models:
- name: "gemini-3.0-pro-preview"
provider: "google"
- name: "gemini-3.0-pro-preview" # Duplicate
provider: "google"
fallback_models: [...]
APIキースコープの不一致
# Pitfall: Google API keys may have different quotas per project
# If using separate provider instances, ensure they use keys with adequate quotas
# Verify in Google Cloud Console:
# APIs & Services > Enabled APIs > Vertex AI API > Quotas
エッジケースのテスト
最後に利用可能なモデルでのレート制限
# Scenario: All models under a provider are rate-limited
# Expected: Should return clear error, not silent success
# Verify error response includes all affected models
curl -X POST https://api.openclaw.io/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemini-3.0-pro-preview", "messages": [{"role": "user", "content": "test"}]}'
# Check response contains actionable information
# Should NOT be empty 200 OK
素早いモデル切り替え
# Pitfall: Race condition during rapid switching may bypass backoff
# Test with concurrent requests
ab -n 100 -c 10 -T 'application/json' \
-p request.json \
https://api.openclaw.io/v1/chat/completions
# Verify all requests are properly rate-limited or processed
🔗 関連するエラー
| エラーコード | 説明 | 関連性 |
|---|---|---|
429 RESOURCE_EXHAUSTED | Google APIがレート制限エラーを返した | プロバイダバックオフを引き起こす元エラーの原因 |
503 Service Unavailable | プロバイダが一時的に利用不可 | 長時間続くプロバイダバックオフの下流エラー |
500 Internal Server Error | バックオフ処理中のゲートウェイエラー | レート制限ミドルウェアでの未処理例外 |
ENOTFOUND | Google APIのDNS解決失敗 | 無関係だがレート制限と誤診される可能性あり |
ETIMEDOUT | Google APIへの接続タイムアウト | 無関係だがincorrect backoff logic をトリガーする可能性がある |
INVALID_ARGUMENT | Gemini APIへの不正なリクエスト | エラーハンドリングでレート制限として誤って処理される可能性あり |
歴史的背景
この問題は、マルチテナントAPIゲートウェイ設計におけるより広範なパターンに関連しています:
- 過度に広範なサーキットブレーカー:モデル/デプロイメントレベルで動作すべきサーキットブレーカーパターンをプロバイダレベルで適用。
- 共有状態の衝突:複数の独立したリソースが単一のレート制限カウンターを共有。
- 不十分なエラーコンテキスト:Googleからの429レスポンスには、どのクォータが使い果たされたかを指定する
retryInfoが含まれているが、解析されていない可能性がある。
関連するGitHubイシュー
- Rate limiting should be scoped per-model not per-provider - モデルレベル分離のための機能リクエスト
- Google Gemini provider backoff blocks all models - 重複トラッキング問題
- Add retry-after parsing from Google 429 responses - 正確なクールダウン計算のための強化