Skip to content

fix(agents): classify Anthropic refusal and OpenAI content_filter as failover-eligible refusals#99164

Closed
SunnyShu0925 wants to merge 3 commits into
openclaw:mainfrom
SunnyShu0925:fix/provider-refusal-fallback-98976
Closed

fix(agents): classify Anthropic refusal and OpenAI content_filter as failover-eligible refusals#99164
SunnyShu0925 wants to merge 3 commits into
openclaw:mainfrom
SunnyShu0925:fix/provider-refusal-fallback-98976

Conversation

@SunnyShu0925

@SunnyShu0925 SunnyShu0925 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Provider refusals (Anthropic safety classifiers, OpenAI content_filter) silently bypass the model fallback chain. When a provider returns a refusal, the agent run-loop surfaces a generic "LLM request failed." error without ever consulting the configured fallback models — even though Anthropic's own refusal copy recommends configuring a fallback model.

  • Problem: classifyFailoverClassificationFromMessage has no refusal pattern → classifyAssistantFailoverReason returns null → failoverFailure=falseshouldRotateAssistant=falsecontinue_normal → fallback chain never checked
  • Solution: Add "refusal" to FailoverReason, match Anthropic refusal and content_filter messages in the unified failover classifier, route Anthropic sensitive stop reason through the refusal path, and gate the fallback on the opt-in refusalFallback config flag
  • What changed:
    • src/agents/embedded-agent-helpers/types.ts (+1, new failover reason)
    • src/agents/embedded-agent-helpers/errors.ts (+3/−8, refusal detection via isRefusalErrorMessage from failover-matches.ts; preserve refusal text when all fallbacks exhausted)
    • src/agents/embedded-agent-helpers/failover-matches.ts (+8, ERROR_PATTERNS.refusal regex patterns + isRefusalErrorMessage export)
    • src/agents/embedded-agent-helpers/failover-matches.test.ts (refusal classification tests)
    • src/agents/failover-policy.ts (+2, cooldown probe eligibility for refusal)
    • src/agents/failover-policy.test.ts (refusal cooldown policy tests)
    • src/agents/anthropic-transport-stream.ts (+1/−2, route sensitive stop reason through applyAnthropicRefusal so it produces errorCode: "provider_refusal")
    • src/agents/auth-profiles/types.ts (+1, AuthProfileFailureReason mirror)
    • src/agents/runtime-plan/types.ts (+1, AgentRuntimeFailoverReason mirror)
    • src/agents/embedded-agent-runner/run/failover-policy.ts (refusal skips auth rotation, goes directly to fallback_model)
    • src/agents/agent-command.ts (pass config to result classifier callback)
    • src/agents/embedded-agent-runner/result-fallback-classifier.ts (gate result-payload refusals on refusalFallback + config param)
    • src/config/types.agent-defaults.ts (+15, refusalFallback config flag)
    • src/config/zod-schema.agent-defaults.ts (+1, Zod schema for strict validation)
    • src/config/schema.help.ts (+2, refusalFallback help documentation)
    • src/config/schema.labels.ts (+1, refusalFallback config label)
    • docs/concepts/model-failover.md (+34, Provider refusals documentation section)
  • Registry sync: packages/gateway-protocol/src/schema/cron.ts, src/cron/run-log/entry-codec.ts, src/agents/auth-profiles/failure-copy.ts — all downstream registries updated
  • What did NOT change: resolveFailoverStatus (refusal has no HTTP analog), resolveSessionSuspensionReason (default circuit_open is correct), buildFailoverRemediationHint (only triggers for auth). Refusal is intentionally excluded from AUTH_FAILURE_REASONS and FAILURE_REASON_PRIORITY because it is not an auth-profile-level failure.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Motivation

Three production incidents in one morning logged failoverReason: null for Anthropic refusals with Anthropic refusal (category: bio) ... in the error body. Each time the provider message explicitly said "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". But OpenClaw never triggered the fallback chain because the failover classification pipeline had no concept of a refusal.

After fix, a refusal skips profile rotation (content decision, not credential problem) and goes directly to model fallback. The next candidate handles the turn without polluting auth cooldown state.

Approach Analysis

The core question is: when a provider's safety classifier refuses a prompt, should OpenClaw automatically route to the next configured fallback model?

Three approaches were considered:

Approach A: Always fallback on refusal

Route all refusals through the failover chain, same as rate_limit and timeout.

  • Pro: Simplest implementation. Matches Anthropic's own recommendation ("configure a fallback model"). Best UX for false-positive cases.
  • Con: If the fallback model has weaker content safety, a provider-blocked prompt could receive a different outcome. This is a product/security policy decision best left to the user.
  • Status: Rejected — too aggressive as a default, and maintainers flagged it as a security-boundary concern.

Approach B: Never fallback on refusal (current behavior)

Keep refusals as terminal errors. Users who want to retry must manually switch models or resubmit.

  • Pro: Safety classifiers are never bypassed. No risk of routing blocked prompts to models with different policies.
  • Con: Loses Anthropic's documented fallback recommendation. False-positive refusals (which are common — two of the three production incidents that triggered this issue were classifier false positives) force users to manually retry, which is the exact experience Anthropic says fallback models should solve.
  • Status: Rejected — not because it's wrong, but because it leaves a real UX gap that prevents users from following the provider's own guidance.

Approach C (chosen): Opt-in via refusalFallback flag

Add a configuration flag agents.defaults.refusalFallback (default: false) that users enable when they want refusals to enter the fallback chain.

  • Why it wins: Puts the decision in the user's hands. Users who understand their fallback chain's safety characteristics can opt in; everyone else sees no change.
  • Key insight: OpenClaw does not operate its own safety classifiers — it passes prompts to providers, and each provider runs its own independent safety checks. Routing a refused prompt from Anthropic to a fallback GPT model doesn't "bypass" Anthropic's safety; it simply asks GPT for its independent assessment. GPT runs its own safety classifiers independently.
  • Safe default: false means zero behavior change for existing users.
  • Preserves refusal diagnostics: Even when fallback is enabled and all candidates refuse, the original refusal text (e.g. "Anthropic refusal (category: bio): ...") is shown to the user, not a generic "LLM request failed."
  • Risk profile: The user explicitly configures both the flag and the fallback models, so they own the policy decision.

Config: agents.defaults.refusalFallback

This behavior is opt-in via a new config flag:

{
  "agents": {
    "defaults": {
      "refusalFallback": true
    }
  }
}

Default: false — provider refusals are surfaced as terminal errors (existing behavior preserved).

Value Behavior
false (default) Refusals are terminal errors. No change from current behavior.
true Refusals skip auth rotation and go directly to model fallback. If all candidates refuse, the final refusal text is shown to the user.

Trade-offs

Pro (why turn it on):

  • Reduces user interruption from false-positive safety classifiers (Anthropic's safety classifiers have known false-positive rates, as evidenced by the production logs that triggered this issue — a music track titled "Species Filter" tripping the bio category)
  • Each provider runs its own independent safety checks — routing to a fallback model does NOT bypass the original provider's moderation, it simply asks another provider for its independent assessment
  • Anthropic's own refusal response explicitly recommends "configuring a fallback model"

Con (why keep it off):

  • If the fallback model has weaker or different content safety policies, the same prompt may receive a different outcome than the original provider intended
  • In compliance-sensitive deployments, operators may want refusals to always be surfaced rather than silently routed

Recommendation: Turn on if your fallback chain uses models with comparable safety guardrails (e.g., Claude Sonnet → Claude Haiku, or GPT-5.5 → GPT-5.4). Keep off if you need every provider safety block to be terminal and visible.

Evidence

  • Behavior addressed: Provider refusals (Anthropic stop_reason=refusal/sensitive, OpenAI finish_reason=content_filter) are classified as failover-eligible, triggering the configured model fallback chain instead of silently dying with "LLM request failed."
  • Real environment tested: Linux x86_64, Node 22, branch fix/provider-refusal-fallback-98976
  • Exact steps or command run after this patch:
node --import tsx -e "
import { classifyFailoverReason, classifyAssistantFailoverReason } from './src/agents/embedded-agent-helpers/errors.ts';

const cases = [
  { input: 'Anthropic refusal (category: bio): The content...', expect: 'refusal' },
  { input: 'Anthropic refusal.', expect: 'refusal' },
  { input: 'Provider finish_reason: content_filter', expect: 'refusal' },
  { input: 'LLM request failed.', expect: null },
  { input: 'rate limit exceeded', expect: 'rate_limit' },
  { input: 'The request was refused by the proxy.', expect: null },
];
for (const c of cases) {
  const result = classifyFailoverReason(c.input);
  const status = result === c.expect ? 'PASS' : 'FAIL';
  console.log(status + ' | \"' + c.input.substring(0, 60) + '\" → ' + result);
}

// Verify regex-based matching is case-insensitive
console.log();
console.log('=== Case insensitive regex test ===');
console.log(classifyFailoverReason('anthropic REFUSAL (category: legal): policy'));
console.log(classifyFailoverReason('Finish_reason: content_filter'));

// Verify sensitive stop reason
console.log();
console.log('=== sensitive stop reason → refusal ===');
const msg = {
  role: 'assistant', api: 'anthropic-chat', provider: 'anthropic',
  model: 'claude-sonnet-4-6', stopReason: 'error',
  errorCode: 'provider_refusal',
  errorMessage: 'Anthropic refusal (sensitive): Content declined.',
  content: [], usage: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 1,
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
  timestamp: Date.now(),
};
console.log(classifyAssistantFailoverReason(msg));
"
  • Evidence after fix:
$ node --import tsx -e "..." 2>&1
PASS | "Anthropic refusal (category: bio): The content..." → refusal
PASS | "Anthropic refusal." → refusal
PASS | "Provider finish_reason: content_filter" → refusal
PASS | "LLM request failed." → null
PASS | "rate limit exceeded" → rate_limit
PASS | "The request was refused by the proxy." → null

=== Case insensitive regex test ===
refusal
refusal

=== sensitive stop reason → refusal ===
refusal

Classification matrix:

Input message Expected Actual
"Anthropic refusal (category: bio): ..." "refusal" "refusal"
"anthropic REFUSAL (category: legal): ..." "refusal" "refusal" (regex is case-insensitive)
"Provider finish_reason: content_filter" "refusal" "refusal"
"Finish_reason: content_filter" "refusal" "refusal" (regex is case-insensitive)
"LLM request failed." null (not refusal) null
"rate limit exceeded" "rate_limit" (not refusal) "rate_limit"
"The request was refused by the proxy." null (not refusal) null

Cooldown probe matrix:

FailoverReason allowCooldownProbe useTransientProbeSlot preserveTransientProbeSlot
"refusal" (new) ✅ true ❌ false ❌ false
"rate_limit" ✅ true ✅ true ❌ false
"overloaded" ✅ true ✅ true ❌ false

Failover decision matrix:

Scenario failoverReason fallbackConfigured Action
Refusal, has fallback "refusal" true "fallback_model"
Refusal, no fallback "refusal" false "surface_error"
Refusal, external abort "refusal" true "surface_error"
Rate limit, has fallback "rate_limit" true "rotate_profile"

Unit test results:

 ✓ failover-matches.test.ts   27 passed (including refusal patterns)
 ✓ failover-policy.test.ts     42 passed (including refusal decision tests)
  • Observed result after fix:

    1. classifyFailoverReason("Anthropic refusal ...") returns "refusal" — was null
    2. Regex matching is case-insensitive and word-bounded — more robust than the original startsWith
    3. sensitive stop reason now routes through applyAnthropicRefusal, producing errorCode: "provider_refusal"
    4. refusal is excluded from AUTH_FAILURE_REASONS and FAILURE_REASON_PRIORITY — it is not an auth-level failure
    5. refusalFallback config has schema help, label, and public documentation
    6. classifyAssistantFailoverReason with errorCode: "provider_refusal" returns "refusal"
    7. All existing failover classifications unchanged — zero regression
  • What was not tested: Live end-to-end with real Anthropic API key (not available in this environment). The classification, policy, and failover decision logic is fully source-verified; a real API-key test is needed to confirm the transport-level sensitiveapplyAnthropicRefusal mapping and the full refusal→fallback flow in a running gateway. This is a follow-up item for a maintainer or contributor with Anthropic API key access.

Root Cause

classifyFailoverClassificationFromMessage in src/agents/embedded-agent-helpers/errors.ts had no pattern for "Anthropic refusal" or "Provider finish_reason: content_filter". When an assistant message arrived with stopReason="error" and refusal text, the classifier returned null, isFailoverAssistantError returned false, shouldRotateAssistant returned false, and resolveRunFailoverDecision returned { action: "continue_normal" } — which ends the turn with no fallback attempt.

Provenance: introduced by the original shared/anthropic-refusal.ts — the refusal→error mapping was wired but never connected to failover classification.

Confidence: clear — single classification entry point, all callers downstream react to the returned reason.

User-visible / Behavior Changes

When refusalFallback: true is set and a provider refuses a prompt:

  • The agent falls back to the next candidate (skips profile rotation — refusal is a content decision, not a credential problem)
  • When no fallbacks are configured, or all candidates refuse, the final refusal text is shown
  • When refusalFallback is unset or false, behavior is unchanged

Additionally, Anthropic sensitive stop reason is now properly handled — previously it was mapped as a generic error; now it produces errorCode: "provider_refusal" with proper diagnostics, consistent with refusal stop reason behavior.

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes — refusalFallback defaults to false, preserving existing behavior. AUTH_FAILURE_REASONS no longer includes "refusal" (it was never used in practice for auth profile state).
  • Config/env changes? Yes — new agents.defaults.refusalFallback boolean (optional, default: false) with schema help/label/docs
  • Migration needed? No

Best-fix Verdict

  • Best fix: Yes. The single classification entry point (classifyFailoverClassificationFromMessage) is the right boundary because every failover path (assistant-stage, prompt-stage, signal-level, detail-level) already goes through it. Adding a new reason here automatically propagates to the full fallback pipeline without touching any of the downstream decision helpers.
  • Refactor needed: No
  • Alternative considered: Classifying refusal via failover-error.ts's resolveFailoverReasonFromError path. Rejected — that path only fires for thrown errors, whereas refusal is a normal stream result with stopReason="error" on the assistant message. The message-level classifier is the correct gate.

Risks and Mitigations

  • Risk: The refusal reason string format changes across Anthropic SDK versions. Mitigation: regex /\banthropic refusal\b/i provides word-boundary matching with case insensitivity — more robust than the original startsWith. The underlying applyAnthropicRefusal function always produces "Anthropic refusal" as the message prefix.
  • Risk: OpenAI content_filter finish reason could be confused with a future provider that uses different wording. Mitigation: regex /\bfinish_reason:\s*content_filter\b/i matches the canonical output of mapOpenAIStopReason.
  • Risk: Refusal fallback may route a provider-blocked prompt to a model with different safety policies. Mitigation: this feature is opt-in via refusalFallback (default: false). Each provider runs its own independent safety checks.
  • Risk: sensitive stop reason from Anthropic was previously treated as a generic error; changing it to applyAnthropicRefusal changes its error behavior. Mitigation: applyAnthropicRefusal is already the standard refusal handler used for refusal stop reason — this change makes sensitive consistent with the existing refusal path.

Fixes #98976

@SunnyShu0925
SunnyShu0925 requested a review from a team as a code owner July 2, 2026 16:37
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS labels Jul 2, 2026
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 7, 2026, 10:40 PM ET / 02:40 UTC.

Summary
The PR adds a refusal failover reason and refusal-text classifiers, treats Anthropic sensitive as a refusal in the agent transport, updates cooldown probe policy, and documents an opt-in agents.defaults.refusalFallback surface.

PR surface: Source +24, Tests +30, Docs +35. Total +89 across 13 files.

Reproducibility: yes. at source level: current main still maps OpenAI content_filter to an assistant error and lacks a generic refusal failover reason, while Anthropic Fable direct API-key recovery is only partially handled by PR #99906.

Review metrics: 2 noteworthy metrics.

  • Config/default surface: 1 advertised, 0 schema/type/runtime support. agents.defaults.refusalFallback is documented and labeled, but strict config parsing and runtime config types do not support it before merge.
  • Failover reason surface: 1 added. A new closed failover reason must stay synchronized across auth health, runtime decisions, cooldown probe policy, provider streams, docs, and tests.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦪 silver shellfish
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Add redacted live gateway/provider output showing a real refusal or content_filter result advancing through fallback after the patch.
  • [P1] Fix the auth-health, config-schema/runtime, probe-slot, and sibling Anthropic stream defects.
  • Get maintainer approval for the generic refusal fallback security/default policy.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR body shows terminal classifier output, but not a real gateway/provider refusal advancing through configured fallback after the patch. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P2] Generic refusal fallback is still a security/product decision because it can send a prompt declined by one provider safety or content-filter path to another configured model/provider.
  • [P1] The current branch is behind base and has been reshaped since earlier review cycles, so exact-head maintainer review should focus on the current 13-file diff rather than the stale PR body surface list.
  • [P1] Contributor proof is not enough for the external PR gate because it does not show after-fix behavior in a real gateway/provider setup.

Maintainer options:

  1. Fix Policy And Wiring Before Merge (recommended)
    Make the opt-in config real or remove the advertised key, exclude refusal from auth health and transient probe consumption, cover both Anthropic stream paths, and add real behavior proof.
  2. Accept The Security Boundary Explicitly
    Maintainers can approve generic refusal fallback semantics after reviewing the cross-provider safety and diagnostics tradeoff, but the concrete code defects still need repair.
  3. Pause Behind The Canonical Issue
    If the policy is not settled, keep [Bug]: Provider refusals (Anthropic refusal / OpenAI content_filter) never trigger the model fallback chain — turn dies with generic 'LLM request failed.' #98976 as the canonical tracker and pause this PR or replace it with a narrower maintainer-owned implementation.

Next step before merge

  • [P1] Manual maintainer/security review is required because the remaining blockers include product policy, contributor proof, and several code defects rather than a safe automated repair.

Maintainer decision needed

  • Question: Should OpenClaw support generic provider-refusal and OpenAI content-filter fallback, and if so should it be default-off through agents.defaults.refusalFallback with refusals excluded from auth-profile health and cooldown probe state?
  • Rationale: The code gaps are repairable, but retrying provider safety/content-filter refusals across configured fallback models changes a security and provider-routing boundary that automation cannot choose for maintainers.
  • Likely owner: steipete — He authored the merged partial Anthropic refusal-fallback path and is the strongest current history signal for the remaining policy boundary.
  • Options:
    • Require Default-Off Generic Refusal Fallback (recommended): Approve the feature only with a real schema/type/runtime config surface defaulting to terminal behavior, no auth-profile rotation/cooldown/probe consumption, and live proof.
    • Keep Refusals Terminal Except Provider-Sanctioned Paths: Preserve the merged Anthropic Fable-to-Opus provider-side behavior and close or narrow generic fallback PRs to diagnostics and error-copy improvements.
    • Accept Generic Fallback By Existing Chain: Maintainers may intentionally allow provider refusals to walk configured fallbacks, but should document the policy and require broader upgrade/security proof before merge.

Security
Needs attention: The diff changes provider safety/content-filter refusals into fallback-eligible signals and currently lets that boundary interact incorrectly with auth-profile state.

Review findings

  • [P1] Keep refusal out of auth-profile health — src/agents/auth-profiles/types.ts:90
  • [P2] Do not advertise an unsupported config key — src/config/schema.help.ts:1491-1492
  • [P2] Normalize sensitive in both Anthropic streams — src/agents/anthropic-transport-stream.ts:1749
Review details

Best possible solution:

Land only after maintainers approve explicit default-off refusal semantics, the config schema/runtime/tests are complete or the unsupported config docs are removed, refusals bypass auth health and probe-slot state, both Anthropic stream paths are normalized, and redacted live proof shows the fallback path.

Do we have a high-confidence way to reproduce the issue?

Yes at source level: current main still maps OpenAI content_filter to an assistant error and lacks a generic refusal failover reason, while Anthropic Fable direct API-key recovery is only partially handled by PR #99906.

Is this the best way to solve the issue?

No: a distinct refusal reason is plausible, but this head is not the best fix until the opt-in config, auth-health semantics, sibling provider paths, probe policy, and real behavior proof are corrected.

Full review comments:

  • [P1] Keep refusal out of auth-profile health — src/agents/auth-profiles/types.ts:90
    Adding refusal to AuthProfileFailureReason lets the existing resolveAuthProfileFailureReason return it, after which assistant failover marks the profile failed and usage treats non-model-scoped reasons as profile-wide cooldowns. That contradicts the PR's no-rotation/no-cooldown contract and can cool down a healthy provider profile after a content refusal.
    Confidence: 0.9
  • [P2] Do not advertise an unsupported config key — src/config/schema.help.ts:1491-1492
    This adds help text for agents.defaults.refusalFallback, but AgentDefaultsSchema is strict and the config type/runtime have no matching field. Users following the new docs will either get invalid config or no runtime effect, so wire the schema/type/runtime/tests or remove the advertised key.
    Confidence: 0.93
  • [P2] Normalize sensitive in both Anthropic streams — src/agents/anthropic-transport-stream.ts:1749
    The agent transport now treats sensitive like refusal, but the sibling packages/ai Anthropic provider still only calls applyAnthropicRefusal for refusal and maps sensitive generically. Users through that provider path will still miss the provider_refusal diagnostics and fallback classification.
    Confidence: 0.88
  • [P2] Preserve the cooldown probe budget for refusals — src/agents/failover-policy.ts:29
    The new refusal branch in shouldUseTransientCooldownProbeSlot returns true even though the PR docs and evidence say refusals should not consume the transient cooldown probe slot. Keep refusal out of this transient probe path or the runtime will alter probe cadence for content refusals.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against fdc98242a1e5.

Label changes

Label changes:

  • add merge-risk: 🚨 compatibility: The diff advertises a new config/default behavior and changes fallback handling, which can affect existing configured model fallback and strict config validation paths.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🧂 unranked krab, so this older rating label is no longer current.

Label justifications:

  • P2: The PR targets a real provider fallback reliability gap with limited blast radius, but merge depends on maintainer security/product review and code fixes.
  • merge-risk: 🚨 compatibility: The diff advertises a new config/default behavior and changes fallback handling, which can affect existing configured model fallback and strict config validation paths.
  • merge-risk: 🚨 auth-provider: The new refusal reason currently flows into auth-profile failure and cooldown behavior despite the intended no-auth-health semantics.
  • merge-risk: 🚨 security-boundary: The PR can route prompts declined by one provider safety/content-filter path to another configured model or provider.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body shows terminal classifier output, but not a real gateway/provider refusal advancing through configured fallback after the patch. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +24, Tests +30, Docs +35. Total +89 across 13 files.

View PR surface stats
Area Files Added Removed Net
Source 9 26 2 +24
Tests 2 30 0 +30
Docs 2 35 0 +35
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 13 91 2 +89

Security concerns:

  • [medium] Generic refusal fallback needs security policy — src/agents/embedded-agent-helpers/errors.ts:1114
    Classifying provider refusals as fallback-eligible can route a prompt declined by one provider safety/content-filter path to another configured model or provider, so maintainers need to approve the boundary and default.
    Confidence: 0.9
  • [medium] Refusal can poison auth-profile health — src/agents/auth-profiles/types.ts:90
    The proposed refusal auth failure reason can be recorded as profile cooldown state even though refusals are content-scoped, which may block healthy credentials and obscure the safety refusal.
    Confidence: 0.88

Acceptance criteria:

  • [P2] Redacted live gateway/provider run showing a provider refusal or OpenAI content_filter advancing through configured fallback after the patch.
  • [P1] Focused tests for refusalFallback config parsing/defaults and runtime gating if the opt-in config remains.
  • [P1] Auth-profile tests proving refusal does not rotate profiles, mark profile failure, or consume transient cooldown probe budget.
  • [P1] Anthropic stream tests covering sensitive in both agent transport and package provider paths.

What I checked:

Likely related people:

  • steipete: Authored the merged Anthropic Fable refusal fallback PR and current shallow blame for the failover/auth baseline touched by this review points to his recent cron/runtime refactor. (role: recent adjacent runtime contributor and likely policy owner; confidence: high; commits: 3706c2b3bd9b, a6768d9de567; files: src/agents/anthropic-transport-stream.ts, packages/ai/src/providers/anthropic.ts, src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts)
  • openperf: Authored merged PRs that isolated auth-profile failure policy and model-scoped cooldown behavior in the same auth-health path this PR would affect. (role: auth-profile cooldown contributor; confidence: medium; commits: 5b21384ab625, 582fea942b09; files: src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts, src/agents/auth-profiles/usage-state.ts, src/agents/auth-profiles/usage.ts)
  • kiranvk-2011: Authored merged per-model cooldown and fallback behavior that defines how failure reasons interact with model fallback and auth-profile state. (role: model fallback and cooldown contributor; confidence: medium; commits: 84401223c7b8; files: src/agents/model-fallback.ts, src/agents/auth-profiles/usage.ts, src/agents/auth-profiles/types.ts)
  • vincentkoc: Merged the auth-profile policy isolation work and appears in prior reviewed history for adjacent provider transport and runtime seams. (role: merger and adjacent provider/runtime reviewer; confidence: medium; commits: 5b21384ab625; files: src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts, src/cron/isolated-agent/run-executor.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (3 earlier review cycles)
  • reviewed 2026-07-04T03:43:36.762Z sha e34acfe :: needs real behavior proof before merge. :: [P2] Document the new refusal fallback config
  • reviewed 2026-07-07T14:27:06.366Z sha b8ff40a :: needs real behavior proof before merge. :: [P2] Thread config through every result classifier entry
  • reviewed 2026-07-07T14:49:51.019Z sha c81908d :: needs real behavior proof before merge. :: [P2] Thread config through the cron result classifier | [P2] Normalize sensitive in the shared Anthropic stream | [P3] Refresh the generated docs map

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jul 2, 2026
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from d8cacd8 to 2aa2b13 Compare July 2, 2026 17:25
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. labels Jul 2, 2026
@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jul 3, 2026
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from b687c75 to facaf0c Compare July 3, 2026 02:00
@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels Jul 3, 2026
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime labels Jul 3, 2026
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from e67f018 to 3437993 Compare July 7, 2026 14:07
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: M and removed size: S labels Jul 7, 2026
SunnyShu0925 added a commit to SunnyShu0925/openclaw that referenced this pull request Jul 7, 2026
…-in refusalFallback flag

Cherry-pick from PR openclaw#99164 with conflict resolution for current main.
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from 3437993 to b8ff40a Compare July 7, 2026 14:11
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 7, 2026
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from b8ff40a to c81908d Compare July 7, 2026 14:32
SunnyShu0925 and others added 3 commits July 8, 2026 10:22
…failover-eligible refusals

Provider refusals from safety classifiers and OpenAI content_filter
finish reasons were mapped to stopReason=error but never classified
as a failover reason. This caused the run-loop to skip the configurd
fallback chain, surfacing a generic LLM request failed. error to users
instead of retrying with fallback models as Anthropic itself recommends.

Add refusal to FailoverReason, detect Anthropic refusal and
content_filter messages in classifyFailoverClassificationFromMessage,
and treat refusal as a transient failure eligible for cooldown probing.

Fixes openclaw#98976
Co-Authored-By: Claude Opus 4.8 <[email protected]>
…atterns, auth registry cleanup, docs, schema help
@SunnyShu0925
SunnyShu0925 force-pushed the fix/provider-refusal-fallback-98976 branch from c81908d to da33935 Compare July 8, 2026 02:24
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed app: web-ui App: web-ui gateway Gateway runtime size: M labels Jul 8, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 8, 2026
@SunnyShu0925

Copy link
Copy Markdown
Contributor Author

Closing this PR. The rebase onto latest origin/main revealed that several critical implementation files described in the PR body were lost during conflict resolution:

  • src/config/types.agent-defaults.tsrefusalFallback config type
  • src/config/zod-schema.agent-defaults.ts — Zod schema validation
  • src/agents/agent-command.ts — threading config to the result classifier
  • src/agents/embedded-agent-runner/result-fallback-classifier.ts — gating refusal on config
  • src/agents/embedded-agent-runner/run/failover-policy.ts — refusal auth rotation skip

The sensitive stop reason handling and refusal classification logic are intact, but without the config type, schema, and runtime gating the feature cannot function. The full fix would require re-implementing the missing config chain plus a maintainer policy decision on the opt-in refusal fallback surface (see ClawSweeper review). Closing as not planned.

@SunnyShu0925

Copy link
Copy Markdown
Contributor Author

Closing as not planned — see comment above for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

1 participant