A refused Claude call returns HTTP 200, empty content. Dashboards log success.
When Claude Fable 5 refuses, it returns HTTP 200 with an empty content array, so error dashboards read it as a success, and with fallback on the model can swap to Opus 4.8 unseen. Instrument now.
Your error rate is flat. Your 5xx graph is flat. And a slice of the people who asked Claude Fable 5 a question this morning got nothing back, with no error anywhere to explain why. When Fable 5 refuses a request it returns an HTTP 200 with an empty content array and stop_reason set to refusal, so every dashboard you own files the refusal under success.
This is not a rare edge. Anthropic pulled Fable 5 on June 12 under a US export-control order1 after Amazon researchers reported a jailbreak, and switched it back on July 1 with a retrained safety classifier.2 That classifier blocks the reported technique in over 99% of cases, and by Anthropic’s own note it does so by “flagging benign requests more often during routine coding and debugging tasks.”2 If your traffic is cyber-adjacent, your refusal rate is climbing this week whether or not anything in your telemetry moves.
The fix is two fields on every call: log stop_reason and response.model. Read them back and the refusal and the swapped model both surface in your logs. Skip them and you spend an afternoon chasing a dropped-response ghost that was never dropped.
Nothing errored, so nothing fired
Here is a refusal on the wire. Not a 4xx, not a 5xx, not a timeout. A clean 200 that your uptime tooling has no reason to touch:
{
"model": "claude-fable-5",
"content": [],
"stop_reason": "refusal", // <- the only field that says "refused"
"stop_details": { "type": "refusal", "category": "cyber", "explanation": "..." },
"usage": { "input_tokens": 412, "output_tokens": 0 }
}
The empty content array is the second trap. Reach for the text the way you do on the happy path and you get an exception, not a refusal you can handle:
answer = resp.content[0].text # IndexError: content is empty on a refusal
So branch on stop_reason before you ever touch content:
resp = client.messages.create(
model="claude-fable-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
if resp.stop_reason == "refusal":
category = resp.stop_details.category if resp.stop_details else None
log.warning("fable5.refusal", category=category, msg_id=resp.id)
handle_refused(category) # content is empty or partial; don't read resp.content[0]
else:
answer = resp.content[0].text
Branch on stop_reason, never on stop_details. Anthropic is explicit that stop_details is informational and can be null3 even on a refusal. When it is present, category is one of "cyber", "bio", "frontier_llm", "reasoning_extraction", or null, and the explanation string is not stable. Display it, do not parse it.
Read stop_reason before you trust a 200. A refusal passes every dashboard you own and shows up in nothing but that field.
That covers the calls Fable 5 answers itself. But some of your calls have somewhere else to go.
The model that answered may not be the one you asked for
Fallback is opt-in on the raw API. A refused call just stops unless you named a fallback. (When Fable 5 blocks a request, Anthropic’s product notifies the user and reroutes it to Opus 4.82; the raw API leaves that to you.) You turn it on with the fallbacks parameter and a beta header:
resp = client.beta.messages.create(
model="claude-fable-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
fallbacks=[{"model": "claude-opus-4-8"}],
betas=["server-side-fallback-2026-06-01"],
)
Now the swap is silent in a different way. The request succeeds, the answer looks normal, and nothing on the happy path tells you a different model wrote it. response.model, resp.model in the code above, does: it reports whoever produced the returned message, requested model or fallback.
A 200 with an answer tells you the call succeeded; response.model tells you which model wrote it.
The exception is a chain that refuses all the way down, where the obvious signal points the wrong way.
When the whole chain refuses, the iteration log still shows a fallback
Alongside response.model, read usage.iterations. It records one entry per attempt: a declined model shows up as a message entry, the model that actually served shows up as a fallback_message entry. For the exact hand-off point, content carries a fallback block3, {"type": "fallback", "from": {"model": ...}, "to": {"model": ...}}, marking where one model’s output gives way to the next.
| Outcome | stop_reason | response.model | usage.iterations (fallback_message?) | Who actually answered |
|---|---|---|---|---|
| Fable answered itself | end_turn | claude-fable-5 | no fallback_message | Fable |
| Fallback rescued the call | end_turn | claude-opus-4-8 | fallback_message present | Opus |
| Whole chain refused (the trap) | refusal | claude-opus-4-8 | fallback_message still present | Nobody |
The trap is inferring “fallback served the answer” from the presence of a fallback_message entry:
served_by_fallback = (
any(it.type == "fallback_message" for it in (resp.usage.iterations or []))
and resp.stop_reason != "refusal"
)
log.info("fable5.call", answered_by=resp.model, served_by_fallback=served_by_fallback)
Notice the second clause. If every model in the chain declines, you get the last one’s refusal and stop_reason is "refusal" again, so the iteration entry alone would report a fallback that never actually answered.
A fully declined chain still writes a fallback_message, so the iteration log alone will credit a fallback that answered nothing. stop_reason breaks the tie.
And do not count on reconstructing any of this from Anthropic later. For Fable 5, you cannot.
No server-side replay, so log it at the call site or lose it
Fable 5 carries 30-day data retention and is not available under zero data retention4: a ZDR org cannot call it at all. There is no Anthropic-side replay for a request you never captured, so whatever you log at the call site is the entire record you will ever have of that refusal or that swap.
Fable refused most of our benign tasks, and fallback quietly served Opus
So how often will you actually reach for that record? We put Claude Fable 5 through our own routing-eval harness on a set of small, benign, machine-checkable coding tasks (parsers, state machines, string transforms, constraint puzzles), 28 trials per effort arm, and it declined most of them.5 It refused 75% (21 of 28) at low effort and 85.7% (24 of 28) at extra-high, a gap well inside the noise of a sample this size. Thinking harder didn’t help. Of 45 refusals across the sweep, 44 carried the API’s own cyber category on tasks with nothing cyber in them: exactly the benign-coding false positives Anthropic said the July 1 classifier would produce.2
The number your users feel is the raw pass rate, refusals counted as the failures they are. There Fable lands between 14% and 18% (4 to 5 of 28 per effort arm), against Claude Opus 4.8 at 89 to 96% (25 to 27 of 28). The gap is all refusals: Fable mostly declined, on tasks Opus finished.
Now turn on server-side fallback to Opus 4.8 and watch the swap do its work. Across those same 28 calls, fallback fired 20 times (71.4%), none were still refused at the end, and the pass rate climbed to 96.4% (95% Wilson interval 82.3 to 99.4). That recovered number is Opus clearing the bar, on 20 calls Fable would have dropped. You routed to the model that lists at twice Opus’s price6, and five times in seven it handed the work to the half-price one.
This suite scores short, checkable tasks only, not the long-horizon agentic work Fable is actually built for, so read it as one narrow slice: on that slice, the premium bought a decline and an Opus answer. And none of the swap is visible unless you logged response.model. We could see all 20 rescues because we recorded every call. Miss that field in production and 20 Opus answers are indistinguishable from 20 Fable answers, and the entire rescue happened off the books.
On our own short-horizon suite Fable refused roughly 75 to 86% of benign checkable tasks at both efforts (21/28 and 24/28), and fallback served Opus on 20 of 28 calls. The swap is common, and response.model is the only field that records it.
The whole fix is two fields you already receive
Both fields already come back on every response; the fix is reading them:
- Log
stop_reasonon every call. A refusal is a 200; error-rate and 5xx dashboards will not show it. - Log
response.modelon every call. It names the model that answered, the cleanest signal that fallback swapped it. - Alert on refusal rate, split by
stop_details.category. The July 1 classifier lifts it on coding-adjacent work. - Alert on unexpected values in
response.model. Aclaude-opus-4-8answer to aclaude-fable-5request means a fallback fired. - Emit one event per refusal and one per fallback-served response, then alert on the gap3 between the two counts. That gap is your refusal-recovery rate, and it is invisible any other way.
An unlogged model swap feeding a downstream agent is how a cascade starts: the step still returns a well-formed answer, so the fault propagates instead of stopping. That failure mode is the subject of error propagation and cascade containment, and the case for treating fallback as a measured reliability lever runs through the production reliability playbook. Log the two fields first. You cannot contain a swap you cannot see.
Footnotes
-
The Hacker News, “Anthropic Restores Claude Fable 5 After U.S. Lifts Jailbreak-Linked Export Controls.” The export-control order and the Amazon jailbreak report: https://thehackernews.com/2026/07/anthropic-restores-claude-fable-5-after.html ↩
-
Anthropic, “Redeploying Claude Fable 5.” The June 12 suspension and July 1 redeploy, the retrained safety classifier, the over-99% block figure, the benign-coding flagging tradeoff, and the notify-and-reroute-to-Opus-4.8 behavior: https://www.anthropic.com/news/redeploying-fable-5 ↩ ↩2 ↩3 ↩4
-
Anthropic, “Refusals and fallback.” The refusal-on-HTTP-200 response shape, the
stop_detailscategories, the opt-infallbacksparameter andserver-side-fallback-2026-06-01header,response.model, thefallbackcontent block, andusage.iterations/fallback_message: https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback ↩ ↩2 ↩3 -
Anthropic, “Introducing Claude Fable 5 and Claude Mythos 5.” The refusal
stop_reasonand the 30-day-retention / no-zero-data-retention requirement: https://platform.claude.com/docs/en/about-claude/models/introducing-claude-fable-5 ↩ -
Our own routing-eval harness: deterministic exact-match / unit-test scoring (no LLM judge), Anthropic API, pricing re-verified live 2026-07-02; effort sweep of 14 pruned tasks × 2 runs = 28 trials per arm (prior round n=40), 95% Wilson intervals, pre-registered stop rule. It measures short-horizon checkable correctness only, not long-horizon agentic or open-ended quality. ↩
-
Anthropic, “Models overview.” Claude Fable 5 at $10 in / $50 out and Claude Opus 4.8 at $5 in / $25 out per million tokens, as of 2026-07-02: https://platform.claude.com/docs/en/about-claude/models/overview ↩