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=disabledTodos 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 contraERROR_PATTERNS.billingenfailover-matches.ts. - Ruta de límite de tasa (ya corregida):
isPureTransientRateLimitSummary(failure: FallbackSummaryError)— verificación estructural contraattempt.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
- La cuota de "uso adicional" personal de OAuth del usuario de Anthropic se agota.
- 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."}} - El mensaje de error sin procesar coincide con
ERROR_PATTERNS.billing→ elauth profileentra enbilling cooldown window=disabled. - Las solicitudes posteriores activan la lógica de omisión de modelo de respaldo en
model-fallback.ts. - Cada modelo candidato se omite con el detalle:
"Provider anthropic has billing issue (skipping all models)". - Se construye un
FallbackSummaryErrorconattempt.reason="billing"para cada intento fallido. - En
agent-runner-execution.ts, el código llama aisBillingErrorMessage(error.message)— pero"has billing issue"no está enERROR_PATTERNS.billing. - 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.ts—ERROR_PATTERNS.billingcontiene patrones como"out of extra usage","insufficient balance","billing error", pero **no**"has billing issue".src/core/agent-runner-execution.ts— llama aisBillingErrorMessage(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.ts—isPureTransientRateLimitSummary()inspecciona correctamenteattempt.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 passPrueba 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 deisPureBillingSummary()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 aisBillingErrorMessage(message)para tipos que no sonFallbackSummaryErrorpreserva 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
FallbackSummaryErrorcontiene una mezcla dereason: 'billing'y otras razones (por ejemplo,reason: 'timeout').isPureBillingSummary()devuelvefalseen este caso mixto. Considera agregarisMostlyBillingSummary()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 solodocker-compose up -d --buildsi el contexto de construcción está desactualizado). Un error común es editar archivos fuente y solo ejecutarup -d, que usa la imagen existente sin la corrección. - La verbosidad de logs enmascara el problema: Si
LOG_LEVEL=errorestá configurado en producción, las líneas detalladas de[model-fallback] model fallback decisionpueden suprimirse, haciendo más difícil diagnosticar si se tomó la ruta de omisión de enfriamiento o la ruta de error sin procesar. ConfiguraLOG_LEVEL=debugdurante la resolución de problemas.
🔗 Errores relacionados
FallbackSummaryErrorcon mensaje"Something went wrong"— El error de respaldo genérico que ven los usuarios cuando niisBillingErrorMessage()niisPureRateLimitSummary()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 enfailover-matches.tsqueisBillingErrorMessage()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"aERROR_PATTERNS.billingsolo 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 estructuralattempt.reasonque 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.