Skip to content

[Bug]: #79396

@slawa19

Description

@slawa19

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

buildRateLimitCooldownMessage() does not check isPureBillingSummary(err) or isBillingErrorMessage(message), so billing failures (e.g., exhausted API balance) are surfaced to the user as rate-limit errors: "⚠️ All models are temporarily rate-limited. Please try again in a few minutes." The misleading message tells the user to wait when they should top up their balance instead.

Steps to reproduce
Configure OpenClaw with a primary model provider and a fallback provider whose API balance is exhausted (negative balance / no credits).
Trigger a model request that causes the primary to fail (e.g., rate-limited) so the fallback is attempted.
Observe the user-visible error message.
Concrete repro from our instance:

agents.defaults.model.primary: "openrouter/deepseek/deepseek-v4-pro" (hit OpenRouter rate limit)
agents.defaults.model.fallbacks: ["openai/gpt-5.4"] (OpenAI balance was negative)
User message: "включи в кабинете теплый свет максимальной яркости"
Result: received "⚠️ All models are temporarily rate-limited. Please try again in a few minutes."
Trajectory evidence: RUN 22 had finalStatus: "success", assistantTexts: [], zero token usage — the model was never actually called, the runtime generated the misleading text.

Steps to reproduce

Configure OpenClaw with a primary model provider and a fallback provider whose API balance is exhausted (negative balance / no credits).
Trigger a model request that causes the primary to fail (e.g., rate-limited) so the fallback is attempted.
Observe the user-visible error message.

Concrete repro from our instance:

agents.defaults.model.primary: "openrouter/deepseek/deepseek-v4-pro" (hit OpenRouter rate limit)
agents.defaults.model.fallbacks: ["openai/gpt-5.4"] (OpenAI balance was negative)
User message: "включи в кабинете теплый свет максимальной яркости"
Result: received "⚠️ All models are temporarily rate-limited. Please try again in a few minutes."
Trajectory evidence: RUN 22 had finalStatus: "success", assistantTexts: [], zero token usage — the model was never actually called, the runtime generated the misleading text.

Expected behavior

When all fallback attempts fail with reason: "billing", the user should see a billing-specific message such as BILLING_ERROR_USER_MESSAGE (already defined in the codebase: "⚠️ API provider returned a billing error — your API key has run out of credits or has an insufficient balance..."). The user should NOT be told to "try again in a few minutes" for an exhausted balance.

Actual behavior

The user sees "⚠️ All models are temporarily rate-limited. Please try again in a few minutes." regardless of whether the underlying failures are rate-limit, billing, or a mix. In our case, the OpenAI fallback returned a billing error (negative balance), but the message gave no indication of a billing problem.

OpenClaw version

2026.5.7 (npm global, commit 8b2a6e5)

Operating system

Debian GNU/Linux 12 (bookworm), kernel Linux 6.12.85-haos

Install method

npm global (openclaw@2026.5.7)

Model

Primary: openrouter/deepseek/deepseek-v4-pro Fallback: openai/gpt-5.4

Provider / routing chain

Primary: openclaw -> openrouter -> deepseek Fallback: openclaw -> openai

Additional provider/model setup details

agents.defaults.thinkingDefault: "off"
Only two models in runtime catalog (no models allowlist set).
OpenAI fallback balance was exhausted at time of failure.

Logs, screenshots, and evidence

File: dist/agent-runner.runtime-*.js

buildRateLimitCooldownMessage (line 381) — no billing check:

function buildRateLimitCooldownMessage(err) {
    const codexUsageLimitMessage = extractCodexUsageLimitErrorMessage(err);
    if (codexUsageLimitMessage) return codexUsageLimitMessage;
    if (!isFallbackSummaryError(err)) 
        return "⚠️ All models are temporarily rate-limited...";
    // ... cooldown expiry logic ...
    return "⚠️ All models are temporarily rate-limited...";
}


function isPureBillingSummary(err) {
    return isFallbackSummaryError(err) 
        && err.attempts.length > 0 
        && err.attempts.every(a => a.reason === "billing");
}

const BILLING_ERROR_USER_MESSAGE = formatBillingErrorMessage();
// "⚠️ API provider returned a billing error — your API key has run out
//  of credits or has an insufficient balance..."

Impact and severity

No response

Additional information

Suggested fix: add a billing check at the top of buildRateLimitCooldownMessage, similar to the existing Codex usage-limit check:

Copy
function buildRateLimitCooldownMessage(err) {
const codexUsageLimitMessage = extractCodexUsageLimitErrorMessage(err);
if (codexUsageLimitMessage) return codexUsageLimitMessage;

// ADD: check for billing first
const message = formatErrorMessage(err);
if (isFallbackSummaryError(err) && isPureBillingSummary(err))
    return BILLING_ERROR_USER_MESSAGE;
if (isBillingErrorMessage(message))
    return BILLING_ERROR_USER_MESSAGE;

if (!isFallbackSummaryError(err)) 
    return "⚠️ All models are temporarily rate-limited...";
// ... rest unchanged

}
Last known good: NOT_ENOUGH_INFO (behavior appears to have always been present; the billing-specific message path exists in some callers but not in buildRateLimitCooldownMessage itself).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions