Skip to content

[Bug]: Provider refusals (Anthropic refusal / OpenAI content_filter) never trigger the model fallback chain — turn dies with generic 'LLM request failed.' #98976

Description

@NOVA-Openclaw

What happened

When a provider returns a refusal (e.g. Anthropic's refusal stop reason from its safety classifiers), the turn ends with a hard LLM request failed. error surfaced to the user. The configured model fallback chain (agents.defaults.model.fallbacks / per-agent model.fallbacks) is never consulted, even though the user's prompt was not processed.

From the user's perspective a refusal is indistinguishable from any other provider failure — the prompt did not get processed — so it should be treated as failover-eligible and retried on the next candidate in the chain.

Evidence (production logs, 2026-07-02)

Three failed runs in one morning on claude-fable-5, all with fallbacks configured:

"error":"LLM request failed.","failoverReason":null,"model":"claude-fable-5","provider":"anthropic",
"rawErrorPreview":"Anthropic refusal (category: bio): API integrators: you can reduce refusals for your users by configuring a fallback model — see https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback"

Note failoverReason: null — no failover classification ever happened. Two of the three were classifier false positives (a music track titled "Species Filter" tripping the bio category). Anthropic's own error copy explicitly recommends falling back to another model.

Why it happens (source analysis)

  • A refusal is not a failed request: the provider returns HTTP 200 with a completed stream whose stop reason is refusal.
  • src/shared/anthropic-refusal.ts (applyAnthropicRefusal, called from src/agents/anthropic-transport-stream.ts, src/llm/providers/anthropic.ts, and src/plugin-sdk/provider-stream-shared.ts) converts it into an assistant message with stopReason: "error", errorMessage: "Anthropic refusal…", and a provider_refusal diagnostic.
  • Model fallback (src/agents/model-fallback.ts) only walks the candidate chain when a request-level error is classified into a FailoverReason (src/agents/embedded-agent-helpers/failover-matches.ts, errors.ts). Since the request "succeeded", no classification occurs and the chain is skipped.
  • Neither failover-matches.ts nor failover-policy.ts mention refusals anywhere; there are no refusal-related failover tests.

The OpenAI transport has the same shape: finish_reason: content_filter is mapped to stopReason: "error" (src/agents/openai-transport-stream.ts) without failover.

Expected behavior

A provider refusal should be classified as failover-eligible (e.g. FailoverReason: "refusal") and the run should continue down the configured fallback chain, same as rate limits / 5xx / timeouts. If all candidates refuse, surface the final refusal message.

Acceptance criteria

  • Anthropic refusal stop reason triggers the model fallback chain when fallbacks are configured
  • OpenAI content_filter finish reason does the same
  • failoverReason is populated (e.g. "refusal") in embedded_run_agent_end diagnostics/logs
  • If every candidate refuses, the user-facing error includes the refusal message (not a generic LLM request failed.)
  • Existing failover behavior for transport/HTTP errors unchanged
  • Tests covering refusal→failover, refusal-exhausted-chain, and no-fallbacks-configured cases

Environment

  • OpenClaw 2026.6.x (nova-openclaw fork tracking upstream), Anthropic API (api_key auth), models: claude-fable-5 primary with claude-opus-4-8/4-7/4-6 + cross-provider fallbacks

Related: #48104 (model safety blocking authorized tasks — adjacent but about policy, not failover routing). We plan to submit a PR for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerAuth, provider routing, model choice, or SecretRef resolution may break.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions