← Documentation API Reference

Algorithme de Routage AISIA

Version : 4.15.0 Société : AISIA — Structure juridique en cours de création URL publique : https://aisia.fr/

---

Table des matières

1. Vue d'ensemble du routage 2. UCB1 : Upper Confidence Bound 3. Thompson Sampling 4. Cost-aware routing 5. Latency-aware routing 6. Domain routing 7. Circuit breaker 8. Feature extraction contextuelle 9. Pipeline de routage complet 10. État persistant Redis 11. Configuration et tuning 12. Analyse et interprétation

---

1. Vue d'ensemble du routage

Le système de routage AISIA orchestre la sélection du meilleur provider pour chaque requête utilisateur. Il combine plusieurs algorithmes :

Requête utilisateur
       │
       ▼
┌──────────────────┐
│ Feature Extractor │ ← Analyse le prompt (domaine, longueur, code, etc.)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│  Domain Router   │ ← Filtre les providers spécialisés (15 domaines)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ Circuit Breaker  │ ← Exclut les providers en panne (OPEN)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│  Bandit Router   │ ← Sélectionne le meilleur (UCB1 ou Thompson)
│  + Cost-aware    │
│  + Latency-aware │
└──────────────────┘
       │
       ▼
  Provider sélectionné

---

2. UCB1 : Upper Confidence Bound

Principe

UCB1 est un algorithme de bandit multi-bras qui équilibre exploration (tester des providers peu connus) et exploitation (utiliser le meilleur provider connu).

Formule

score_i = mean_reward_i + C * sqrt(ln(N) / n_i) + cost_bonus_i + latency_bonus_i

Où :

- Formule : total_reward_i / n_i - Plage : [0.0, 1.0] - Variable : BANDIT_EXPLORATION_BONUS - Plus grand C = plus d'exploration - N = sum(n_i) pour tous les providers - pulls dans le code

Exploration vs exploitation

Le terme C * sqrt(ln(N) / n_i) est le bonus d'exploration.

SituationValeur du termeComportement
Provider peu testé (n_i petit)ÉlevéSera sélectionné pour exploration
Provider très testé (n_i grand)FaibleSélectionné uniquement si mean_reward élevé
Provider jamais testé (n_i = 0)InfiniToujours sélectionné en premier

Garantie d'exploration initiale

# Code dans bandit.py
for p in candidates:
    if states[p][0] == 0:  # pulls == 0
        return p  # Sélectionné immédiatement

Tout provider avec 0 pulls est automatiquement sélectionné avant d'appliquer la formule UCB1. Cela garantit que chaque provider est testé au moins une fois.

Exemple numérique

Prenons 3 providers après 100 requêtes :

Providerpullsmean_rewardUCB1 termCost bonusLatency bonusScore final
Cerebras500.850.430.0980.1451.523
OpenAI300.900.520.0500.1001.570
Groq200.800.600.0970.1401.637
Groq est sélectionné car son bonus d'exploration compense sa reward moyenne plus faible. Après plus de tests, si sa qualité reste inférieure, son score diminuera.

---

3. Thompson Sampling

Principe

Thompson Sampling utilise un échantillonnage bayésien. Chaque provider est modélisé par une distribution Beta(alpha, beta) qui représente notre croyance sur sa probabilité de succès.

Formule

sample_i ~ Beta(alpha_i, beta_i)

À chaque requête, un échantillon aléatoire est tiré de la distribution de chaque provider. Le provider avec l'échantillon le plus élevé est sélectionné.

Paramètres

ParamètreInitialisationMise à jour
alpha1.0+1.0 si reward >= 0.5 (succès)
beta1.0+1.0 si reward < 0.5 (échec)

Interprétation de la distribution Beta

alphabetaInterprétation
11Inconnu (distribution uniforme)
102Bon provider (80% succès)
55Provider moyen (50% succès)
210Mauvais provider (17% succès)
505Excellent provider, haute confiance

Avantages par rapport à UCB1

sont naturellement explorés davantage

Quand utiliser Thompson vs UCB1

SituationRecommandation
Peu de providers (< 10)UCB1 (plus stable)
Beaucoup de providers (> 20)Thompson (convergence plus rapide)
Performances stablesUCB1
Performances variablesThompson
Besoin d'explicabilitéUCB1 (formule déterministe)
---

4. Cost-aware routing

Principe

Le routage cost-aware favorise les providers économiques en ajoutant un bonus au score des providers moins chers.

Formule

cost_bonus = max(0, 1.0 - cost / 10.0) * cost_weight

Où :

Table des coûts (COST_PER_1M)

ProviderCoût $/1M tokensCost bonus (weight=0.1)
Cerebras0.200.098
Groq0.300.097
DeepSeek0.500.095
DeepInfra0.600.094
Together0.800.092
Fireworks0.900.091
Cohere1.000.090
Perplexity1.000.090
Gemini1.250.088
AI211.500.085
Mistral2.000.080
OpenRouter3.000.070
OpenAI5.000.050
Anthropic8.000.020
HuggingFace0.000.100

Impact du cost_weight

cost_weightEffet
0.0Aucun impact du coût sur le routage
0.05Léger avantage pour les providers économiques
0.10 (défaut)Avantage modéré
0.20Fort avantage pour les providers économiques
0.50Le coût domine la décision

Résolution du nom de provider

Le coût est résolu par le préfixe du provider_id :

cost = self.COST_PER_1M.get(p.split("-")[0], 2.0)

Par exemple, openai-gpt4 utilise le coût d'openai. Si le préfixe n'est pas dans la table, un coût par défaut de 2.0 est utilisé.

---

5. Latency-aware routing

Principe

Le routage latency-aware favorise les providers rapides en ajoutant un bonus basé sur leur latence moyenne historique.

Formule

latency_bonus = max(0, 1.0 - avg_latency / 30.0) * latency_weight

Où :

Exponential Moving Average (EMA)

La latence moyenne est calculée avec un EMA qui donne plus de poids aux mesures récentes :

# Pour un provider avec des mesures existantes (pulls > 0)
new_avg_lat = avg_lat  0.8 + last_latency_s  0.2

Pour un nouveau provider (pulls == 0)

new_avg_lat = last_latency_s

Le facteur 0.8/0.2 signifie que chaque nouvelle mesure représente 20% du poids, et l'historique 80%.

Exemples

Provideravg_latency (s)Latency bonus (weight=0.15)
Cerebras0.40.148
Groq0.60.147
Ollama local2.00.140
OpenAI3.00.135
Anthropic4.00.130
Provider lent15.00.075
Provider très lent30.00.000

Initialisation

La latence moyenne est initialisée à 5.0 secondes pour tous les providers. Cela permet une estimation conservatrice avant les premiers appels.

---

6. Domain routing

Principe

Le DomainRouter analyse le contenu de la requête pour détecter un domaine de spécialisation et filtrer les providers disposant de modèles spécialisés.

Les 15 domaines

DomaineDétectionPatterns clés
mathRegex multi-languecalcul, équation, dérivée, matrice, probability, sqrt, \sum
codingRegex + détection syntaxecode, function, class, def, python, API, debug, pytest
reasoningRegex analyse complexeraisonne, explique pourquoi, step by step, chain of thought
imageRegex générationgénère image, dessine, illustre, stable diffusion, dall-e
voiceRegex TTStext to speech, synthèse vocale, voix, narration
transcriptionRegex STTtranscrire, speech to text, whisper, dictée
translationRegex multilinguetraduis, traduction, en anglais, en français, translate to
visionRegex analyse imagecette image, décris image, OCR, lire document
embeddingRegex vectorisationembed, vectorise, similarity, semantic search, cosine
medicalRegex cliniquemédical, diagnostic, symptôme, patient, maladie, IRM
financeRegex financierfinanc, bourse, stock, trading, ROI, EBITDA
scienceRegex scientifiquephysique, chimie, biologie, molécule, quantum
classificationRegex classifclassifie, catégorise, label, spam, toxic, detect
sentimentRegex opinionsentiment, opinion, positif négatif, satisfaction, avis
legalRegex juridiquejuridique, contrat, réglementation, RGPD, droit, tribunal

Mécanisme de détection

def detect_domain(prompt: str) -> str | None:
    scores = {}
    for domain, pattern in _DOMAIN_PATTERNS.items():
        matches = pattern.findall(prompt)
        if matches:
            scores[domain] = len(matches)
    if not scores:
        return None
    best = max(scores, key=scores.get)
    return best if scores[best] >= 1 else None

Le domaine avec le plus de matches est sélectionné. En cas d'égalité, max() retourne le premier trouvé (ordre du dictionnaire).

Filtrage des providers spécialisés

Quand un domaine est détecté :

1. Les providers sont filtrés par le champ specialized dans leurs modèles (providers.yaml) 2. Si des spécialistes existent pour ce domaine, seuls ces providers sont proposés au BanditRouter 3. Si aucun spécialiste n'existe, tous les providers généralistes sont utilisés (fallback)

def filter_specialized_providers(providers, domain):
    specialized = []
    for p in providers:
        has_specialist = any(
            m.get("specialized") == domain for m in p.get("models", [])
        )
        if has_specialist:
            specialized.append(p)
    return specialized

Sélection du modèle spécialisé

Si un provider spécialisé est sélectionné, AISIA utilise le modèle ayant l'annotation specialized correspondante plutôt que le modèle par défaut du provider.

def get_specialized_model(provider, domain):
    for m in provider.get("models", []):
        if m.get("specialized") == domain:
            return m["name"]
    return None

---

7. Circuit breaker

Principe

Le circuit breaker protège le système contre les appels répétés à un provider défaillant. Il suit le pattern classique à trois états.

Diagramme d'états

     succès (décrémente failures)
          ┌─────┐
          │     │
          ▼     │
     ┌─────────┐                    ┌──────────┐
     │ CLOSED  │─── failures >= 5 ──▶│  OPEN    │
     │ (normal)│                    │ (bloqué) │
     └─────────┘                    └──────────┘
          ▲                              │
          │                              │ timeout_s expiré
          │                              ▼
          │                        ┌───────────┐
          └── 3 succès ────────────│ HALF_OPEN │
                                   │  (test)   │
                                   └───────────┘
                                        │
                                        │ 1 échec
                                        ▼
                                   ┌──────────┐
                                   │  OPEN    │
                                   └──────────┘

Paramètres

ParamètreVariableDéfautDescription
Seuil d'échecsCB_FAILURE_THRESHOLD5Échecs consécutifs avant ouverture
TimeoutCB_TIMEOUT_S60Durée en OPEN avant test (secondes)
Max appels half-openCB_HALF_OPEN_MAX_CALLS3Succès requis pour fermer

Transitions détaillées

#### CLOSED -> OPEN

Quand le nombre d'échecs atteint failure_threshold (5 par défaut), le circuit s'ouvre. Toutes les requêtes suivantes sont bloquées.

if new_failures >= self.failure_threshold:
    await self._save_state(CBState.OPEN, new_failures, now, 0)

#### OPEN -> HALF_OPEN

Après timeout_s secondes (60 par défaut), le circuit passe en HALF_OPEN. Un nombre limité de requêtes de test sont autorisées.

elapsed = time.time() - last_failure_time
if elapsed > self.timeout_s:
    await self._save_state(CBState.HALF_OPEN, failures, last_failure_time, 0)
    return False  # autoriser la requête

#### HALF_OPEN -> CLOSED

Si half_open_max_calls succès consécutifs (3 par défaut) sont enregistrés, le circuit se ferme et le fonctionnement normal reprend.

if new_success >= self.half_open_max_calls:
    await self._save_state(CBState.CLOSED, 0, 0.0, 0)

#### HALF_OPEN -> OPEN

Si un seul échec survient en HALF_OPEN, le circuit se rouvre immédiatement.

if s["state"] == CBState.HALF_OPEN:
    await self._save_state(CBState.OPEN, new_failures, now, 0)

#### CLOSED : auto-guérison

En état CLOSED, chaque succès décrémente le compteur d'échecs :

if s["state"] == CBState.CLOSED:
    new_failures = max(0, s["failures"] - 1)

État Redis

L'état du circuit breaker est persisté dans Redis (hash) :

cb:{provider_id} → {state, failures, last_failure_time, success_count}

TTL : 3600 secondes (1 heure).

Si Redis est indisponible, l'état est maintenu en mémoire locale (non partagé entre replicas).

Métrique Prometheus

ai_circuit_breaker_state{provider="xxx"} = 0|1|2
  0 = CLOSED
  1 = HALF_OPEN
  2 = OPEN

---

8. Feature extraction contextuelle

Le ContextualFeatureExtractor analyse le prompt de l'utilisateur pour extraire des caractéristiques qui influencent le routage :

FeatureTypeDétectionImpact
task_typestringRegex par domaineSélection du domaine
has_codeboolDétection syntaxe codeBonus modèles code
is_creativeboolDétection contenu créatifBonus modèles créatifs
is_realtimeboolDétection besoin temps réelBonus modèles rapides
length_bucketstringLongueur du promptBonus modèles grand contexte
modalitystringType de contenuFiltrage par modalité
requested_modalitieslistAnalyse multi-modaleFiltrage strict
Ces features sont transmises au DomainRouter et au AutonomousLocalRouter pour affiner la sélection du modèle.

---

9. Pipeline de routage complet

Flux complet d'une requête /v1/invoke

1. Réception de la requête POST /v1/invoke
   │
2. Extraction des features (ContextualFeatureExtractor)
   │
3. Chargement des guardrails (check_input)
   │  ├── Mots-clés interdits ? → Refus
   │  ├── Rate limit dépassé ? → Refus
   │  └── OK → Continue
   │
4. Mode local-first activé ?
   │  ├── OUI → AutonomousLocalRouter.answer(prompt, features)
   │  │         ├── Confiance >= 0.75 ? → Retourne réponse locale
   │  │         └── Confiance < 0.75 → Continue vers cloud
   │  └── NON → Continue vers cloud
   │
5. Domain routing
   │  ├── Domaine détecté + spécialistes ? → Filtre providers
   │  └── Pas de domaine ou pas de spécialiste → Tous les providers
   │
6. Circuit breaker filtering
   │  └── Exclut les providers en état OPEN
   │
7. Bandit router selection
   │  ├── UCB1 : score = mean_reward + exploration + cost + latency
   │  └── Thompson : sample ~ Beta(alpha, beta)
   │
8. Appel au provider sélectionné
   │
9. Post-traitement
   │  ├── Scoring : calcul du reward
   │  ├── Bandit update : mise à jour alpha/beta/pulls/reward
   │  ├── Circuit breaker : record_success() ou record_failure()
   │  ├── Métriques Prometheus : record_request()
   │  ├── RAG : append_to_conversation()
   │  ├── Guardrails : check_output()
   │  └── Billing : record_usage()
   │
10. Retour de la réponse au client

---

10. État persistant Redis

Structure des clés Redis

#### Bandit Router

bandit:{provider_id} → Hash {
  pulls: int,
  total_reward: float,
  alpha: float,
  beta: float,
  avg_latency: float
}
TTL: 86400 (24h)

#### Circuit Breaker

cb:{provider_id} → Hash {
  state: "CLOSED"|"HALF_OPEN"|"OPEN",
  failures: int,
  last_failure_time: float,
  success_count: int
}
TTL: 3600 (1h)

Fallback mémoire

Si Redis est indisponible, chaque replica maintient son propre état en mémoire :

self._pulls: Dict[str, int]       # Compteur par provider
self._rewards: Dict[str, float]   # Récompense totale par provider
self._alpha: Dict[str, float]     # Paramètres Thompson
self._beta: Dict[str, float]
self._avg_latency: Dict[str, float]  # Latence EMA

Conséquence : sans Redis, chaque replica a une vue différente de l'état des providers. Le routage reste fonctionnel mais non coordonné.

---

11. Configuration et tuning

Variables de configuration

VariableDéfautDescriptionImpact
BANDIT_STRATEGYucb1Algorithme de sélectionucb1 ou thompson
BANDIT_EXPLORATION_BONUS1.4142Bonus d'exploration UCB1Plus grand = plus d'exploration

Tuning du bonus d'exploration

ValeurComportement
0.5Très exploiteur — choisit rapidement le meilleur connu
1.0Équilibré — bon compromis exploration/exploitation
1.4142 (défaut)Standard UCB1 — exploration modérée
2.0Très explorateur — teste régulièrement tous les providers
3.0Fortement explorateur — pour phases de découverte

Tuning cost_weight et latency_weight

Ces paramètres sont définis dans le constructeur de BanditRouter :

cost_weight: float = 0.1       # Poids du bonus coût
latency_weight: float = 0.15   # Poids du bonus latence

Pour modifier ces valeurs, il faut actuellement modifier le code. Un futur ajout via variables d'environnement est prévu.

Recommandations de tuning

ObjectifConfiguration
Minimiser les coûtscost_weight=0.3, latency_weight=0.05
Minimiser la latencecost_weight=0.05, latency_weight=0.3
Maximiser la qualitécost_weight=0.0, latency_weight=0.0
Équilibre (défaut)cost_weight=0.1, latency_weight=0.15
---

12. Analyse et interprétation

Consulter les statistiques du bandit

GET /admin/bandit/stats
{
  "openai": {"pulls": 150, "mean_reward": 0.92, "alpha": 140.0, "beta": 12.0},
  "cerebras": {"pulls": 300, "mean_reward": 0.88, "alpha": 265.0, "beta": 36.0},
  "groq": {"pulls": 200, "mean_reward": 0.85, "alpha": 172.0, "beta": 29.0}
}

Interprétation

Métrique de distribution

ai_bandit_choice_total{provider="xxx", strategy="ucb1"}

Permet de visualiser dans Grafana la répartition des choix du bandit et de détecter si un provider est sur-exploité ou sous-exploré.

---

Document AISIA v4.21.0