Building Fallback-Aware Agents: Handling Fable 5's Refusal Stop Reason
Fable 5 refusals arrive as HTTP 200 with stop_reason refusal — not an error. Check it before reading content, and wire a server-side or client-side fallback to Opus 4.8. Code and a checklist inside.
Building Fallback-Aware Agents: Handling Fable 5's Refusal Stop Reason
There's a failure mode in Claude Fable 5 that won't show up in your happy-path testing and won't throw an exception when it bites. It's a refusal, and it arrives as a perfectly ordinary HTTP 200.
Fable 5's safety classifiers can decline a request. When they do, the API returns a 200 response with stop_reason: "refusal" and a stop_details object naming the category — things like "cyber", "bio", "reasoning_extraction", or null. Because it's a 200 and not a 4xx or 5xx, your error handling never fires. Code that does the natural thing — call the model, then read response.content — will happily treat a refusal as if it were an answer, passing empty or partial text downstream into whatever parses it next.
For a chatbot, that's an awkward blank reply. For an autonomous agent that acts on the model's output — commits code, files a ticket, sends an email — an unhandled refusal is a bug that ships. As of July 2026, if you run Fable 5 in production, handling this correctly is table stakes. This piece is the pattern.
Key Takeaways
- A refusal is
HTTP 200, not an error.stop_reason: "refusal", with a category instop_details. Your try/except won't catch it. - Always check
stop_reasonbefore readingcontent. Make it the first thing your response handler does. - Refusals fire before or mid-stream. Pre-output refusals have empty content and aren't billed; mid-stream ones bill for partial output you must discard.
- Fallbacks are opt-in and recommended. Server-side
fallbacksre-runs the request on Opus 4.8 in one call; SDK middleware does it client-side on Bedrock/Vertex/Foundry. - False positives happen — under ~5% of sessions on average, often on benign work adjacent to cyber or bio topics.
What a refusal actually is
The classifiers target offensive cybersecurity and biology/life-sciences content. That's the intent. The practical reality for builders is that benign adjacent work can trip a false positive. A security team's defensive tooling, a bioinformatics pipeline, a pen-test writeup for an authorized engagement — all sit near the classifier boundary and can occasionally get declined even though they're legitimate.
Anthropic reports refusals trigger in under ~5% of sessions on average. That's low enough to feel invisible in a demo and high enough to be a daily event at scale. If your agent makes, say, 500 model calls a day, "under 5%" is potentially dozens of declines you need to handle without corrupting a workflow.
Two timing cases matter for your billing and your buffers:
- Before any output:
contentis empty and the request is not billed. Cleanest case. - Mid-stream: the model started, then the classifier cut in. You're billed for the partial output, and you must discard it — don't parse or persist half an answer.
Rule one: check stop_reason first
Every response handler for Fable 5 should branch on stop_reason before touching content. Treat it like checking a status code before reading a body. Here's the shape in Python:
response = client.messages.create(
model="claude-fable-5",
max_tokens=16000,
messages=[{"role": "user", "content": user_prompt}],
)
if response.stop_reason == "refusal":
category = getattr(response, "stop_details", None)
log.warning("Fable 5 declined: %s", category)
# Do NOT read response.content as an answer — route to fallback/handler
return handle_refusal(category, user_prompt)
# Only now is it safe to use the content
answer = response.content
The comment is the whole lesson. The instant you let content be read before the stop_reason branch, you've reintroduced the bug. In a streaming handler, the same rule applies at the end of the stream: check the final stop_reason before committing anything you accumulated, and drop the buffer if it's a refusal.
Rule two: fall back to Opus 4.8
A refusal handler that just logs and gives up turns a <5% event into a <5% outage. Because false positives are the common case for legitimate work, the right response is usually to retry the same request on a fallback model. Anthropic makes this opt-in and provides two paths.
Server-side fallbacks (Claude API + Claude Platform on AWS)
Add the beta header and a fallbacks list. If Fable 5 refuses, the API re-runs the same request on the fallback model — Opus 4.8 at launch — inside the same call. A "fallback credit" reprices the cache-switch cost so you're not double-charged for the prompt cache.
response = client.beta.messages.create(
model="claude-fable-5",
max_tokens=16000,
betas=["server-side-fallback-2026-06-01"],
fallbacks=[{"model": "claude-opus-4-8"}],
messages=[{"role": "user", "content": "..."}],
)
if response.stop_reason == "refusal":
... # the whole chain declined — Fable 5 AND the fallback
Note what the stop_reason check means here: if you still see refusal after configuring fallbacks, it means both Fable 5 and Opus 4.8 declined. That's a strong signal the request genuinely crosses a policy line — treat it as a real refusal, not a false positive, and surface it to the user rather than retrying harder.
Client-side middleware (Bedrock / Vertex / Foundry)
On the cloud platforms, use the SDK's BetaRefusalFallbackMiddleware instead. It wraps your client and performs the same fallback on the client side, so you get equivalent behavior where the server-side param isn't available. Same principle, different layer.
Which path should you pick?
| Deployment | Use | Why |
|---|---|---|
| Claude API direct | Server-side fallbacks param | One call, fallback credit handles cache repricing |
| Claude Platform on AWS | Server-side fallbacks param | Supported alongside the direct API |
| Bedrock / Vertex / Foundry | BetaRefusalFallbackMiddleware | Server-side param not available; middleware fills the gap |
| Any, want custom logic | Manual stop_reason branch | Full control — e.g. route to a human, not another model |
The manual path matters when a fallback model isn't the right answer. For genuinely sensitive categories you may want the refusal to stand and escalate to a human reviewer. Fallbacks are for reliability against false positives, not a tool for laundering a request past a classifier.
Don't confuse a refusal with a normal stop
refusal is one of several stop_reason values, and your handler needs to tell them apart. A response that stops at end_turn finished normally. One that stops at max_tokens ran out of room — you may want to continue it. refusal means a classifier declined, and it's the only one where reading content as an answer is actively wrong. Branch on the specific value; don't treat "not end_turn" as a single error bucket, because "out of tokens" and "declined" need opposite responses — one you resume, the other you discard and fall back.
There's also a cost nuance worth knowing before you turn fallbacks on everywhere. A refused-before-output request isn't billed, so a pure false-positive that never produced text costs you nothing on the Fable 5 side. When the server-side fallback fires, a "fallback credit" reprices the cache-switch cost so you aren't double-charged for re-sending a prompt that was already cached. That makes fallbacks cheap enough to leave on by default for most agents — the reliability is almost free, and the failure it prevents (an agent acting on an empty answer) is expensive.
The handling checklist
Wire this into any agent or skill that calls Fable 5:
- Branch on
stop_reasonfirst — before reading, parsing, or persistingcontent. - Configure a fallback — server-side
fallbacks(Opus 4.8) or the client-side middleware for your platform. - Log the
stop_detailscategory — you need thecyber/bio/reasoning_extractionbreakdown to spot patterns. - Discard mid-stream partials — never commit a half-answer from a refused stream.
- Treat post-fallback refusals as real — if both models decline, escalate to a human or return a clear message; don't loop.
- Alert on rate, not on each event — a single refusal is noise; a spike in one category is a signal your prompts drifted toward the boundary.
- Audit for
reasoning_extraction— if you see that category, a prompt is asking the model to show its work; fix the prompt (see below), don't just fall back.
That last point connects to a subtle trap. If your refusals cluster on the reasoning_extraction category, the fix isn't a better fallback — it's editing the skill prompt that's asking Fable 5 to reproduce its internal reasoning. We cover exactly that in the reasoning-extraction trap, and it's often the difference between a skill that refuses 10% of the time and one that never does.
The takeaway
Fable 5 gives you a stronger model and one new contract term: some requests come back declined, dressed as a success. Build for it once and it disappears into infrastructure — check stop_reason before content, fall back to Opus 4.8, log the category, and discard partials.
Do this at the framework level, not per-skill, so every agent and workflow you ship inherits it. Then look at why you're refusing: if it's reasoning_extraction, rewrite the prompt; if it's over-scripted skills generally, trim them. The full run is at the Claude Fable 5 series hub, and you can find agents and automation loops to harden across /agents and /loops.