請求クールダウンによりスキップパスが isBillingErrorMessage() をバイパス — 請求メッセージの代わりに汎用エラーが表示される
すべての model-fallback 候補が請求クールダウンによりスキップされると、ユーザーは BILLING_ERROR_USER_MESSAGE ではなく、汎用的な「問題が発生しました」エラーを目にします。これは、クールダウンで生成されたスキップメッセージが isBillingErrorMessage() のいずれのパターンにも一致しないためです。
🔍 症状
ユーザーから見える症状
Anthropic OAuth認証済みアカウントで最初の請求処理に失敗した後、すべての後続の再試行が汎用的な、アクションにつながれないエラーメッセージを表示します:
⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.
これは、根本原因がAnthropic側のクォータ使い果たしであっても、約30分間隔で何時間も繰り返し表示されます。
開発者側から見える症状(エージェントログ)
最初の障害では請求エラーを正しく検出します:
[agent] embedded run agent end: runId=e8520f5d-... isError=true model=claude-opus-4-6 provider=anthropic error=LLM request rejected: You're out of extra usage. Add more at claude.ai/settings/usage and keep going.
[agent] auth profile failure state updated: runId=e8520f5d-... profile=sha256:154a23a3efe6 provider=anthropic reason=billing window=disabled後続のすべての障害ではクールダウンスキップパスが実行されます:
[model-fallback] model fallback decision: decision=skip_candidate requested=anthropic/claude-opus-4-6 candidate=anthropic/claude-opus-4-6 reason=billing next=anthropic/claude-sonnet-4-6 detail=Provider anthropic has billing issue (skipping all models)
[model-fallback] model fallback decision: decision=skip_candidate requested=anthropic/claude-opus-4-6 candidate=anthropic/claude-sonnet-4-6 reason=billing next=none detail=Provider anthropic has billing issue (skipping all models)
Embedded agent failed before reply: All models failed (2): anthropic/claude-opus-4-6: Provider anthropic has billing issue (skipping all models) (billing) | anthropic/claude-sonnet-4-6: Provider anthropic has billing issue (skipping all models) (billing)構造化データによる確認
FallbackSummaryErrorは各試行でattempt.reason=“billing”を保持しますが、agent-runner-execution.tsのisBillingErrorMessage()チェックはfailover-matches.tsのERROR_PATTERNS.billingに対して文字列照合を行い、“has billing issue”パターンを含んでいません。
頻出パターン
このサイクルは長時間を通じて30分ごとに繰り返されます:
2026-04-13T22:41:05 ... Embedded agent failed before reply: All models failed (2): ... (billing) | ... (billing)
2026-04-13T23:11:05 ... Embedded agent failed before reply: All models failed (2): ... (billing) | ... (billing)
2026-04-13T23:41:05 ... Embedded agent failed before reply: All models failed (2): ... (billing) | ... (billing)🧠 原因
アーキテクチャ:2つのエラー分類戦略
OpenClawはエラーを請求関連として分類するために2つの異なる戦略を使用し、生APIエラーパスとクールダウンスキップパスの間に非対称性があります:
- 生APIエラーパス:
isBillingErrorMessage(message: string)—failover-matches.tsのERROR_PATTERNS.billingに対する正規表現/文字列照合。 - レート制限パス(すでに正しい):
isPureTransientRateLimitSummary(failure: FallbackSummaryError)—attempt.reason === 'rate_limit'に対する構造的チェック。 - 請求クールダウンスキップパス(壊れている): 同等の構造的チェックがなく、
isBillingErrorMessage()文字列照合のみに依存しており、"has billing issue (skipping all models)"に一致しない。
障害シーケンス
- ユーザーのAnthropic OAuth個人「追加使用分」クォータが使い果たされる。
- 最初のLLMリクエストが、生APIエラーを返す:
400 {"type":"error","error":{"type":"invalid_request_error","message":"You're out of extra usage. Add more at claude.ai/settings/usage and keep going."}} - 生エラーメッセージが
ERROR_PATTERNS.billingに一致 →auth profileがbilling cooldown window=disabled状態になる。 - 後続のリクエストが
model-fallback.tsでモデルフォールバックスキップロジックをトリガーする。 - すべての候補モデルがdetail:
"Provider anthropic has billing issue (skipping all models)"でスキップされる。 - 各失敗した試行に対して
attempt.reason="billing"を持つFallbackSummaryErrorが構築される。 agent-runner-execution.tsで、コードがisBillingErrorMessage(error.message)を呼び出すが、"has billing issue"はERROR_PATTERNS.billingに含まれていない。- 請求チェックが失敗するため、汎用フォールバックエラーパスが採用され、
"Something went wrong"が生成される。
関連するコード箇所
src/core/failover/failover-matches.ts—ERROR_PATTERNS.billingには"out of extra usage"、"insufficient balance"、"billing error"などのパターンが含まれるが、"has billing issue"は**含まない**。src/core/agent-runner-execution.ts— エラー描画パスの請求分類ゲートとしてisBillingErrorMessage(message)のみを呼び出す。src/core/model-fallback/model-fallback.ts— 請求クールダウンがアクティブな場合に"Provider X has billing issue (skipping all models)"メッセージを生成する。src/core/failover/failover-matches.ts—isPureTransientRateLimitSummary()がattempt.reason === 'rate_limit'を構造的フィールドとして正しく検査し、請求パスが欠けている正しいパターンを実証する。
レート制限パスが正常に動作する理由
レート制限パスはすでに構造的attempt.reasonフィールドを使用している:
export function isPureTransientRateLimitSummary(failure: FallbackSummaryError): boolean {
return failure.attempts.every(a => a.reason === 'rate_limit');
}このアプローチは、人が読めるメッセージではなく意味的分類フィールドを検査するため、メッセージ文字列の変更に影響されない。
OAuthがこの問題を悪化させる理由
claude.aiの個人「追加使用分」クォータは、組織のAPI予算一般的に小さい。OAuth認証済みアカウントはAPIキー認証済み組織アカウントよりも頻繁に個人クォータに達するため、このバグはOAuthインストールパスで一般的なユーザー体験の問題になっている。
🛠️ 解決手順
修正戦略
failover-matches.tsに既存のisPureTransientRateLimitSummary()パターンを模倣した構造的請求チェック関数isPureBillingSummary()を追加する。agent-runner-execution.tsを更新して、この構造的チェックを請求分類の主ゲートとして使用し、レガシー生APIエラーに対してのみ文字列照合にフォールバックする。
ステップ1:failover-matches.tsに構造的請求チェックを追加
既存のisPureTransientRateLimitSummary()とともに、src/core/failover/failover-matches.tsに次の関数を追加する:
修正前:
export function isPureTransientRateLimitSummary(failure: FallbackSummaryError): boolean {
return failure.attempts.every(a => a.reason === 'rate_limit');
}修正後:
export function isPureTransientRateLimitSummary(failure: FallbackSummaryError): boolean {
return failure.attempts.every(a => a.reason === 'rate_limit');
}
/**
* Structural check: true when every attempt in the FallbackSummaryError
* is classified as billing-cooldown. This correctly handles the
* "Provider X has billing issue (skipping all models)" skip path,
* which is not matched by isBillingErrorMessage() string patterns.
*/
export function isPureBillingSummary(failure: FallbackSummaryError): boolean {
return failure.attempts.every(a => a.reason === 'billing');
}ステップ2:agent-runner-execution.tsの請求分類ゲートを更新
src/core/agent-runner-execution.tsの請求エラー分類ロジックを見つける。文字列のみチェックを構造体を優先するアプローチに置き換える:
修正前:
const isBilling = isBillingErrorMessage(message);修正後:
// Prefer structural classification (cooldown skip path) over string matching.
const isBilling = error instanceof FallbackSummaryError
? isPureBillingSummary(error)
: isBillingErrorMessage(message);FallbackSummaryErrorとisPureBillingSummaryがインポートされていることを確認する:
import { FallbackSummaryError } from '../model-fallback/types';
import { isPureBillingSummary } from '../failover/failover-matches';ステップ3:(オプションの強化)ERROR_PATTERNS.billingの拡張
クールダウンウィンドウを通る生APIエラーも適切に処理されることを確認するために、src/core/failover/failover-matches.tsの請求パターンを拡張してクールダウンスキップフレーズを含める:
修正前:
export const ERROR_PATTERNS = {
billing: [
/out of extra usage/i,
/insufficient balance/i,
/billing error/i,
/api key (has|runs out).*credit/i,
/add more at.*usage/i,
/out of credits/i,
],
// ...
};修正後:
export const ERROR_PATTERNS = {
billing: [
/out of extra usage/i,
/insufficient balance/i,
/billing error/i,
/api key (has|runs out).*credit/i,
/add more at.*usage/i,
/out of credits/i,
/has billing issue \(skipping all models\)/i, // cooldown skip path
],
// ...
};この3番目のステップは防御的である。ステップ1〜2の主要な修正で十分である。なぜならisPureBillingSummary()はFallbackSummaryErrorケースに到達する前にショートサーキットするからである。
ステップ4:再ビルドとデプロイ
npm run build
# or for Docker deployments:
docker build -t openclaw:fixed .🧪 検証
ユニットテスト:isPureBillingSummary()
src/core/failover/failover-matches.test.tsにテストケースを追加する:
import { isPureBillingSummary } from './failover-matches';
import { FallbackSummaryError, FallbackAttempt } from '../model-fallback/types';
describe('isPureBillingSummary', () => {
it('returns true when all attempts have reason=billing', () => {
const attempts: FallbackAttempt[] = [
{
provider: 'anthropic',
model: 'claude-opus-4-6',
reason: 'billing',
message: 'Provider anthropic has billing issue (skipping all models)',
durationMs: 0,
startTime: 0,
endTime: 0,
},
{
provider: 'anthropic',
model: 'claude-sonnet-4-6',
reason: 'billing',
message: 'Provider anthropic has billing issue (skipping all models)',
durationMs: 0,
startTime: 0,
endTime: 0,
},
];
const error = new FallbackSummaryError('All models failed', attempts);
expect(isPureBillingSummary(error)).toBe(true);
});
it('returns false when attempts contain mixed reasons', () => {
const attempts: FallbackAttempt[] = [
{ provider: 'anthropic', model: 'claude-opus-4-6', reason: 'billing', message: '', durationMs: 0, startTime: 0, endTime: 0 },
{ provider: 'anthropic', model: 'claude-sonnet-4-6', reason: 'rate_limit', message: '', durationMs: 0, startTime: 0, endTime: 0 },
];
const error = new FallbackSummaryError('All models failed', attempts);
expect(isPureBillingSummary(error)).toBe(false);
});
it('returns false when no attempt has reason=billing', () => {
const attempts: FallbackAttempt[] = [
{ provider: 'anthropic', model: 'claude-opus-4-6', reason: 'rate_limit', message: '', durationMs: 0, startTime: 0, endTime: 0 },
];
const error = new FallbackSummaryError('All models failed', attempts);
expect(isPureBillingSummary(error)).toBe(false);
});
});テストスイートを実行する:
npm test -- --testPathPattern="failover-matches"
# Expected: isPureBillingSummary tests pass統合テスト:請求クールダウンエラーの描画
テストプロバイダーまたはモックされたAuthProfileを使用して請求クールダウンシナリオをシミュレートする:
# Using the OpenClaw CLI test harness (if available):
openclaw test:integration --scenario=billing-cooldown --auth-type=oauth
# Expected output in user-facing message channel:
# "⚠️ API provider returned a billing error — your API key has run out of credits
# or has an insufficient balance. Check your provider's billing dashboard and
# top up or switch to a different API key."
# (i.e., BILLING_ERROR_USER_MESSAGE, not "Something went wrong")手動検証:ログ検査
請求クールダウンをトリガーし、修正された分類についてエージェントログを検査する:
# Trigger a billing exhaustion scenario, then observe subsequent failures:
grep -E "(billing|isBilling|Something went wrong)" /var/log/openclaw/agent.log
# Before fix — "Something went wrong" appears repeatedly:
# Embedded agent failed before reply: ... (Something went wrong)
# Embedded agent failed before reply: ... (Something went wrong)
# After fix — BILLING_ERROR_USER_MESSAGE appears:
# [agent] embedded run agent end: ... userMessage=⚠️ API provider returned a billing error...
# Embedded agent failed before reply: ... (billing)終了コードの検証
# Verify graceful degradation with billing error exit code
openclaw run --prompt="Hello" --model=anthropic/claude-opus-4-6
echo "Exit code: $?"
# Expected: non-zero exit (indicating error state was properly surfaced), NOT a crash⚠️ よくある落とし穴
- isPureBillingSummary()を追加せずにERROR_PATTERNSのみを拡張する:
"has billing issue"を文字列パターンに追加することは回避策として機能するが、脆弱である。将来のリリースでモデルフォールバックメッセージ形式が変更された場合(例:「Provider X billing cooldown — skipping all models」)、パターンは再度壊れる。構造的isPureBillingSummary()アプローチはメッセージ文字列の変更に対して堅牢である。 - isPureBillingSummary()を無条件に適用する: チェックは
instanceof FallbackSummaryErrorでガードされなければならない。生文字列または他のエラータイプで呼び出すとTypeErrorをスローする。非FallbackSummaryErrorタイプに対するisBillingErrorMessage(message)へのフォールバックは、生APIエラーとの下位互換性を維持する。 - テストにおけるOAuthとAPIキーの非対称性: 個人「追加使用分」クォータがより小さいため、バグはOAuth認証済みアカウントでより簡単に発生する。組織レベルのAPI-keysでテストすると問題が発生しない可能性があり、修正が機能しているという誤った自信につながる。常にOAuth個人クォータシナリオとAPIキー使い果たしシナリオの両方でテストする。
- クールダウンウィンドウ状態永続化: 永続ストア(Redis、SQLite)でバックアップされている場合、請求クールダウン状態は再起動Acrossても永続化する。テスト環境では実行間のauth profile失敗状態をリセットすることを確認するか、修正後もエラー 메시지 경로がブロックされたままになる。
- 部分的モデルフォールバックカバレッジ: ルーティングチェーンの一部のプロバイダーのみが請求クールダウンに入る場合、
FallbackSummaryErrorにはreason: 'billing'と他の理由(例:reason: 'timeout')が混在する。isPureBillingSummary()はこの混合ケースでfalseを返す。混合失敗が一般的な場合は、isMostlyBillingSummary()をセカンダリヒューリスティックとして追加することを検討する。 - Dockerボリュームマウントタイミング: Dockerデプロイメントでは、再ビルドされたコンテナイメージが使用されていることを確認する(
docker build、ビルドコンテキストが古くなっている場合はdocker-compose up -d --buildだけでは不十分)。ソースファイルを編集してup -dのみを実行し、修正なしで既存のイメージを使用するというのは一般的な間違いである。 - ログ詳細度が多問題を見えなくする: 本番環境で
LOG_LEVEL=errorが設定されている場合、詳細な[model-fallback] model fallback decision行が抑制され、クールダウンスキップパスまたは生エラーパスのどちらが採用されたかを診断することが困難になる。トラブルシューティング中はLOG_LEVEL=debugに設定する。
🔗 関連するエラー
FallbackSummaryErrorwith"Something went wrong"message —isBillingErrorMessage()もisPureRateLimitSummary()も一致しない場合にユーザーが見る汎用フォールバックエラー。エラールーティングロジック内の分類ギャップを示す。ERROR_PATTERNS.billingpattern mismatch —isBillingErrorMessage()が文字列ベースの請求検出に使用するfailover-matches.ts内の正規表現パターンセット。クールダウンスキップフレーズが欠けていたことが根本原因であった。- PR #61608 — 部分的請求パターン修正 — 生APIエラーのみを対象に
ERROR_PATTERNS.billingに"out of extra usage"を追加した。クールダウン生成スキップパスには対応しなかった。 - Issue #48526 — 関連する請求エラー分類ギャップ(パターンマッチング対構造的分類問題の以前の実例である可能性あり)。
- Issue #64224 — OAuth認証の請求クォータ使い果たしが繰り返しエラーを引起こす(同じ根本原因である可能性あり、異なる症状)。
- Issue #64308 — 汎用エラー出力を伴うモデルフォールバック全モデルスキップシナリオ。
- Issue #62375 — OAuth個人使用クォータを伴うAnthropicプロバイダー請求エラー処理のエッジケース。
isPureTransientRateLimitSummary()— 請求パスが模倣すべき正しいパターン。レート制限エラーに対してすでに実装されていた構造的attempt.reason検査アプローチを実証する。BILLING_ERROR_USER_MESSAGE— 表示されるべきだったユーザー向けメッセージ:"⚠️ API provider returned a billing error — your API key has run out of credits or has an insufficient balance. Check your provider's billing dashboard and top up or switch to a different API key."auth profile failure state: reason=billing window=disabled— 請求クールダウンがアクティブ化されたことを示すauth profileメタデータ。定義されたクールダウン期間 동안後続の試行を防止する。