April 17, 2026 • Versión: 2026.4.9

Billing Cooldown Skip Path Evita isBillingErrorMessage() — Se Muestra Error Genérico en Lugar del Mensaje de Facturación

Cuando todos los candidatos model-fallback se omiten debido al enfriamiento de facturación, los usuarios ven un error genérico de 'Algo salió mal' en lugar del BILLING_ERROR_USER_MESSAGE procesable, porque el mensaje de omisión generado por el enfriamiento no coincide con ningún patrón en isBillingErrorMessage().

🔍 Síntomas

Síntoma visible para el usuario

Después del primer fallo de facturación con una cuenta autenticada mediante OAuth de Anthropic, todos los intentos de reintento posteriores muestran un mensaje de error genérico y no accionable:

⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.

Esto se repite con una cadencia de aproximadamente 30 minutos durante horas, aunque la causa subyacente es un agotamiento de la cuota de facturación del lado de Anthropic.

Síntoma visible para el desarrollador (Logs del agente)

El primer fallo muestra correctamente el error de facturación:

[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

Todos los fallos posteriores producen la ruta de omisión de enfriamiento:

[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)

Confirmación mediante datos estructurados

El FallbackSummaryError contiene attempt.reason=“billing” para cada intento, pero la verificación isBillingErrorMessage() en agent-runner-execution.ts realiza coincidencia de cadenas contra ERROR_PATTERNS.billing en failover-matches.ts, que no incluye el patrón “has billing issue”.

Patrón de frecuencia

El ciclo se repite cada 30 minutos durante períodos prolongados:

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)

🧠 Causa raíz

Arquitectura: dos estrategias de clasificación de errores

OpenClaw utiliza dos estrategias distintas para clasificar los errores como relacionados con facturación, con una asimetría entre la ruta de error de API sin procesar y la ruta de omisión por enfriamiento:

  • Ruta de error de API sin procesar: isBillingErrorMessage(message: string) — coincidencia regex/cadena contra ERROR_PATTERNS.billing en failover-matches.ts.
  • Ruta de límite de tasa (ya corregida): isPureTransientRateLimitSummary(failure: FallbackSummaryError) — verificación estructural contra attempt.reason === 'rate_limit'.
  • Ruta de omisión por enfriamiento de facturación (rota): No existe una verificación estructural equivalente; depende únicamente de la coincidencia de cadenas isBillingErrorMessage(), que no puede hacer coincidir "has billing issue (skipping all models)".

La secuencia de fallo

  1. La cuota de "uso adicional" personal de OAuth del usuario de Anthropic se agota.
  2. La primera solicitud LLM recibe un error de API sin procesar: 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. El mensaje de error sin procesar coincide con ERROR_PATTERNS.billing → el auth profile entra en billing cooldown window=disabled.
  4. Las solicitudes posteriores activan la lógica de omisión de modelo de respaldo en model-fallback.ts.
  5. Cada modelo candidato se omite con el detalle: "Provider anthropic has billing issue (skipping all models)".
  6. Se construye un FallbackSummaryError con attempt.reason="billing" para cada intento fallido.
  7. En agent-runner-execution.ts, el código llama a isBillingErrorMessage(error.message) — pero "has billing issue" no está en ERROR_PATTERNS.billing.
  8. La verificación de facturación falla, por lo que se toma la ruta de error de respaldo genérica, produciendo "Something went wrong".

Ubicaciones relevantes del código

  • src/core/failover/failover-matches.tsERROR_PATTERNS.billing contiene patrones como "out of extra usage", "insufficient balance", "billing error", pero **no** "has billing issue".
  • src/core/agent-runner-execution.ts — llama a isBillingErrorMessage(message) como la única puerta de clasificación de facturación para la ruta de renderizado de errores.
  • src/core/model-fallback/model-fallback.ts — produce mensajes "Provider X has billing issue (skipping all models)" cuando el enfriamiento de facturación está activo.
  • src/core/failover/failover-matches.tsisPureTransientRateLimitSummary() inspecciona correctamente attempt.reason === 'rate_limit' como un campo estructural, demostrando el patrón correcto que falta en la ruta de facturación.

Por qué la ruta de límite de tasa funciona correctamente

La ruta de límite de tasa ya utiliza el campo estructural attempt.reason:

export function isPureTransientRateLimitSummary(failure: FallbackSummaryError): boolean {
  return failure.attempts.every(a => a.reason === 'rate_limit');
}

Este enfoque es inmune a cambios en las cadenas de mensaje porque inspecciona el campo de clasificación semántica, no el mensaje legible para humanos.

Por qué OAuth agrava el problema

Las cuotas de “uso adicional” personales en claude.ai generalmente son más pequeñas que los presupuestos de API organizacionales. Las cuentas autenticadas con OAuth golpean su cuota personal con más frecuencia que las cuentas de organización autenticadas con clave API, lo que convierte este error en un problema común de experiencia de usuario para la ruta de instalación de OAuth.

🛠️ Solución paso a paso

Estrategia de corrección

Agregar una función de verificación de facturación estructural isPureBillingSummary() a failover-matches.ts, reflejando el patrón existente de isPureTransientRateLimitSummary(). Actualizar agent-runner-execution.ts para usar esta verificación estructural como la puerta principal para la clasificación de facturación, cayendo de vuelta a la coincidencia de cadenas solo para errores de API sin procesar heredados.

Paso 1: Agregar verificación de facturación estructural a failover-matches.ts

Agregar la siguiente función a src/core/failover/failover-matches.ts junto a la isPureTransientRateLimitSummary() existente:

Antes:

export function isPureTransientRateLimitSummary(failure: FallbackSummaryError): boolean {
  return failure.attempts.every(a => a.reason === 'rate_limit');
}

Después:

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');
}

Paso 2: Actualizar la puerta de clasificación de facturación en agent-runner-execution.ts

Ubicar la lógica de clasificación de errores de facturación en src/core/agent-runner-execution.ts. Reemplazar la verificación solo de cadenas con un enfoque estructural primero:

Antes:

const isBilling = isBillingErrorMessage(message);

Después:

// Prefer structural classification (cooldown skip path) over string matching.
const isBilling = error instanceof FallbackSummaryError
  ? isPureBillingSummary(error)
  : isBillingErrorMessage(message);

Asegurar que FallbackSummaryError e isPureBillingSummary estén importados:

import { FallbackSummaryError } from '../model-fallback/types';
import { isPureBillingSummary } from '../failover/failover-matches';

Paso 3: (Mejora opcional) Extender ERROR_PATTERNS.billing

Para asegurar que los errores de API sin procesar a través de la ventana de enfriamiento también se manejen correctamente, extender los patrones de facturación en src/core/failover/failover-matches.ts para incluir la frase de omisión por enfriamiento:

Antes:

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,
  ],
  // ...
};

Después:

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
  ],
  // ...
};

Este tercer paso es defensivo; la corrección principal en los Pasos 1-2 es suficiente porque isPureBillingSummary() corta el flujo antes de que la coincidencia de cadenas llegue al caso de FallbackSummaryError.

Paso 4: Reconstruir y desplegar

npm run build
# or for Docker deployments:
docker build -t openclaw:fixed .

🧪 Verificación

Prueba unitaria: isPureBillingSummary()

Agregar un caso de prueba a 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);
  });
});

Ejecutar el conjunto de pruebas:

npm test -- --testPathPattern="failover-matches"
# Expected: isPureBillingSummary tests pass

Prueba de integración: renderizado de error de enfriamiento de facturación

Simular un escenario de enfriamiento de facturación usando un proveedor de prueba o un AuthProfile simulado:

# 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")

Verificación manual: inspección de logs

Activar el enfriamiento de facturación e inspeccionar los logs del agente para la clasificación corregida:

# 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)

Verificación del código de salida

# 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

⚠️ Errores comunes

  • Solo extender ERROR_PATTERNS sin agregar isPureBillingSummary(): Agregar "has billing issue" a los patrones de cadena funciona como solución alternativa pero es frágil. Si el formato del mensaje de modelo de respaldo cambia en una versión futura (por ejemplo, "Provider X billing cooldown — skipping all models"), el patrón se rompe nuevamente. El enfoque estructural de isPureBillingSummary() es resiliente a cambios en las cadenas de mensaje.
  • Aplicar isPureBillingSummary() sin condiciones: La verificación debe estar protegida con instanceof FallbackSummaryError. Llamarla en una cadena sin formato u otro tipo de error arroja un TypeError. El fallback a isBillingErrorMessage(message) para tipos que no son FallbackSummaryError preserva la compatibilidad con errores de API sin procesar.
  • Asimetría de OAuth vs clave API en pruebas: El error se manifiesta más fácilmente con cuentas autenticadas con OAuth porque las cuotas de "uso adicional" personales son más pequeñas. Las pruebas con claves API de nivel organizacional pueden no reproducir el problema, llevando a una falsa confianza de que una corrección está funcionando. Siempre prueba con escenarios de cuota personal de OAuth y escenarios de agotamiento de clave API.
  • Persistencia del estado de ventana de enfriamiento: El estado de enfriamiento de facturación persiste entre reinicios si está respaldado por un almacén persistente (Redis, SQLite). Asegura que los entornos de prueba reinicien el estado de fallo del perfil de autenticación entre ejecuciones, o el enfriamiento seguirá bloqueando solicitudes incluso después de corregir la ruta del mensaje de error.
  • Cobertura parcial del modelo de respaldo: Si solo algunos proveedores en una cadena de enrutamiento entran en enfriamiento de facturación, el FallbackSummaryError contiene una mezcla de reason: 'billing' y otras razones (por ejemplo, reason: 'timeout'). isPureBillingSummary() devuelve false en este caso mixto. Considera agregar isMostlyBillingSummary() como una heurística secundaria si los fallos mixtos son comunes.
  • Tiempo de montaje de volúmenes Docker: En despliegues Docker, asegúrate de que se use la imagen del contenedor reconstruida (docker build, no solo docker-compose up -d --build si el contexto de construcción está desactualizado). Un error común es editar archivos fuente y solo ejecutar up -d, que usa la imagen existente sin la corrección.
  • La verbosidad de logs enmascara el problema: Si LOG_LEVEL=error está configurado en producción, las líneas detalladas de [model-fallback] model fallback decision pueden suprimirse, haciendo más difícil diagnosticar si se tomó la ruta de omisión de enfriamiento o la ruta de error sin procesar. Configura LOG_LEVEL=debug durante la resolución de problemas.

🔗 Errores relacionados

  • FallbackSummaryError con mensaje "Something went wrong" — El error de respaldo genérico que ven los usuarios cuando ni isBillingErrorMessage() ni isPureRateLimitSummary() coinciden. Indica una brecha de clasificación en la lógica de enrutamiento de errores.
  • No coincidencia de patrón ERROR_PATTERNS.billing — El conjunto de patrones regex en failover-matches.ts que isBillingErrorMessage() usa para la detección de facturación basada en cadenas. No incluir la frase de omisión por enfriamiento fue la causa raíz aquí.
  • PR #61608 — corrección parcial del patrón de facturación — Agregó "out of extra usage" a ERROR_PATTERNS.billing solo para errores de API sin procesar; no abordó la ruta de omisión generada por el enfriamiento.
  • Issue #48526 — Brecha relacionada de clasificación de errores de facturación (posiblemente una instancia anterior del mismo problema de clasificación por coincidencia de patrones vs. clasificación estructural).
  • Issue #64224 — Agotamiento de cuota de facturación de autenticación OAuth causando errores repetidos (probablemente la misma causa raíz, manifestación diferente).
  • Issue #64308 — Escenario de omisión de todos los modelos en modelo de respaldo con salida de error genérica.
  • Issue #62375 — Caso límite del manejo de errores de facturación del proveedor Anthropic con cuotas de uso personal de OAuth.
  • isPureTransientRateLimitSummary() — El patrón correcto que la ruta de facturación debería reflejar. Demuestra el enfoque de inspección del campo estructural attempt.reason que ya estaba implementado para errores de límite de tasa.
  • BILLING_ERROR_USER_MESSAGE — El mensaje esperado visible para el usuario que no se estaba mostrando: "⚠️ 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 — Los metadatos del perfil de autenticación que indican que se ha activado un enfriamiento de facturación, evitando más intentos durante un período de enfriamiento definido.

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.