Aller au contenu principal

Fallbacks côté serveur & Fallback Credit

Avancé

Avant Opus 5, un refus de Claude était votre problème. Le classifier déclinait, vous voyiez stop_reason: "refusal" revenir sur un heureux HTTP 200, et maintenant vous possédiez le retry : choisir un autre modèle, renvoyer tout l'historique, regarder votre cache de prompt fondre parce que le nouveau modèle a un espace de nom de cache différent, et essayer d'expliquer à votre équipe finance pourquoi la même conversation a facturé deux fois.

Le lancement d'Opus 5 (24 juillet 2026) a livré deux bêtas liées qui s'effondrent tout ça dans un seul appel API :

  1. Fallback côté serveur (server-side-fallback-2026-07-01) — mettez fallbacks: "default" et l'API retente la requête refusée sur un modèle qu'Anthropic choisit pour la catégorie de refus, dans le même aller-retour. Vous pouvez aussi nommer jusqu'à trois cibles à vous.
  2. Fallback credit (fallback-credit-2026-07-01) — un token de crédit à usage unique attaché à chaque refus qui, quand écho sur un retry, retarifie le retry comme si la conversation avait toujours été sur le modèle de fallback. Les écritures cache sur le nouveau modèle deviennent des lectures cache.

Les deux bêtas sont indépendantes — vous pouvez utiliser fallback credit seul si vous avez déjà une logique de retry côté client — mais le point de la release est que vous ne devriez presque jamais en avoir besoin. Cette page vous fait traverser les deux, du one-liner copier-coller aux cas limites qui mordent en prod (streaming en plein tool_use, sticky routing, output_config.format verrouillant la forme de continuation).

What you'll learn
  • Ce à quoi ressemble vraiment un refus sur le fil (JSON, cinq catégories d'arrêt, quand les tokens sont facturés)
  • Les trois façons de fallback (côté serveur / middleware SDK / HTTP brut manuel) et quand chacune est la bonne
  • Le one-liner : fallbacks: 'default' plus le header bêta, et ce que la forme de réponse ajoute
  • Liste explicite vs mode default, allowed_fallback_models, et pourquoi l'ordre compte
  • Comment le fallback credit vous empêche de payer le cache de prompt deux fois — le token, les deux formes de corps de retry, et ce que usage.iterations devrait montrer
  • L'échelle de rejet à 3 barreaux que tout retry manuel doit implémenter (continuation → corps inchangé → forfait du token)
  • Où ça NE marche PAS : Message Batches, trous Bedrock/GCP/Foundry, Sonnet 5, refus streaming en plein tool_use, output_config.format + outils serveur

★ Insight ───────────────────────────────────── Il y a deux empreintes spécifiques à Anthropic à intérioriser ici. Premièrement, un refus classifier est un 200 avec stop_reason: "refusal" — pas un 4xx. Si votre handler d'erreur traite non-2xx comme « retry » vous ignorerez silencieusement les refus ; s'il traite 200 comme « succès » vous afficherez silencieusement du contenu vide. Ni l'un ni l'autre n'est ce que vous voulez. Deuxièmement, les caches de prompt sont par modèle, donc un retry naïf sur un autre modèle Claude paie toujours le coût d'écriture cache à zéro même quand le préfixe de conversation est identique byte-à-byte. Le token de crédit est la pièce qui bouche ce trou — et la raison pour laquelle fallback-credit existe comme bêta séparée de server-side fallback. ─────────────────────────────────────────────────

Ce à quoi ressemble vraiment un refus

Un refus classifier est une réponse message normale avec un tableau content vide et stop_reason: "refusal" :

{
"id": "msg_01XFUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-fable-5",
"content": [],
"stop_reason": "refusal",
"stop_details": {
"type": "refusal",
"category": "cyber",
"explanation": "This request was declined because it could enable cyber harm."
},
"usage": {
"input_tokens": 412,
"output_tokens": 0
}
}

Le stop_details.category est une de cinq valeurs. Deux sont null quand le refus ne se mappe pas à une catégorie nommée (un null permanent, pas un placeholder) :

categoryCe qui l'a déclenché
"cyber"La requête pourrait permettre du cyber-dommage (malware, dev d'exploit). Le travail bénin de cybersécurité peut aussi le déclencher.
"bio"La requête pourrait permettre du dommage biologique. Le travail bénéfique en sciences de la vie peut aussi le déclencher.
"frontier_llm"La requête pourrait aider au développement de modèles IA concurrents, restreint par les termes commerciaux d'Anthropic.
"reasoning_extraction"La requête demande au modèle de reproduire son raisonnement interne dans le texte de réponse. Utilisez le thinking adaptatif pour obtenir du raisonnement sous forme structurée.
"general_harms"Zones de dommage diverses ; du travail bénin déclenche parfois ça.

Un refus qui arrive avant toute sortie n'est pas facturé (ses tokens apparaissent dans usage mais ne sont pas chargés) ; il compte encore contre vos rate limits. Un refus mid-stream facture l'input et l'output déjà streamé à taux normaux. Quoi qu'il en soit, traitez toute sortie partielle comme incomplète et jetez-la — le classifier de sécurité a tiré sur la propre trajectoire du modèle.

La chaîne explanation n'est pas stable entre versions. Affichez-la, ne la parsez pas.

Choisir une approche de fallback

Trois saveurs existent. Choisissez la ligne qui vous correspond :

Votre situationUtilisezPourquoi
API Claude, vous voulez la chose la plus simpleFallback côté serveur avec fallbacks: "default"Une requête, une réponse. L'API choisit le fallback et applique le crédit pour vous.
N'importe quelle plateforme (Bedrock, Vertex, Foundry), utilisant un SDK AnthropicMiddleware SDK (BetaRefusalFallbackMiddleware)Configurez une fois sur le client. Retries + crédit sont automatiques. C'est le seul chemin sur Bedrock / Vertex / Foundry aujourd'hui.
HTTP brut, logique de retry custom, ou SDK non-AnthropicRetry manuel avec le header fallback-credit-2026-07-01Contrôle total. Vous implémentez l'échelle à 3 barreaux vous-même.

Le fallback côté serveur et le middleware SDK appliquent le fallback credit pour vous. Vous n'avez à penser à la danse du token de crédit que si vous construisez le retry vous-même.

Le one-liner : fallbacks: "default"

La feature entière, en une requête :

Fallback côté serveur en mode default

curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: server-side-fallback-2026-07-01" \
-H "content-type: application/json" \
-d '{
  "model": "claude-fable-5",
  "max_tokens": 1024,
  "fallbacks": "default",
  "messages": [{"role": "user", "content": "Hello, Claude"}]
}'

Si Fable 5 décline et que la catégorie de refus a un fallback recommandé par Anthropic, l'API lance la même requête sur ce modèle dans le même appel. Vous récupérez une seule réponse et le champ top-level model nomme quel modèle a en fait répondu. Si la catégorie n'a pas de fallback recommandé, le refus tient et vous récupérez le refus exactement comme si fallbacks n'était pas défini.

Ce que « default » fait vraiment : l'API lit la table de routing définie côté serveur du modèle demandé et choisit un fallback dans celle-ci basé sur la catégorie de refus. Comme Anthropic met à jour cette table (ajout d'un nouveau fallback pour une catégorie, promotion d'Opus 5 comme cible par défaut de Fable 5, etc.) vous obtenez le nouveau routing gratuitement. C'est le pitch : arrêtez de maintenir une liste de modèles de fallback qui sera fausse dans un mois.

La liste explicite, pour quand vous devez épingler

Si vous voulez contrôler le routing vous-même, passez une liste au lieu de "default". Jusqu'à trois entrées, essayées dans l'ordre :

response = client.beta.messages.create(
model="claude-fable-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
fallbacks=[
{"model": "claude-opus-5"}, # try Opus 5 first
{"model": "claude-opus-4-8"}, # then Opus 4.8
],
betas=["server-side-fallback-2026-07-01"],
)

Les règles qui vont vous faire trébucher si vous ne les lisez pas :

  • Chaque cible doit être un fallback permis pour le modèle demandé. La liste des cibles permises est publiée comme allowed_fallback_models sur l'entrée de chaque modèle dans la Models API quand le header bêta server-side-fallback-2026-07-01 est défini. (Pour Claude Fable 5, à l'heure d'écriture cette liste est claude-opus-4-8 et claude-opus-5.)
  • Les entrées doivent être distinctes entre elles et du modèle demandé.
  • Chaque entrée peut surcharger max_tokens, thinking, output_config et speed pour cette tentative seulement. C'est comme ça que vous dites « sur le fallback, tourne à effort inférieur » sans toucher votre requête principale.
  • La requête doit être valide comme requête directe vers chaque modèle nommé. Si un fallback ne supporte pas une feature que la requête utilise (par ex. une bêta que le modèle de fallback n'accepte pas), l'API rejette toute la requête d'entrée, pas juste la tentative de fallback.
  • Seuls les refus classifier déclenchent le fallback. Rate limits, surcharges et erreurs serveur sur le modèle demandé remontent tel quel.

Le mode "default" marche seulement sous server-side-fallback-2026-07-01. La forme liste explicite marche aussi sous l'ancien header server-side-fallback-2026-06-01.

Ce que la réponse contient

La réponse est un message normal avec deux ajouts :

  • Le champ top-level model nomme le modèle qui a produit le message retourné (demandé ou fallback).
  • Un bloc content fallback marque chaque point où la sortie d'un modèle cède à la suivante : {"type": "fallback", "from": {"model": ...}, "to": {"model": ...}}. Sur un refus-avant-sortie, ce bloc est le premier bloc content ; sur un fallback mid-stream il apparaît au point de handoff.
  • usage.iterations enregistre chaque tentative. Un modèle qui a décliné apparaît comme une entrée message (ses tokens reportés mais pas chargés) ; le modèle qui a servi le tour apparaît comme une entrée fallback_message.

Exemple après un refus avant toute sortie, quand le routing default sélectionne Opus 4.8 :

{
"id": "msg_01XFUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-8",
"content": [
{ "type": "fallback", "from": { "model": "claude-fable-5" }, "to": { "model": "claude-opus-4-8" } },
{ "type": "text", "text": "Hi! How can I help you today?" }
],
"stop_reason": "end_turn",
"stop_details": null,
"usage": {
"input_tokens": 412,
"output_tokens": 264,
"iterations": [
{ "type": "message", "model": "claude-fable-5", "input_tokens": 535, "output_tokens": 0 },
{ "type": "fallback_message", "model": "claude-opus-4-8", "input_tokens": 412, "output_tokens": 264 }
]
}
}

Si chaque modèle de la chaîne refuse, la réponse est le refus du dernier modèle, avec une entrée message pour chaque hop antérieur et une entrée fallback_message pour le dernier.

Continuer la conversation

Au tour suivant, écho du content assistant tel que reçu. Après un fallback mid-output, le content que vous avez récupéré peut inclure des blocs que le modèle qui a décliné a produit avant le handoff. Quoi garder et quoi lâcher :

Type de blocAu tour suivant
fallbackGardez-le exactement où il apparaissait. Sa position est utilisée pour valider les blocs thinking autour de lui. Le bouger ou le lâcher → 400.
textGardez.
Tout bloc après le dernier bloc fallbackGardez.
thinking, redacted_thinking, connector_text avant le dernier fallbackLâchez.
tool_use côté client avant le dernier fallbackLâchez.
server_tool_use avant le dernier fallbackGardez quand couplé avec son résultat. Lâchez quand il n'a pas de résultat correspondant.

Le modèle mental : tout ce qui a tourné sur le modèle de fallback reste ; le travail intermédiaire non corroboré du modèle qui a décliné disparaît.

Sticky routing

Une fois qu'une conversation est tombée en fallback, l'API s'en souvient. Les requêtes ultérieures pour cette conversation qui incluent aussi un paramètre fallbacks vont directement au modèle de fallback, sautant le modèle demandé entièrement. Ça vous empêche de payer une taxe-de-refus sur chaque follow-up dans une session qui allait toujours refuser à nouveau.

Propriétés à connaître :

  • Retenu ~1 heure, scopé à votre organisation.
  • Stocké comme hash de contenu du préfixe de conversation + le modèle qui l'a servi. Le contenu de message lui-même n'est pas stocké côté serveur.
  • Best effort — votre code doit encore gérer le modèle demandé essayé à nouveau à tout moment.
  • Un tour servi en sticky n'a aucun bloc content fallback (rien n'a décliné ce tour). Identifiez-le par la présence d'un fallback_message dans usage.iterations, l'absence d'une entrée message pour le modèle demandé, et le champ model de réponse.

Sur le streaming, la décision de routing est faite avant que le stream ouvre, donc message_start porte déjà l'ID du modèle de fallback.

Comportement de streaming

Le retry se passe sur le même stream — rien de ce que vous avez déjà reçu n'est invalidé.

Refus avant toute sortie

  • message_start nomme le modèle de fallback.
  • Le bloc fallback est le premier bloc content.
  • Le time to first byte inclut la tentative déclinée (parce que message_start attend que le fallback démarre).

Refus mid-output

  • Le bloc content actuellement ouvert se ferme.
  • Un bloc fallback (content_block_start + content_block_stop, pas de deltas) marque la frontière.
  • Le modèle de fallback continue à partir de la sortie partielle. Seuls les blocs text de la sortie partielle sont passés en contexte au modèle de fallback ; les autres types de bloc restent dans content mais ne sont pas vus par le fallback.
  • message_start a déjà nommé le modèle demandé, donc lisez le modèle qui sert depuis le to.model du bloc fallback et depuis l'entrée fallback_message dans l'usage.iterations du message_delta final.

Non-streaming, refus mid-output : la réponse omet la sortie partielle du modèle qui a décliné et le fallback répond de zéro. Le résultat ressemble à un refus-avant-sortie — bloc fallback en premier — avec les tokens de la tentative déclinée encore enregistrés dans usage.iterations. C'est une vraie différence de comportement par rapport au streaming ; les tests de dimensionnement faits sur le stream peuvent sous-prédire le coût quand vous basculez en non-streaming.

Fallback credit : la retarification invisible

Les caches de prompt sont par modèle. Si Fable 5 a mis en cache 400k tokens de votre préfixe de conversation et refuse, un retry naïf sur Opus 5 doit écrire tous les 400k dans le cache d'Opus 5 depuis zéro — et les écritures cache coûtent plus que les lectures cache. Le fallback credit retire ce coût supplémentaire. Le refus porte un token de crédit à usage unique, vous écho le token sur le retry, et le retry est facturé comme si la conversation avait toujours été sur le modèle de fallback.

Le fallback côté serveur et le middleware SDK appliquent le crédit automatiquement. Vous n'avez à penser au token vous-même que si vous construisez le retry sur HTTP brut.

Le flux manuel à quatre étapes

Guided walkthrough1 of 4
  1. Envoyez la première requête avec anthropic-beta: fallback-credit-2026-07-01. (server-side-fallback-2026-07-01 accorde les mêmes champs, et l'ancien header fallback-credit-2026-06-01 est toujours accepté.)

L'échelle de rejet que tout retry manuel doit avoir

La plupart des retries rachètent à la première tentative. Quand un ne le fait pas, l'API retourne un 400 qui vous dit quoi essayer ensuite. Implémentez les trois barreaux :

Guided walkthrough1 of 3
  1. La cause la plus commune est que output_config.format ou un tool_choice qui force l'usage d'outil exclut la forme de continuation. Lâchez le message assistant appendu ; gardez le token.
Watch out
  • "redemption temporarily unavailable" est une erreur transitoire, PAS un verdict sur votre forme de retry. Retentez la MÊME requête avec le MÊME token, dans la fenêtre de 5 minutes. Ne descendez pas l'échelle.

Champs qui doivent matcher exactement (les règles strict-match)

Le rachat compare votre retry à la requête refusée. Chaque champ qui façonne le prompt doit matcher :

RègleChamps
Doit matcher exactementsystem, messages, tools, tool_choice, thinking, cache_control, et (quand utilisés) output_config, mcp_servers, context_management, container
Peut changer sur le retrymodel, max_tokens, stop_sequences, temperature, top_p, top_k, stream, metadata, service_tier

La forme de continuation est la seule exception au match messages : elle ajoute exactement un message assistant à la fin de messages.

Deux pièges subtils :

  1. Les headers bêta doivent aussi matcher. Un header bêta présent sur une des deux requêtes mais pas l'autre peut faire échouer le match même quand les corps sont identiques. Le 400 dit request body ... does not match, ce qui se lit comme une différence de corps mais est une différence de header. Deux familles sont exemptes : server-side-fallback-* (lâchez-le sur le retry avec le param fallbacks), et fallback-credit-* (gardez-le sur les deux).
  2. Ne strippez pas les blocs thinking ou redacted_thinking des tours antérieurs sur le retry, même si un retry sans token le fait habituellement. Le corps doit matcher la requête refusée ; le serveur gère ces blocs lui-même.

Vérifier que le crédit a vraiment été appliqué

Le remboursement est visible dans l'usage du retry. Comparé à ce que la même requête reporterait sans le token, cache_creation_input_tokens est plus bas, et cache_read_input_tokens est plus haut du même montant. Un décalage de zéro signifie que le token a été honoré mais qu'il n'y avait rien à retarifier (par ex. le cache du modèle de retry était déjà chaud).

Scope et durée de vie du token

  • Ne rachète que depuis l'organisation et le workspace qui ont reçu le refus (sur Foundry aussi). Sur Bedrock et Vertex, qui n'ont pas de workspaces, le token est lié à l'identité d'appelant de la plateforme.
  • Expire 5 minutes après le refus. Après ça, retry sans lui.
  • Stateless — le serveur ne stocke rien à son sujet, et il n'y a pas d'endpoint pour l'inspecter ou le révoquer.

Où ça ne marche pas (ou marche différemment)

Guided walkthrough1 of 6
  1. Le paramètre fallbacks n'est pas supporté sur l'API Message Batches (un item de batch qui l'inclut revient comme résultat en erreur). Les refus dans Message Batches ne minent pas de tokens de crédit non plus, et un token passé sur une requête batch est accepté mais ignoré. Fallback sur retry côté client après que le batch se résolve.

Une config pragmatique pour une app Claude en production

Guided walkthrough1 of 5
  1. Protection zéro-effort contre les catégories pour lesquelles Anthropic a des fallbacks recommandés. C'est un sur-ensemble de l'approche manuelle parce que la table de routing se met à jour automatiquement.

Comment ça se compare à ce que font les autres fournisseurs

FournisseurRefus → fallback automatique dans un seul appel API ?
Anthropic Claude Fable 5 / Opus 5Oui — fallbacks: "default" + token de crédit. Le sticky routing porte les follow-ups.
Anthropic Claude Opus 4.8Était le modèle cible de la variante credit-token-only (bêta juin 2026). Le mode default côté serveur a atterri avec Opus 5.
OpenAI GPT-5 / 6Pas de fallback first-party côté serveur. Vous détectez un finish_reason refusal vous-même et retentez sur un autre modèle côté client ; l'API Responses ne publie pas d'équivalent d'allowed_fallback_models.
Google Gemini 3Les refus remontent comme raisons de bloc SAFETY ; retry est côté client contre un autre modèle de la famille.
Passerelles IA (LiteLLM, Portkey, OpenRouter)Un fallback niveau routeur agnostique du fournisseur existe mais est facturé indépendamment à chaque tentative — pas d'équivalent de cache-credit par fournisseur. Voir AI gateways.

Les harnais cross-modèle peuvent quand même utiliser le token de crédit : il est spécifique au modèle mais le concept (écho d'un token opaque sur le retry, être retarifié) peut être feature-détecté par fournisseur.

Modes d'échec courants et ce qu'ils veulent dire

  • Vous récupérez un tableau content vide et votre UI affiche un message blanc. Vous avez oublié de vérifier stop_reason: "refusal" avant de render. Détectez-le et soit affichez un message spécifique à la catégorie soit câblez les fallbacks.
  • Votre retry continue de 400 avec request body ... does not match. Mismatch de header, très probablement. Diffez chaque header anthropic-beta entre les deux requêtes, pas juste le corps.
  • Vous utilisez le middleware SDK et voyez le même modèle facturé deux fois. Vous avez oublié de partager le BetaFallbackState entre les requêtes de la même conversation. Le sticky routing a besoin de l'état pour épingler les follow-ups.
  • Votre rapport de coût montre un gros saut sur Opus 4.8 même si vous pensiez être sur Fable 5. Le sticky routing a porté les follow-ups après un refus. Loggez response.model et usage.iterations pour voir la répartition.
  • Vous avez oublié le header bêta sur le retry et récupéré un échec de rachat. Le retry a besoin de fallback-credit-2026-07-01 pour racheter le token.
  • Le job batch a silencieusement lâché vos fallbacks. Les batches ignorent fallbacks et les tokens de crédit. Faites le retry après complétion du batch.
Aucune carte pour l'instant — ajoutez-en pour commencer à réviser. 🃏

Check yourself

0/7
  1. Une requête Claude Fable 5 retourne HTTP 200 avec `stop_reason: 'refusal'` et un tableau content vide. Combien êtes-vous facturé ?
  2. Vous envoyez `fallbacks: 'default'` avec le header `server-side-fallback-2026-07-01` sur une requête Fable 5 qui est refusée avec catégorie `reasoning_extraction`. Que se passe-t-il ?
  3. Quels champs de l'API Claude doivent matcher exactement entre la requête refusée et le retry avec token de crédit ?
  4. Vous récupérez `stop_details.fallback_has_prefill_claim: true` sur un refus tiré mid-output. Quel corps de retry devriez-vous construire ?
  5. Votre requête Fable 5 en streaming refuse pendant qu'un bloc `tool_use` est encore ouvert sur le stream. Que fait l'API ?
  6. Votre facturation montre des charges Opus 4.8 sur des tours que vous pensiez aller à Fable 5, des jours après un seul tour refusé. Que se passe-t-il ?
  7. Vous avez construit un retry côté client solide, donc vous préféreriez NE PAS utiliser le fallback côté serveur. Pouvez-vous encore obtenir les économies de cache-credit ?

Sources & lectures complémentaires