# Guide du formateur — Niveau Avancé, Session 1
# « Claude API : plongée en profondeur »

**Programme :** Applied AI — Yann Isola
**Public :** Architectes solutions préparant la certification *Claude Certified Architect*
**Durée :** 2 h 00 (+ 10 min de pause recommandée à mi-parcours)
**Prérequis :** Python intermédiaire, notions HTTP/REST (REST = Representational State Transfer), avoir déjà appelé une API (API = Application Programming Interface, interface de programmation applicative) quelconque.
**Matériel :** clé API Anthropic de démonstration, terminal avec `curl` et Python 3.10+, SDK (SDK = Software Development Kit, kit de développement logiciel) `anthropic` installé, page web interactive de la session (`webpage/index.html`).

---

## Objectifs pédagogiques

À la fin de la session, chaque participant sait :

1. **Construire** une requête API Claude complète et justifier chaque paramètre (`model`, `max_tokens`, `messages`, `system`, `temperature`, `stop_sequences`).
2. **Interpréter** chaque valeur de `stop_reason` et concevoir la logique applicative correspondante.
3. **Raisonner** sur la fenêtre de contexte comme une mémoire de travail : coût linéaire, effet « lost in the middle », stratégies de placement.
4. **Mettre en œuvre** le prompt caching avec `cache_control` et calculer le retour sur investissement (ROI = Return On Investment).
5. **Implémenter** le streaming SSE (SSE = Server-Sent Events, événements envoyés par le serveur) et nommer les événements du cycle de vie d'un message.
6. **Choisir** entre appel synchrone, streaming et Batches API selon le cas d'usage, avec les arguments coût/latence/SLA (SLA = Service Level Agreement, engagement de niveau de service).
7. **Concevoir** une stratégie de gestion d'erreurs robuste : 429, backoff exponentiel, en-têtes `retry-after`.

---

## Plan minuté

| Bloc | Durée | Contenu |
|------|-------|---------|
| 0. Ouverture | 5 min | Cadrage certification, tour de table express |
| 1. Anatomie d'une requête | 20 min | model, max_tokens, messages, system — démo curl + Python |
| 2. Rôles & prefilling | 15 min | user/assistant/system, prefilling de réponse |
| 3. stop_reason | 10 min | Les 4 valeurs, logique applicative |
| 4. Fenêtre de contexte | 15 min | Mémoire de travail, coût linéaire, lost in the middle |
| **Pause** | 10 min | |
| 5. Prompt caching | 20 min | cache_control, économie 90 %, surcoût 25 %, calculs ROI |
| 6. Comptage de tokens | 10 min | Endpoint count_tokens, tokenizers par famille |
| 7. Streaming SSE | 15 min | Cycle d'événements, démo live |
| 8. Batches API | 10 min | Async, −50 %, SLA 24 h, 100k requêtes |
| 9. Gestion d'erreurs | 10 min | 429, backoff exponentiel, retry-after |
| 10. Clôture | 5 min | Exit tickets, annonce des exercices |

---

## Bloc 0 — Ouverture (5 min)

**Message d'accroche :** « Aujourd'hui on ne parle pas de *prompting*. On parle de ce qui distingue un développeur qui appelle Claude d'un architecte qui conçoit un système autour de Claude : le contrat d'API, l'économie des tokens, et les modes de défaillance. »

Question flash au groupe : *« Qui a déjà reçu une erreur 429 en production ? Qu'avez-vous fait ? »* — collecter 2-3 réponses, y revenir au bloc 9.

---

## Bloc 1 — Anatomie d'une requête API (20 min)

### 1.1 Le contrat minimal

Trois champs sont **obligatoires** : `model`, `max_tokens`, `messages`.

**Démo curl (à taper en live) :**

```bash
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "Explique le protocole FIX en une phrase."}
    ]
  }'
```

⚠ Le nom de modèle (`claude-sonnet-4-5`) est **volatil** : vérifier la documentation officielle avant tout support de cours ou déploiement.

**Points à marteler :**

- `anthropic-version` : en-tête d'épinglage de version d'API. Sans lui → erreur. C'est un contrat : le comportement n'évoluera pas sous vos pieds.
- `max_tokens` est un **plafond de génération**, pas une cible. Le modèle peut s'arrêter avant. Mais s'il l'atteint, la réponse est **tronquée** (voir `stop_reason: "max_tokens"` au bloc 3).
- La facturation porte sur tokens d'entrée **+** tokens de sortie, à des tarifs différents (la sortie coûte typiquement ~5× l'entrée ⚠ selon le modèle).

### 1.2 Équivalent Python (SDK officiel)

```python
import anthropic

client = anthropic.Anthropic()  # lit ANTHROPIC_API_KEY dans l'environnement

response = client.messages.create(
    model="claude-sonnet-4-5",          # ⚠ volatil
    max_tokens=1024,
    temperature=0.2,                     # déterminisme relatif pour tâches techniques
    stop_sequences=["FIN_RAPPORT"],      # arrêt personnalisé
    system="Tu es un analyste financier. Réponds en français, style concis.",
    messages=[
        {"role": "user", "content": "Résume les risques d'un stablecoin adossé à des matières premières."}
    ],
)

print(response.content[0].text)
print(response.usage)        # input_tokens / output_tokens → pilotage des coûts
print(response.stop_reason)  # toujours l'inspecter en production
```

**Décryptage de la réponse :**

```json
{
  "id": "msg_01XFDUDYJgAACzvnptvVoYEL",
  "type": "message",
  "role": "assistant",
  "content": [{"type": "text", "text": "..."}],
  "model": "claude-sonnet-4-5",
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 58, "output_tokens": 212}
}
```

- `content` est un **tableau de blocs** (pas une simple chaîne) : blocs `text`, `tool_use`, etc. Habitude d'architecte : toujours itérer sur les blocs, jamais supposer `content[0]` unique.
- `usage` : c'est votre compteur de facturation. En production, on le **logge systématiquement** (observabilité des coûts).

### 1.3 Les paramètres d'échantillonnage

| Paramètre | Rôle | Recommandation architecte |
|-----------|------|---------------------------|
| `temperature` (0–1) | Aléa de l'échantillonnage | 0–0.3 pour extraction/classification, 0.7–1 pour créativité |
| `top_p` | Nucleus sampling (échantillonnage par noyau de probabilité) | Ne pas combiner avec `temperature` — choisir l'un des deux |
| `top_k` | Restreint aux k tokens les plus probables | Rarement nécessaire, cas avancés |
| `stop_sequences` | Chaînes d'arrêt personnalisées | Utile pour délimiter des sorties structurées |

**Piège certification :** `temperature: 0` ne garantit **pas** un déterminisme parfait (non-déterminisme numérique résiduel possible). Formulation correcte : « réduit fortement la variabilité ».

---

## Bloc 2 — Rôles de messages & prefilling (15 min)

### 2.1 Trois canaux, trois niveaux de privilège

1. **`system`** — canal **privilégié**. Ce n'est PAS « un premier message user déguisé ». Il est traité avec une priorité particulière par le modèle. Usages : persona, règles de sécurité, politique de sortie (format, langue, refus), contexte métier stable.
2. **`user`** — le tour de l'appelant. Contenu : question, documents, résultats d'outils (`tool_result`).
3. **`assistant`** — les tours du modèle. Mais aussi : le **prefilling**.

**Règle structurelle :** le tableau `messages` doit alterner user/assistant et **commencer par `user`**. Deux messages consécutifs de même rôle → erreur 400.

### 2.2 Prefilling : mettre des mots dans la bouche du modèle

Le dernier message peut être un `assistant` **partiel** : le modèle continue à partir de là.

```python
response = client.messages.create(
    model="claude-sonnet-4-5",  # ⚠
    max_tokens=500,
    messages=[
        {"role": "user", "content": "Liste 3 risques de contrepartie en JSON."},
        {"role": "assistant", "content": "{"}   # prefill : force l'ouverture JSON
    ],
)
# La réponse commence directement après "{" → pas de préambule "Voici le JSON :"
```

**Cas d'usage architecte :**
- Forcer un format de sortie (JSON — JavaScript Object Notation, XML — eXtensible Markup Language).
- Supprimer les préambules (« Bien sûr ! Voici… »).
- Contraindre un choix : prefill `"La réponse est ("` pour un QCM (QCM = Questionnaire à Choix Multiples).

**Attention :** le texte du prefill ne fait **pas** partie de la réponse retournée — pensez à le re-préfixer côté client (`"{" + response.content[0].text`).

**Démo live suggérée :** même question avec et sans prefill, comparer les sorties. Effet immédiat, très parlant.

---

## Bloc 3 — stop_reason : les 4 signaux (10 min)

Chaque réponse indique **pourquoi** le modèle s'est arrêté. Un architecte écrit une branche de code pour chacun.

| `stop_reason` | Signification | Réaction applicative |
|---------------|---------------|----------------------|
| `end_turn` | Fin naturelle du tour | Cas nominal — traiter la réponse |
| `max_tokens` | Plafond atteint → **réponse tronquée** | Alerter/relancer avec plafond plus haut, ou continuer la génération |
| `stop_sequence` | Une `stop_sequences` a été rencontrée | Lire `response.stop_sequence` pour savoir laquelle ; parser la sortie délimitée |
| `tool_use` | Le modèle demande l'exécution d'un outil | Exécuter l'outil, renvoyer un `tool_result`, reboucler |

**Code de garde canonique (à faire écrire aux participants) :**

```python
match response.stop_reason:
    case "end_turn":
        return extract_text(response)
    case "max_tokens":
        logger.warning("Réponse tronquée — usage=%s", response.usage)
        raise TruncatedResponseError(partial=extract_text(response))
    case "stop_sequence":
        return parse_delimited(extract_text(response), response.stop_sequence)
    case "tool_use":
        return handle_tool_loop(response)
    case _:
        raise UnexpectedStopReason(response.stop_reason)
```

**Piège certification :** `max_tokens` n'est **pas une erreur HTTP** — la requête retourne 200. C'est un état métier à détecter soi-même. Beaucoup de systèmes en production livrent silencieusement des JSON tronqués parce que personne ne teste `stop_reason`.

---

## Bloc 4 — La fenêtre de contexte comme mémoire de travail (15 min)

### 4.1 Changer de mental model

La fenêtre de contexte (⚠ 200 000 tokens sur la plupart des modèles Claude actuels, certains modèles proposent 1M en bêta) n'est pas « une limite à ne pas dépasser ». C'est la **mémoire de travail** du modèle : tout ce qu'il « sait » pour cette requête s'y trouve — system prompt, historique, documents, définitions d'outils, résultats d'outils.

**Trois conséquences d'architecture :**

1. **Coût linéaire.** Chaque token d'entrée est facturé à chaque appel. Une conversation qui accumule 150k tokens d'historique coûte 150k tokens d'entrée **par tour**. Sans stratégie (résumé, troncature, caching), le coût d'une conversation croît quadratiquement avec sa longueur (somme des longueurs de préfixes).
2. **Effet « lost in the middle ».** Les modèles rappellent mieux l'information placée en **début** et en **fin** de contexte qu'au **milieu**. Placement stratégique : instructions critiques et question au plus près de la fin ; documents volumineux en tête ; ne jamais enterrer une consigne clé au milieu de 80k tokens de logs.
3. **Latence.** Le temps jusqu'au premier token (TTFT = Time To First Token) croît avec la taille de l'entrée.

### 4.2 Exercice mental (2 min, à l'oral)

« Un assistant RAG (RAG = Retrieval-Augmented Generation, génération augmentée par récupération) injecte 40 chunks de 1 000 tokens. Où placez-vous la question de l'utilisateur ? » → Réponse attendue : après les documents, en fin de prompt, éventuellement répétée si les documents sont très longs.

### 4.3 Calcul de coût en direct

```python
# Ordre de grandeur — tarifs ⚠ volatils, vérifier la grille officielle
PRIX_INPUT_PAR_MTOK = 3.00    # $ / million de tokens d'entrée ⚠
PRIX_OUTPUT_PAR_MTOK = 15.00  # $ / million de tokens de sortie ⚠

def cout_appel(input_tokens: int, output_tokens: int) -> float:
    return (input_tokens * PRIX_INPUT_PAR_MTOK
            + output_tokens * PRIX_OUTPUT_PAR_MTOK) / 1_000_000

# Conversation de 20 tours, historique moyen 30k tokens, réponses 500 tokens
total = sum(cout_appel(30_000, 500) for _ in range(20))
print(f"{total:.2f} $")   # ≈ 1.95 $ pour UNE conversation
```

Multiplier par 10 000 utilisateurs/jour → l'économie du contexte devient un sujet de **direction technique**, pas un détail.

---

## Bloc 5 — Prompt caching (20 min)

### 5.1 Le principe

Le prompt caching permet de **réutiliser le préfixe** d'un prompt déjà traité. On pose des points de rupture `cache_control` ; tout ce qui précède (et correspond exactement) est servi depuis le cache.

**Économie :** ⚠
- **Lecture cache (cache hit) : ~10 % du prix normal → 90 % d'économie** sur la portion cachée.
- **Écriture cache (cache write) : surcoût de ~25 %** sur la portion écrite.
- TTL (TTL = Time To Live, durée de vie) de base : ~5 minutes ⚠, rafraîchi à chaque hit ; option 1 h ⚠ disponible avec surcoût supérieur.

### 5.2 Syntaxe

```python
response = client.messages.create(
    model="claude-sonnet-4-5",  # ⚠
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": GROS_CONTEXTE_METIER,   # ex. 50k tokens de documentation
            "cache_control": {"type": "ephemeral"}   # ← point de rupture
        }
    ],
    messages=[{"role": "user", "content": question_utilisateur}],
)

u = response.usage
print(u.cache_creation_input_tokens)  # tokens écrits au cache (surcoût 25 % ⚠)
print(u.cache_read_input_tokens)      # tokens lus du cache (−90 % ⚠)
print(u.input_tokens)                 # tokens non cachés, plein tarif
```

**Règles à connaître (certification) :**
- Le cache fonctionne sur **préfixe exact** : le moindre octet modifié en amont du breakpoint invalide le cache. → Mettre le contenu **stable en premier** (system, outils, documents), le contenu **variable en dernier** (question).
- Ordre dans le préfixe : `tools` → `system` → `messages`. Un changement de définition d'outils invalide tout.
- Minimum cachable : ⚠ 1 024 tokens sur la plupart des modèles (2 048 sur certains). En dessous, le breakpoint est ignoré silencieusement.
- Jusqu'à **4 breakpoints** ⚠ par requête.

### 5.3 Calcul de rentabilité (à faire faire)

« System prompt + docs = 60k tokens, question = 300 tokens, 500 requêtes/heure. »

- Sans cache : 500 × 60 300 tokens plein tarif.
- Avec cache : 1 écriture (60k × 1,25) + 499 lectures (60k × 0,10) + 500 × 300 plein tarif.
- → économie ≈ **88 %** sur l'entrée. Le cache est rentable dès **2 requêtes** dans la fenêtre TTL (1,25 + 0,10 = 1,35 < 2,00).

**Anti-pattern à citer :** placer un horodatage ou un identifiant de session au début du system prompt → 0 % de cache hit, surcoût de 25 % à chaque appel. On paie *plus cher* qu'en désactivant le cache.

---

## Bloc 6 — Comptage de tokens (10 min)

### 6.1 Pourquoi compter avant d'envoyer

- Valider qu'on tient dans la fenêtre **avant** de payer.
- Dimensionner `max_tokens` intelligemment.
- Pré-calculer un budget / faire du chargeback interne.

### 6.2 L'endpoint dédié

```python
count = client.messages.count_tokens(
    model="claude-sonnet-4-5",   # ⚠ le comptage dépend du modèle
    system="Tu es un assistant juridique.",
    messages=[{"role": "user", "content": contrat_complet}],
)
print(count.input_tokens)   # gratuit, pas de génération ⚠ (soumis à rate limit dédié)
```

**Points clés :**
- Chaque **famille de modèles a son tokenizer** : le même texte ne fait pas le même nombre de tokens sur Claude et sur un modèle GPT (GPT = Generative Pre-trained Transformer), ni forcément entre générations de Claude. → Ne jamais réutiliser un comptage tiktoken (tokenizer OpenAI) pour dimensionner un appel Claude.
- Ordres de grandeur pour l'estimation mentale : **~3,5–4 caractères/token en anglais**, un peu plus de tokens/mot en français (accents, morphologie). Le code et le JSON tokenisent plus densément qu'on ne croit.
- Le comptage inclut system + messages + tools : passer la requête **complète** à `count_tokens`.

---

## Bloc 7 — Streaming SSE (15 min)

### 7.1 Pourquoi streamer

- **UX** (UX = User eXperience, expérience utilisateur) : TTFT perçu de ~1 s au lieu d'attendre 30 s une réponse complète.
- **Obligatoire en pratique** pour les longues générations (les timeouts HTTP guettent les réponses > 10 min).

### 7.2 Le cycle d'événements SSE

SSE = Server-Sent Events : flux HTTP unidirectionnel `text/event-stream`, événements `event:` + `data:` séparés par des lignes vides.

Séquence pour un message simple :

```
message_start          → enveloppe du message (id, model, usage d'entrée)
content_block_start    → ouverture du bloc n°0 (type: text)
content_block_delta    → {"delta": {"type": "text_delta", "text": "Le"}}
content_block_delta    → {"delta": {"type": "text_delta", "text": " protocole"}}
...                       (des dizaines/centaines de deltas)
content_block_stop     → fermeture du bloc n°0
message_delta          → stop_reason + usage de sortie finaux
message_stop           → fin du flux
```

(+ événements `ping` de keep-alive à ignorer, + `error` possible en cours de flux.)

**Piège certification :** `stop_reason` et le compte final de tokens de sortie arrivent dans **`message_delta`**, pas dans `message_start`. Un client qui ne lit pas `message_delta` ne saura jamais si la réponse a été tronquée.

### 7.3 Implémentation

```python
# Version haut niveau (SDK)
with client.messages.stream(
    model="claude-sonnet-4-5",  # ⚠
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explique le netting bilatéral."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()
    print("\n→", final.stop_reason, final.usage)
```

```python
# Version bas niveau (événements bruts) — celle qu'il faut connaître pour la certif
stream = client.messages.create(..., stream=True)
for event in stream:
    match event.type:
        case "message_start":
            msg_id = event.message.id
        case "content_block_delta":
            if event.delta.type == "text_delta":
                buffer += event.delta.text
        case "message_delta":
            stop_reason = event.delta.stop_reason
            output_tokens = event.usage.output_tokens
        case "message_stop":
            break
```

**Démo :** ouvrir le visualiseur de streaming de la page web de session (`webpage/index.html`) — chaque événement s'affiche avec son type coloré. Très efficace pour ancrer la séquence.

---

## Bloc 8 — Batches API (10 min)

### 8.1 Le troisième mode d'exécution

| Mode | Latence | Coût | Cas d'usage |
|------|---------|------|-------------|
| Synchrone | secondes | plein tarif | interactif |
| Streaming | premier token en ~1 s | plein tarif | interactif, longues sorties |
| **Batch** | **jusqu'à 24 h (SLA)** ⚠ | **−50 %** ⚠ | traitement de masse, non urgent |

- Jusqu'à **100 000 requêtes** ⚠ (ou ~256 Mo ⚠) par batch.
- La plupart des batches terminent en **moins d'une heure** en pratique ; 24 h est l'engagement contractuel.
- Chaque requête du batch est indépendante (pas de partage d'état).
- Résultats disponibles 29 jours ⚠, **non garantis dans l'ordre** → toujours corréler par `custom_id`.

### 8.2 Code

```python
batch = client.messages.batches.create(
    requests=[
        {
            "custom_id": f"doc-{i}",                      # clé de corrélation — OBLIGATOIRE
            "params": {
                "model": "claude-haiku-4-5",              # ⚠ petit modèle pour la masse
                "max_tokens": 512,
                "messages": [{"role": "user", "content": f"Classifie : {doc}"}],
            },
        }
        for i, doc in enumerate(documents)
    ]
)

# Polling de l'état
status = client.messages.batches.retrieve(batch.id)
print(status.processing_status)        # in_progress → ended
print(status.request_counts)           # succeeded / errored / canceled / expired

# Récupération (JSONL — JSON Lines, un objet JSON par ligne)
for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        traiter(result.custom_id, result.result.message)
    else:
        rejouer(result.custom_id, result.result)   # errored / expired / canceled
```

**Cas d'usage à faire trouver au groupe :** classification nocturne de tickets, enrichissement de CRM (CRM = Customer Relationship Management), évaluation massive de prompts, génération de métadonnées documentaires, backtesting de prompts sur historiques.

**Piège certification :** batch + prompt caching se **combinent** — les requêtes partageant un long préfixe dans le même batch peuvent bénéficier du cache, cumulant les remises (les hits de cache ne sont pas garantis en batch).

---

## Bloc 9 — Gestion d'erreurs (10 min)

### 9.1 Taxonomie

| Code | Nom | Cause | Retry ? |
|------|-----|-------|---------|
| 400 | invalid_request_error | Requête malformée (rôles, JSON…) | ❌ corriger le code |
| 401 | authentication_error | Clé invalide | ❌ |
| 403 | permission_error | Clé sans accès à la ressource | ❌ |
| 404 | not_found_error | Modèle/ressource inexistant | ❌ |
| 413 | request_too_large | Requête trop grosse | ❌ réduire |
| **429** | **rate_limit_error** | RPM/ITPM/OTPM dépassés | ✅ backoff |
| 500 | api_error | Erreur interne | ✅ backoff |
| 529 | overloaded_error | Surcharge du service | ✅ backoff |

(RPM = Requests Per Minute ; ITPM/OTPM = Input/Output Tokens Per Minute — les trois compteurs de rate limit.)

### 9.2 Backoff exponentiel avec jitter

```python
import random, time
import anthropic

def appel_robuste(client, max_retries=5, **kwargs):
    for tentative in range(max_retries):
        try:
            return client.messages.create(**kwargs)
        except anthropic.RateLimitError as e:
            # Priorité au serveur : respecter retry-after s'il est fourni
            retry_after = e.response.headers.get("retry-after")
            if retry_after is not None:
                delai = float(retry_after)
            else:
                delai = min(60, (2 ** tentative)) * random.uniform(0.5, 1.5)  # jitter
            time.sleep(delai)
        except anthropic.APIStatusError as e:
            if e.status_code in (500, 529):
                time.sleep(min(60, 2 ** tentative) * random.uniform(0.5, 1.5))
            else:
                raise    # 4xx ≠ 429 : inutile de réessayer
    raise ExhaustedRetriesError()
```

**Trois points d'architecte :**
1. **`retry-after` prime sur votre formule** — le serveur sait mieux que vous quand réessayer.
2. **Jitter obligatoire** — sans aléa, tous vos workers réessaient au même instant (« thundering herd », effet troupeau).
3. Le SDK officiel fait déjà 2 retries par défaut — connaître ce comportement avant d'empiler votre propre couche (risque de retries multiplicatifs).

Mentionner aussi : surveiller les en-têtes `anthropic-ratelimit-*-remaining` pour du throttling **proactif** plutôt que réactif.

---

## Bloc 10 — Clôture (5 min)

- Rappel du fil rouge : *paramètres → signaux (stop_reason) → économie (contexte, cache, batch) → robustesse (erreurs)*.
- Annoncer les 3 exercices (constructeur de requête, streaming, conception batch) et le QCM.
- Distribuer les exit tickets.

---

## Exit tickets (5)

À remplir en 3 minutes, ramassés à la sortie :

1. **stop_reason :** votre application reçoit `stop_reason: "max_tokens"` sur une extraction JSON. Que s'est-il passé et que fait votre code ? (2 phrases)
2. **Caching :** pourquoi placer un horodatage au début du system prompt ruine-t-il le prompt caching, et combien cela coûte-t-il en plus (%) ? 
3. **Streaming :** dans quel événement SSE trouve-t-on le `stop_reason` final ? (nom exact)
4. **Batch :** citez deux conditions qui rendent la Batches API préférable à des appels synchrones, et la remise associée.
5. **Erreurs :** on reçoit un 429 avec en-tête `retry-after: 12`. Quel délai appliquer et pourquoi ne pas utiliser sa propre formule de backoff dans ce cas ?

**Corrigé express :** 1) plafond `max_tokens` atteint, JSON tronqué → détecter et relancer avec plafond supérieur ou continuation. 2) Le cache exige un préfixe exact ; l'horodatage change à chaque appel → 0 hit, mais on paie le surcoût d'écriture ~25 % ⚠. 3) `message_delta`. 4) Volume massif + pas d'exigence de latence (SLA 24 h ⚠) → −50 % ⚠. 5) 12 s : l'en-tête serveur prime sur toute heuristique client.

---

## Annexes formateur

- **Risques de démo live :** prévoir des réponses enregistrées (fixtures) au cas où le réseau/la clé API lâche. Le visualiseur SSE de la page web fonctionne **hors ligne** (simulation).
- **Différenciation :** participants rapides → leur faire implémenter la continuation après `max_tokens` (re-prompt avec la sortie partielle en prefill assistant).
- **Toutes les valeurs ⚠** (tarifs, TTL, limites, noms de modèles) doivent être revalidées sur https://docs.anthropic.com avant chaque session.
