# Documentation API DahuGPT

## Base URL
```
http://localhost:8000/api
```

## Authentication
Toutes les routes protégées nécessitent un token Bearer obtenu lors de l'inscription ou de la connexion.

```
Authorization: Bearer {token}
```

Les métadonnées publiques sous `/.well-known` (OAuth/OIDC documentaires) et leur rapport avec Sanctum sont décrites dans **`docs/backend/agent-discovery.md`**.

---

## 🔓 Routes Publiques

**Limitation de débit :** `POST /api/register` et `POST /api/login` acceptent au plus **10 requêtes par minute et par adresse IP** (`throttle:10,1`). Au-delà, la réponse est **429 Too Many Requests**.

### POST /api/register
Inscription d'un nouvel utilisateur avec 10 crédits de bienvenue.

**Request Body:**
```json
{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "password123",
  "password_confirmation": "password123",
  "preferred_language": "fr"
}
```

**Response (201):**
```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits_balance": "10.00",
    "preferred_language": "fr",
    "created_at": "2025-12-31T10:00:00.000000Z"
  },
  "token": "1|xxx...",
  "token_type": "Bearer"
}
```

**Erreur serveur (500) :** en cas d’exception interne lors de l’inscription, le message renvoyé au client est **générique** (aucun détail technique). En environnement `local` uniquement, une clé `debug` peut contenir le message d’exception pour faciliter le diagnostic.

---

### POST /api/login
Connexion d'un utilisateur existant.

**Request Body:**
```json
{
  "email": "john@example.com",
  "password": "password123"
}
```

**Response (200):**
```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits_balance": "10.00",
    "preferred_language": "fr"
  },
  "token": "2|xxx...",
  "token_type": "Bearer"
}
```

**Error (422):**
```json
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["Les identifiants fournis sont incorrects."]
  }
}
```

**429 :** voir la limitation de débit sur les routes publiques d’authentification ci-dessus.

---

## 🔒 Routes Protégées

### GET /api/user
Récupérer les informations de l'utilisateur connecté.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):** l’objet `user` suit `UserResource` (crédits, langue, etc.). Si la relation `subscription` est chargée, un objet `subscription` peut être présent :

```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits": 10,
    "subscription": {
      "id": "01JGXXX...",
      "plan": "plus",
      "stripe_status": "active",
      "cancel_at_period_end": false,
      "stripe_canceled_at": null,
      "expires_at": "2026-04-24T12:00:00+00:00"
    }
  }
}
```

---

### POST /api/logout
Déconnexion de l'utilisateur (révocation du token).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "message": "Déconnexion réussie"
}
```

---

## 💬 Chat

### POST /api/chat
Envoyer un message au chat IA et recevoir une réponse.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "prompt": "Explique-moi la théorie de la relativité",
  "model": "anthropic/claude-3.5-sonnet",
  "conversation_id": "conv-123-abc",
  "type": "general"
}
```

**Paramètres:**
- `prompt` (required) : Le message de l'utilisateur (5-5000 caractères)
- `model` (optional) : Le modèle à utiliser (auto-sélectionné si non spécifié)
- `conversation_id` (optional) : ID de la conversation pour inclure le contexte
- `type` (optional) : Type de chat (`general` ou `code`, défaut: `general`)

**Response (200):**
```json
{
  "status": "success",
  "chat": {
    "id": "01JGYYY...",
    "conversation_id": "conv-123-abc",
    "prompt": "Explique-moi la théorie de la relativité",
    "response": "La théorie de la relativité...",
    "model": "anthropic/claude-3.5-sonnet",
    "credits_used": "0.025",
    "cached": false,
    "created_at": "2026-01-03T10:05:00Z"
  },
  "user": {
    "credits_balance": "9.975"
  }
}
```

**Métadonnées de Contexte:**

Lorsqu'un `conversation_id` est fourni, le système inclut automatiquement les messages précédents de la conversation. Le nombre de messages inclus dépend de votre plan :

| Plan | Max Messages | Max Tokens |
|------|--------------|------------|
| Basic | 5 | 2 000 |
| Plus | 15 | 8 000 |
| Pro | 30 | 16 000 |

Les champs suivants sont ajoutés à la réponse :
- `context_messages_count` : Nombre de messages de contexte utilisés
- `context_tokens_estimated` : Tokens estimés du contexte

**Exemple avec Contexte:**
```json
{
  "status": "success",
  "chat": {
    "id": "01JGYYY...",
    "conversation_id": "conv-123-abc",
    "prompt": "Et Einstein, que pensait-il ?",
    "response": "Einstein, dont nous parlions précédemment...",
    "model": "anthropic/claude-3.5-sonnet",
    "credits_used": "0.035",
    "cached": false,
    "context_messages_count": 5,
    "context_tokens_estimated": 1200,
    "created_at": "2026-01-03T10:06:00Z"
  },
  "user": {
    "credits_balance": "9.940"
  }
}
```

**Error (402):**
```json
{
  "error": "Crédits insuffisants",
  "credits_balance": "0.00"
}
```

**Error (500):**
```json
{
  "error": "Erreur lors de la communication avec l'API",
  "details": {}
}
```

### GET /api/models/themes
Thèmes (contextes) disponibles pour le plan courant, pour le sélecteur du chat.

**Headers :** `Authorization: Bearer {token}`

**Response (200) :** `plan`, `themes` (liste de `{ key, label }`). L’ordre des entrées suit le champ **`sort_order`** des lignes `plan_contexts` (configurable par plan dans l’admin Filament) : valeur la plus petite en premier ; en cas d’égalité, ordre alphabétique des clés de thème.

### GET /api/models/levels
Niveaux disponibles pour un thème donné (`?theme=...`).

---

### GET /api/conversations
Récupérer la liste des conversations de l'utilisateur (paginé, 20 par page).

**Headers:**
```
Authorization: Bearer {token}
```

**Query Parameters:**
- `page` (optional) : Numéro de page (défaut: 1)

**Response (200):**
```json
{
  "conversations": {
    "current_page": 1,
    "data": [
      {
        "conversation_id": "conv-123-abc",
        "last_message_at": "2026-01-03T10:05:00.000000Z",
        "message_count": 5,
        "last_prompt": "Explique-moi la théorie de la relativité",
        "last_response": "La théorie de la relativité...",
        "model": "anthropic/claude-3.5-sonnet",
        "model_theme": "general",
        "model_level": "premium"
      }
    ],
    "total": 1,
    "per_page": 20,
    "last_page": 1
  }
}
```

**Champs de réponse:**
- `conversation_id` : Identifiant unique de la conversation
- `last_message_at` : Date du dernier message
- `message_count` : Nombre total de messages dans la conversation
- `last_prompt` : Dernier prompt de l'utilisateur
- `last_response` : Dernière réponse de l'IA
- `model` : Clé du modèle OpenRouter utilisé (pour compatibilité/débogage)
- `model_theme` : Type de modèle/thème utilisé (ex: `general`, `development`, `marketing`, etc.)
- `model_level` : Niveau du modèle utilisé (ex: `light`, `standard`, `premium`, ou `null` si non trouvé)

**Note:** Les champs `model_theme` et `model_level` sont dérivés du dernier message de la conversation en recherchant le `ModelTheme` correspondant au `model_key`. Si aucun `ModelTheme` n'est trouvé, `model_theme` prend la valeur par défaut `"general"` et `model_level` est `null`.

---

### GET /api/conversations/today
Récupérer la dernière conversation du jour pour l'utilisateur connecté.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "conversation_id": "conv-123-abc",
  "messages": [
    {
      "id": "01JGYYY...",
      "prompt": "Bonjour",
      "response": "Bonjour ! Comment puis-je vous aider ?",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.000",
      "created_at": "2026-01-03T10:00:00.000000Z"
    }
  ]
}
```

**Comportement:**
- Si aucune conversation n'existe aujourd'hui, `conversation_id` est `null` et `messages` est un tableau vide
- Seules les conversations créées le jour même sont retournées
- Les conversations ouvertes depuis l'historique ne sont pas affectées par cette route

---

### GET /api/conversations/{conversationId}
Récupérer tous les messages d'une conversation spécifique.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "conversation_id": "conv-123-abc",
  "messages": [
    {
      "id": "01JGYYY...",
      "prompt": "Premier message",
      "response": "Première réponse",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.025",
      "created_at": "2026-01-03T10:00:00.000000Z"
    },
    {
      "id": "01JGZZZ...",
      "prompt": "Deuxième message",
      "response": "Deuxième réponse",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.030",
      "created_at": "2026-01-03T10:05:00.000000Z"
    }
  ]
}
```

**Note:** Cette route permet de charger une conversation depuis l'historique. Lorsqu'une conversation est chargée de cette manière, elle reste affichée dans le chat même si elle n'est pas du jour, contrairement au comportement automatique de chargement de la conversation du jour.

---

### DELETE /api/conversations/{conversationId}
Supprimer une conversation complète (tous ses messages).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "message": "Conversation supprimée avec succès",
  "deleted_count": 5
}
```

**Sécurité:** Un utilisateur ne peut supprimer que ses propres conversations. Les tentatives de suppression de conversations d'autres utilisateurs sont ignorées silencieusement.

---

## Persona (assistants Infomaniak)

### GET /api/personas/{persona}/modules
Configuration publique des modules (prompts masqués côté client utile : flags d’import, `allowed_document_mimes`, etc.). Voir [personas.md](../backend/personas.md).

### POST /api/personas/{persona}/chat
Envoi d’un message (JSON ou `multipart/form-data` si pièce jointe).

**Champs courants :**
- `persona_type` : doit être identique à `{persona}` dans l’URL (notamment si `attachment` est présent).
- `prompt` : texte utilisateur.
- `mode` (optionnel) : clé du module (ex. `general`). Si absent, le backend résout un module par défaut (voir doc backend Persona).
- `conversation_id` (optionnel) : poursuite d’une conversation existante.
- Avec fichier : `attachment`, `attachment_kind` (`image` | `file` | `audio` | `video` | `document`).

**Validation des fichiers :** les types MIME des **documents** sont imposés par la liste **`allowed_document_mimes`** du module en base (admin Filament), en complément des flags `allow_*`. Les réponses d’erreur de validation utilisent le code **422**.

Documentation détaillée : [personas.md](../backend/personas.md).

---

## 💳 Crédits

### GET /api/credits/balance
Consulter le solde de crédits.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "credits_balance": "10.00",
  "subscription": {
    "id": "01JGZZZ...",
    "plan": "plus",
    "expires_at": "2026-01-31T10:00:00.000000Z"
  }
}
```

---

### POST /api/credits/topup (DÉPRÉCIÉ)

> **Attention** : Ce endpoint est déprécié depuis l'intégration Stripe Checkout. Les achats de crédits passent désormais par `POST /api/stripe/checkout`.

**Response (410 Gone):**
```json
{
  "message": "Ce endpoint est déprécié. Utilisez Stripe Checkout via POST /api/stripe/checkout."
}
```

---

### GET /api/credits/history
Historique des achats de crédits (paginé, 20 par page).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "current_page": 1,
  "data": [
    {
      "id": "01JGAAA...",
      "user_id": "01JGXXX...",
      "amount": "50.00",
      "stripe_payment_id": "pi_xxx...",
      "created_at": "2026-01-15T10:10:00.000000Z"
    }
  ],
  "total": 1,
  "per_page": 20
}
```

---

## 💰 Stripe Checkout (Paiements)

### GET /api/subscribe/data
Récupérer les données des plans et packs de crédits (public, sans authentification).

**Query Parameters:**
- `locale` (optional) : Langue des traductions (fr, en, de). Défaut: fr

**Response (200):**
```json
{
  "plans": [
    {
      "key": "basic",
      "name": "Basic",
      "price_chf": 6.90,
      "credits_per_month": 100,
      "stripe_price_id": "price_xxx",
      "is_popular": false,
      "features": ["100 crédits/mois", "5 messages de contexte"]
    }
  ],
  "personas": [
    {
      "key": "bureautique",
      "name": "Bureautique",
      "description": "...",
      "price_chf": 9.90,
      "credits_per_billing_period": 200
    }
  ],
  "packs": [
    {
      "id": "01KHRQ...",
      "label": "Starter",
      "price_chf": 5.00,
      "credits": 50,
      "bonus": null,
      "popular": false
    }
  ],
  "currency": "CHF"
}
```

---

### POST /api/stripe/checkout
Créer une session Stripe Checkout et retourner l'URL de redirection.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body (abonnement):**
```json
{
  "type": "subscription",
  "plan_key": "plus",
  "persona_keys": ["bureautique"]
}
```

Les personas sélectionnables ont un `stripe_price_id` configuré. Le paramètre `persona_keys` est optionnel.

**Request Body (pack de crédits):**
```json
{
  "type": "pack",
  "pack_id": "01KHRQ..."
}
```

**Paramètres:**
- `type` (required) : `subscription` ou `pack`
- `plan_key` (required si type=subscription) : `basic`, `plus` ou `pro`
- `persona_keys` (optional) : tableau de clés de personas à ajouter à l'abonnement (ex: `["bureautique"]`)
- `pack_id` (required si type=pack) : ID du pack de crédits

**Response (200):**
```json
{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
```

**Error (422):**
```json
{
  "error": "Plan invalide ou indisponible"
}
```

**Codes promo :** si `STRIPE_ALLOW_PROMOTION_CODES` est activé (défaut), la page Stripe Checkout affiche un champ pour saisir un code promotionnel (configuré dans le Dashboard Stripe : Coupons + codes promotionnels).

**Flux après paiement:**
1. L'utilisateur est redirigé vers Stripe Checkout
2. Après paiement, redirection vers `/subscribe/success` ou `/subscribe/cancel`
3. Stripe envoie un webhook qui finalise l'opération côté backend

---

### POST /api/stripe/billing-portal
Créer une session du **portail client Stripe** et retourner l’URL de redirection (factures, carte, annulation selon le Dashboard Stripe).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "return_url": "https://votre-app.com/subscribe"
}
```

L’utilisateur doit avoir un `stripe_customer_id` (après un checkout ou un client créé côté API).

**Sécurité :** `return_url` doit être une URL **http(s)** dont le **hôte** correspond à celui de `APP_URL` (protection contre les redirections ouvertes). En production, seul **https** est accepté. Des hôtes supplémentaires (ex. `127.0.0.1` si `APP_URL` utilise `localhost`) peuvent être listés dans `.env` : `STRIPE_BILLING_PORTAL_EXTRA_HOSTS` (CSV).

**Response (200):**
```json
{
  "url": "https://billing.stripe.com/session/..."
}
```

**Error (422):** sans client Stripe ou URL invalide.

---

### POST /api/stripe/webhook
Recevoir et traiter les événements Stripe (public, vérifié par signature).

**Headers:**
```
Stripe-Signature: t=...,v1=...
```

**Événements gérés:**

| Événement | Action |
|-----------|--------|
| `checkout.session.completed` (subscription) | Créer Subscription + items (plan + personas) + crédits chat + crédits persona |
| `checkout.session.completed` (payment) | Créer CreditPurchase + crédits achetés |
| `customer.subscription.updated` | Mettre à jour `expires_at` et champs Stripe (`stripe_status`, `cancel_at_period_end`, etc.) ; resynchroniser `billing_subscription_items` |
| `invoice.paid` | Renouveler crédits chat + crédits persona (idempotence via invoice_id) |
| `customer.subscription.deleted` | Détacher l’utilisateur, supprimer items et crédits persona, **vider les crédits mensuels** |

**Response:** `200` (succès) ou `400` (signature invalide)

---

## 📦 Abonnements (Legacy)

### POST /api/subscribe
Souscrire à un plan d'abonnement (legacy, utiliser `POST /api/stripe/checkout` de préférence).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "plan": "plus",
  "stripe_subscription_id": "sub_xxx..."
}
```

**Plans disponibles (CHF):**
- `basic` - 100 crédits/mois - 6.90 CHF
- `plus` - 500 crédits/mois - 14.90 CHF
- `pro` - 2000 crédits/mois - 29.90 CHF

---

### PATCH /api/subscribe/plan
Changer de plan (upgrade/downgrade) sur l'abonnement Stripe actif.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "plan_key": "pro"
}
```

### POST /api/subscribe/personas/{personaKey}
Ajouter un persona à l'abonnement actif.

### DELETE /api/subscribe/personas/{personaKey}
Retirer un persona de l'abonnement actif. N'impacte pas le plan principal.

---

### POST /api/subscribe/cancel
Annuler l’abonnement **via Stripe** : par défaut **fin de période** (`cancel_at_period_end`), l’accès reste jusqu’à `expires_at` (synchronisé par webhooks).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body (optionnel):**
```json
{
  "immediate": true
}
```

- Sans `immediate` ou `immediate: false` : annulation en fin de période (recommandé).
- `immediate: true` : annulation immédiate côté Stripe (webhook `customer.subscription.deleted`).

Exige un `stripe_subscription_id` sur la subscription locale.

**Response (200)** — fin de période :
```json
{
  "message": "Votre abonnement reste actif jusqu'à la fin de la période en cours.",
  "subscription": {
    "id": "01JGXXX...",
    "plan": "plus",
    "expires_at": "2026-04-24T12:00:00+00:00",
    "stripe_status": "active",
    "cancel_at_period_end": true,
    "stripe_canceled_at": null
  }
}
```

**Response (200)** — immédiat :
```json
{
  "message": "Annulation immédiate demandée. La résiliation sera confirmée sous peu.",
  "immediate": true
}
```

**Error (422):** pas d’abonnement Stripe (`stripe_subscription_id` manquant) ou erreur API.

---

### POST /api/subscribe/resume
Annuler une **annulation planifiée** (`cancel_at_period_end`) si Stripe l’autorise encore.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):** objet `subscription` mis à jour (comme ci-dessus).

**Error (422):** pas d’abonnement Stripe actif.

---

## 📊 Codes de statut HTTP

| Code | Signification |
|------|---------------|
| 200  | OK - Requête réussie |
| 201  | Created - Ressource créée |
| 400  | Bad Request - Signature webhook invalide |
| 401  | Unauthorized - Token manquant ou invalide |
| 402  | Payment Required - Crédits insuffisants |
| 404  | Not Found - Ressource non trouvée |
| 410  | Gone - Endpoint déprécié (ex: `topup` direct) |
| 422  | Unprocessable Entity - Validation échouée |
| 429  | Too Many Requests - Limite de débit (ex. auth publique) |
| 500  | Internal Server Error - Erreur serveur |

---

## 🔐 Sécurité

- Tous les mots de passe sont hashés avec bcrypt
- Les tokens sont générés avec Laravel Sanctum ; une **durée de vie optionnelle** (en minutes) peut être définie via `SANCTUM_TOKEN_EXPIRATION` dans `.env` (voir `.env.example`)
- Les prompts sont hashés en SHA-256 pour éviter les duplications
- Validation stricte sur toutes les entrées ; pièces jointes **Persona** : types MIME alignés sur la configuration admin (`allowed_document_mimes`, flags `allow_*`)
- Réponses HTML (Inertia) : en-têtes `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` (pas de CSP stricte par défaut, compatibilité Vite / HMR)
- Authentification publique : limitation de débit sur `/api/register` et `/api/login` (voir ci-dessus)

---

## 💡 Exemples avec cURL

### Inscription
```bash
curl -X POST http://localhost:8000/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123",
    "password_confirmation": "password123"
  }'
```

### Chat
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Bonjour!",
    "language": "fr"
  }'
```

### Consulter le solde
```bash
curl -X GET http://localhost:8000/api/credits/balance \
  -H "Authorization: Bearer YOUR_TOKEN"
```

---

## 🧪 Tests

Pour tester l'API, vous pouvez utiliser:
- **Postman** - Importer la collection
- **Insomnia** - Tester les endpoints
- **cURL** - Ligne de commande
- **PHPUnit** - Tests automatisés

```bash
php artisan test
```

---

## 📝 Notes

- Les crédits sont calculés en fonction des tokens utilisés par OpenRouter
- Le hash du prompt évite les requêtes en double (cache potentiel)
- Les abonnements expirent automatiquement après 30 jours
- Un bonus de 10 crédits est offert à l'inscription
- Tous les montants sont en **CHF** (franc suisse)
- Les paiements passent par **Stripe Checkout** (mode subscription ou payment)
- Les plans et packs de crédits sont configurés en base de données avec traductions dynamiques
- Pour tester les paiements, utiliser la carte Stripe de test : `4242 4242 4242 4242`
- Documentation complète Stripe : [stripe-integration.md](../backend/stripe-integration.md)

## 🧠 Système de Contexte Adaptatif

DahuGPT utilise un système intelligent de gestion du contexte des conversations :

### Comment ça marche ?

Lorsque vous envoyez un message avec un `conversation_id`, le système :
1. Charge les N derniers messages de la conversation (N dépend de votre plan)
2. Vérifie que le total ne dépasse pas la limite de tokens
3. Ajuste automatiquement si nécessaire
4. Envoie tout le contexte au modèle IA

### Limites par Plan

| Plan | Messages | Tokens | Prix/mois |
|------|----------|--------|-----------|
| **Basic** | 5 | 2 000 | 6.90 CHF |
| **Plus** | 15 | 8 000 | 14.90 CHF |
| **Pro** | 30 | 16 000 | 29.90 CHF |

### Limites par Modèle

Le système respecte également les limites de contexte des modèles :

| Modèle | Contexte Max |
|--------|--------------|
| Claude 3.5 Sonnet | 200k tokens |
| GPT-4 Turbo | 128k tokens |
| Deepseek V3 | 64k tokens |
| Gemini 2.0 Flash | 1M tokens |

### Exemple d'Utilisation

**Premier message (sans contexte):**
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Qui était Einstein ?",
    "conversation_id": "conv-123"
  }'
```

**Deuxième message (avec contexte):**
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Quelles étaient ses découvertes ?",
    "conversation_id": "conv-123"
  }'
```

Le modèle IA reçoit automatiquement le premier message et sa réponse comme contexte.

### Avantages

✅ **Réponses cohérentes** : L'IA se souvient de la conversation  
✅ **Automatique** : Pas besoin de gérer manuellement l'historique  
✅ **Optimisé** : Ajustement intelligent selon votre plan  
✅ **Économique** : Cache pour réduire les coûts

Pour plus de détails, consultez [context-system.md](context-system.md).
