April 17, 2026 • バージョン: 2026.4.9

請求クールダウンによりスキップパスが 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.tsisBillingErrorMessage()チェックはfailover-matches.tsERROR_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.tsERROR_PATTERNS.billingに対する正規表現/文字列照合。
  • レート制限パス(すでに正しい): isPureTransientRateLimitSummary(failure: FallbackSummaryError)attempt.reason === 'rate_limit'に対する構造的チェック。
  • 請求クールダウンスキップパス(壊れている): 同等の構造的チェックがなく、isBillingErrorMessage()文字列照合のみに依存しており、"has billing issue (skipping all models)"に一致しない。

障害シーケンス

  1. ユーザーのAnthropic OAuth個人「追加使用分」クォータが使い果たされる。
  2. 最初の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."}}
  3. 生エラーメッセージがERROR_PATTERNS.billingに一致 → auth profilebilling cooldown window=disabled状態になる。
  4. 後続のリクエストがmodel-fallback.tsでモデルフォールバックスキップロジックをトリガーする。
  5. すべての候補モデルがdetail: "Provider anthropic has billing issue (skipping all models)"でスキップされる。
  6. 各失敗した試行に対してattempt.reason="billing"を持つFallbackSummaryErrorが構築される。
  7. agent-runner-execution.tsで、コードがisBillingErrorMessage(error.message)を呼び出すが、"has billing issue"ERROR_PATTERNS.billingに含まれていない。
  8. 請求チェックが失敗するため、汎用フォールバックエラーパスが採用され、"Something went wrong"が生成される。

関連するコード箇所

  • src/core/failover/failover-matches.tsERROR_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.tsisPureTransientRateLimitSummary()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);

FallbackSummaryErrorisPureBillingSummaryがインポートされていることを確認する:

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に設定する。

🔗 関連するエラー

  • FallbackSummaryError with "Something went wrong" messageisBillingErrorMessage()isPureRateLimitSummary()も一致しない場合にユーザーが見る汎用フォールバックエラー。エラールーティングロジック内の分類ギャップを示す。
  • ERROR_PATTERNS.billing pattern mismatchisBillingErrorMessage()が文字列ベースの請求検出に使用する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メタデータ。定義されたクールダウン期間 동안後続の試行を防止する。

エビデンスとソース

このトラブルシューティングガイドは、FixClaw Intelligence パイプラインによってコミュニティの議論から自動的に合成されました。