# Raisons d'arrêt et erreurs

Chaque stop_reason et erreur HTTP avec cause, détection et code de traitement — la référence de fiabilité pour la boucle agentique.

import { Tabs, TabItem, Steps } from '@prosefly/astro-components';

Les items de fiabilité reposent sur deux énumérations : les valeurs de `stop_reason` sur lesquelles une boucle doit brancher, et les erreurs HTTP qu'un client doit classer en réessai-ou-correction. Cette page couvre chacune avec cause, détection et traitement.

:::danger[La règle de fiabilité]
Branchez sur **`stop_reason`** pour contrôler la boucle et sur le **statut HTTP** pour décider réessai vs correction. Ne parsez jamais la prose pour « done », n'utilisez jamais un plafond d'itérations comme arrêt principal, et ne réessayez jamais une erreur client 4xx.
:::

## stop_reason — chaque valeur

| Valeur | Cause | Détection | Traitement |
| --- | --- | --- | --- |
| `end_turn` | Le modèle a terminé naturellement | `stop_reason == "end_turn"` | Fini — renvoyez la réponse |
| `tool_use` | Le modèle veut exécuter des outils | `stop_reason == "tool_use"` | Exécuter tous les blocs `tool_use`, ajouter le(s) `tool_result` dans un message user, rappeler |
| `max_tokens` | La sortie a atteint `max_tokens` | `stop_reason == "max_tokens"` | Tronqué — continuer (« Continue. ») ou augmenter le plafond ; ne jamais traiter comme complet |
| `stop_sequence` | A atteint une chaîne d'arrêt configurée | `stop_reason == "stop_sequence"` ; vérifiez `stop_sequence` | Fini — la chaîne était le terminateur voulu |
| `pause_turn` | Long tour d'outil côté serveur mis en pause | `stop_reason == "pause_turn"` | Renvoyer la conversation inchangée pour continuer |
| `refusal` | Les systèmes de sécurité ont refusé | `stop_reason == "refusal"` | Repli explicite (reformuler, transfert humain, message sûr) ; ne réessayez **pas** aveuglément |

```python
def handle(r, messages):
    sr = r.stop_reason
    if sr == "tool_use":
        messages.append({"role": "assistant", "content": r.content})
        messages.append({"role": "user", "content": run_tools(r.content)})
        return "continue"
    if sr == "max_tokens":
        messages.append({"role": "assistant", "content": r.content})
        messages.append({"role": "user", "content": "Continue from where you stopped."})
        return "continue"
    if sr == "pause_turn":
        return "resend"                 # re-send unchanged
    if sr == "refusal":
        return handle_refusal(r)        # explicit fallback path
    return "done"                       # end_turn / stop_sequence
```

:::caution[max_tokens n'est pas un succès]
Un arrêt `max_tokens` signifie que la réponse est coupée. La renvoyer comme finale est l'anti-pattern de silent-failure — particulièrement dangereux pour une sortie structurée où le JSON est désormais invalide.
:::

## Erreurs HTTP — chaque statut

| HTTP | Type | Cause | Retry ? | Traitement |
| --- | --- | --- | --- | --- |
| 400 | `invalid_request_error` | Requête malformée (p. ex. `tool_choice` forcé sur Fable 5.1, `budget_tokens` sur non-Haiku, mauvais schéma) | **Non** | Corriger la requête |
| 401 | `authentication_error` | Clé API manquante/invalide | **Non** | Corriger les identifiants |
| 403 | `permission_error` | La clé manque du droit d'accès (modèle/fonctionnalité) | **Non** | Vérifier l'accès/le droit |
| 404 | `not_found_error` | Le modèle/la ressource n'existe pas (p. ex. modèle retiré) | **Non** | Corriger l'ID / migrer |
| 413 | `request_too_large` | La charge utile dépasse les limites | **Non** | Réduire l'entrée ; chunker ; Files API |
| 429 | `rate_limit_error` | RPM/ITPM/OTPM dépassés | **Oui** | Backoff + jitter, respectez `retry-after` ; réduisez le débit proactivement |
| 500 | `api_error` | Erreur côté serveur | **Oui** | Backoff + jitter |
| 529 | `overloaded_error` | Capacité saturée | **Oui** | Backoff ; envisagez un repli vers un modèle plus récent ou équivalent |

```text
Retry?  ── status in {429, 500, 502, 503, 529} ──► YES: exponential backoff + jitter, honour retry-after
        └─ status in {400, 401, 403, 404, 413}  ──► NO: fix the request/credentials/entitlement
```

<Tabs>
  <TabItem label="Python">
```python
import time, random
from anthropic import APIStatusError, RateLimitError, APIConnectionError

RETRYABLE = {429, 500, 502, 503, 529}

def call(fn, **params):
    for attempt in range(6):
        try:
            return fn(**params)
        except RateLimitError as e:
            wait = float(e.response.headers.get("retry-after", 0)) or min(60, 2 ** attempt)
            time.sleep(wait + random.uniform(0, 0.5))
        except APIStatusError as e:
            if e.status_code in RETRYABLE:
                time.sleep(min(60, 2 ** attempt) + random.uniform(0, 0.5))
            else:
                raise                     # 4xx → fix, don't retry
        except APIConnectionError:
            time.sleep(min(60, 2 ** attempt) + random.uniform(0, 0.5))
    raise RuntimeError("exhausted retries")
```
  </TabItem>
  <TabItem label="TypeScript">
```typescript
import { APIError } from '@anthropic-ai/sdk';
const RETRYABLE = new Set([429, 500, 502, 503, 529]);
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export async function call<T>(fn: () => Promise<T>): Promise<T> {
  for (let attempt = 0; attempt < 6; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof APIError && RETRYABLE.has(err.status ?? 0)) {
        const ra = Number(err.headers?.['retry-after']) || Math.min(60, 2 ** attempt);
        await sleep((ra + Math.random() * 0.5) * 1000);
        continue;
      }
      throw err; // 4xx → fix
    }
  }
  throw new Error('exhausted retries');
}
```
  </TabItem>
</Tabs>

## Erreurs de streaming

Un événement `error` peut arriver en cours de flux (communément `overloaded_error`). Traitez-le comme le statut HTTP : surcharge/timeout réessayable → backoff et redémarrez le flux ; requête malformée → corrigez.

```text
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
```

## Erreurs d'exécution d'outil (pas HTTP)

Un outil qui échoue n'est **pas** une erreur API — l'appel API a réussi. Renvoyez un `tool_result` structuré avec `is_error: true` :

```json
{ "type": "tool_result", "tool_use_id": "toolu_01", "is_error": true,
  "content": "{\"category\":\"not_found\",\"retryable\":false,\"message\":\"No order ORD-999\"}" }
```

| Category | Signification | Le modèle devrait |
| --- | --- | --- |
| `not_found` | Ressource absente | Le dire à l'utilisateur, demander un ID valide |
| `invalid_input` | Mauvais arguments | Corriger et réessayer |
| `transient` | Temporaire (timeout) | Réessayer une fois, puis signaler |
| `forbidden` | Non autorisé | S'arrêter ; escalader |

Ne renvoyez jamais un succès vide sur échec (anti-pattern de silent empty-success).

## Référence rapide de décision

| Symptôme | Diagnostic | Action |
| --- | --- | --- |
| La boucle ne finit jamais | Utilise la prose/un plafond d'itérations pour s'arrêter | Brancher sur `stop_reason` |
| JSON tronqué | `max_tokens` atteint | Augmenter le plafond / continuer ; valider |
| « Ça s'est arrêté et mis en pause » | `pause_turn` | Renvoyer inchangé |
| Refus de sécurité traité comme un crash | `refusal` mal géré | Repli explicite |
| Tempêtes de 429 | Réessais sans backoff ou ignore `retry-after` | Backoff + jitter + en-tête ; monter de palier |
| Réessayer un 400 indéfiniment | Réessayer une erreur client | Corriger la requête |
| Thinking disparu après un repli | A basculé vers un modèle plus ancien | Migrer uniquement vers plus récent ou équivalent |
| L'outil a « marché » mais n'a rien renvoyé | Succès vide | `is_error` structuré |

## Idées reçues courantes

| Idée reçue | Réalité | Pourquoi cela compte à l'examen |
| --- | --- | --- |
| « Toute erreur devrait être réessayée » | Seulement 429/5xx/529 ; corrigez les 4xx | Distracteur de tempête de retries |
| « `max_tokens` est une réponse complète » | C'est une troncature | Anti-pattern de silent-failure |
| « Parser le texte pour savoir que c'est fini » | Brancher sur `stop_reason` | Anti-pattern de parsing de prose |
| « Plafonner les itérations pour arrêter la boucle » | Le plafond est un garde-fou ; `stop_reason` l'arrête | Anti-pattern de plafond d'itérations |
| « `refusal` est une erreur serveur » | C'est un refus de sécurité ; gérez-le explicitement | Distracteur de mauvaise gestion de refus |
| « Un échec d'outil est une erreur API » | Les erreurs d'outil utilisent `is_error` sur le résultat | Confusion de couche d'erreur |
| « 529 signifie abandonner » | Backoff ; envisagez un modèle de repli | Distracteur de disponibilité |

## Analyse de scénario

Un agent sur Opus 5 en production, par intermittence : (a) renvoie du JSON coupé, (b) atteint des 529 sous charge, (c) a une fois renvoyé un refus de sécurité montré aux utilisateurs comme « Error 500 », et (d) un ingénieur a ajouté `tool_choice: "any"` après avoir basculé un flux vers Fable 5.1 et obtient maintenant des 400.

<Steps>
1. **JSON coupé** — `stop_reason: max_tokens`. Augmentez `max_tokens` et/ou continuez ; validez avant usage.
2. **529** — réessayable ; backoff exponentiel + jitter respectant `retry-after` ; après des 529 répétés, basculez vers un modèle plus récent ou équivalent (pas vers le bas).
3. **Refus montré comme 500** — `stop_reason: refusal` a été mal géré ; routez vers un message de repli explicite, pas une erreur générique.
4. **400 sur Fable 5.1** — le `tool_choice` forcé est non supporté (400, non réessayable) ; basculez vers `auto`+instruction, `strict: true`, ou `output_config.format`.
</Steps>

Alternatives rejetées : réessayer le 400 (erreur client — corrigez-la), traiter le JSON tronqué comme valide (silent failure), et présenter le refus comme un 500 (mauvaise gestion de refus).

## Points clés à retenir

- Valeurs de `stop_reason` : `end_turn`, `tool_use`, `max_tokens`, `stop_sequence`, `pause_turn`, `refusal` — branchez sur chacune.
- Réessayez 429/5xx/529 avec backoff + jitter + `retry-after` ; **corrigez** 400/401/403/404/413.
- `max_tokens` = troncature, `refusal` = refus de sécurité — les deux nécessitent une gestion explicite, pas un laisser-passer silencieux.
- Les échecs d'outil utilisent un `is_error` structuré, jamais un succès vide ni une erreur HTTP.
- Sur des 529 répétés, ne basculez que vers un modèle plus récent ou équivalent pour garder valides les blocs de thinking de Fable 5.1.
