AI for Builders

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.

Outcomestop_reasonresponse.modelusage.iterations (fallback_message?)Who actually answered
Fable answered itselfend_turnclaude-fable-5no fallback_messageFable
Fallback rescued the callend_turnclaude-opus-4-8fallback_message presentOpus
Whole chain refused (the trap)refusalclaude-opus-4-8fallback_message still presentNobody

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.

Our routing-eval harness, 28 trials per effort arm: Claude Fable 5 refused 75% (21 of 28) of benign checkable tasks at low effort and 85.7% (24 of 28) at extra-high, 44 of 45 refusals API-labeled cyber false positives; the raw pass rate a caller sees collapses to 14 to 18% while Claude Opus 4.8 passes 89 to 96%. Small sample, deterministic short-horizon scoring only.

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.

Of 28 Claude Fable 5 calls with server-side fallback to Opus 4.8 on, fallback fired 20 times (71.4%), none were still refused, and the pass rate rose to 96.4% (95% Wilson interval 82.3 to 99.4). The rescue is Opus answering, so you pay for the 2x model and read the half-price model's output. Small sample, n=28.

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_reason on every call. A refusal is a 200; error-rate and 5xx dashboards will not show it.
  • Log response.model on 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. A claude-opus-4-8 answer to a claude-fable-5 request 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

  1. 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

  2. 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

  3. Anthropic, “Refusals and fallback.” The refusal-on-HTTP-200 response shape, the stop_details categories, the opt-in fallbacks parameter and server-side-fallback-2026-06-01 header, response.model, the fallback content block, and usage.iterations / fallback_message: https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback 2 3

  4. Anthropic, “Introducing Claude Fable 5 and Claude Mythos 5.” The refusal stop_reason and the 30-day-retention / no-zero-data-retention requirement: https://platform.claude.com/docs/en/about-claude/models/introducing-claude-fable-5

  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.

  6. 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