B06 - Résolveur de capacités
| Groupe | Technologie | Statut | Service | Phase de la feuille de route | Contrats |
|---|---|---|---|---|---|
| B - Gestion des LLM et Model Gateway | Développé | à développer | llm | 2 | model-manifest |
Rôle et responsabilités
Normalise les paramètres d'appel en paramètres natifs du provider ou rejette explicitement.
Position dans l'architecture et interactions
Appelé par la gateway avant le routage et exposé via POST /v1/models/resolve pour les SDK qui veulent valider les paramètres à l'avance.
Composant open source et partie développée
Paramètres contrôlés par le résolveur au regard des capacités du Model Registry : temperature, top_p (certains modèles de raisonnement les refusent : rejet, pas de suppression silencieuse), max_tokens (plafonné par max_output_tokens et la politique), response_format (json_mode / json_schema), tools et tool_choice (capacité tools requise), stream (supports_streaming requis), directives de cache (supports_caching requis), paramètres multimodaux (entrées image, audio, vidéo vérifiées contre modalities.input).
| Effort | Intention | OpenAI (reasoning_effort) | Anthropic / Bedrock Claude (thinking.budget_tokens) | Gemini (thinking_budget) | DeepSeek reasoner |
|---|---|---|---|---|---|
none | Pas de raisonnement | omis (modèle sans raisonnement) ou minimal si le profil l'autorise | thinking désactivé | 0 (si le modèle l'autorise) | rejet : raisonnement toujours actif |
low | Raisonnement court | low | 1024 à 4096 | 1024 | profil compatible always_on ou rejet |
medium | Par défaut | medium | 8192 | 8192 | profil compatible always_on |
high | Tâches difficiles | high | 16384 à 32000 | 24576 | profil compatible always_on |
max | Maximum autorisé par la politique | high (niveau natif le plus haut) | min(limite du modèle, plafond de politique) | maximum du modèle | profil compatible always_on |
Règles : le résolveur traduit vers le paramètre natif, applique un profil compatible configuré explicitement, ou rejette avec effort_not_supported. Il n'y a jamais de conversion silencieuse : la réponse contient la correspondance appliquée et le X-EAI-Decision-Id. Les politiques peuvent plafonner l'effort par tenant, projet ou finalité.
Modèle de données
Tables (schéma eai, chaque table tenant porte tenant_id uuid NOT NULL avec une politique RLS sur current_setting('app.tenant_id', true)::uuid) : eai.llm_models, eai.audit_events.
Endpoints API
POST /v1/models/resolve
POST /v1/models/resolve HTTP/1.1
Host: <control-plane-host>
X-API-Key: eai_xxx_secret
Accept-Language: fr
Content-Type: application/json
{"model_id": "claude-sonnet-eu", "effort": "high", "max_tokens": 4096, "temperature": 0.2, "stream": true}
{"allowed": true, "native_params": {"thinking": {"type": "enabled", "budget_tokens": 16000}, "max_tokens": 4096}, "dropped": [], "decision_id": "dec_01H..."}
Les erreurs utilisent l'enveloppe commune {"error": {"code", "message", "message_i18n", "details", "request_id"}} ; les listes sont paginées avec limit et cursor.
Utilisation des SDK
Python (enterprise_ai):
from enterprise_ai import EnterpriseAI
client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_xxx_secret")
items = client.models.list()
TypeScript (@enterprise-ai/sdk):
import { EnterpriseAI } from "@enterprise-ai/sdk";
const eai = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY });
const items = await eai.models.list();
Java (enterprise-ai-java):
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
var items = client.models().list();
Go (enterprise-ai-go):
client, _ := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")))
items, err := client.Models.List(ctx)
Configuration
- Environnement :
EAI_DATABASE_URL,EAI_OTEL_EXPORTER_OTLP_ENDPOINT. - Valeurs Helm :
controlPlane.enabled,controlPlane.replicas,controlPlane.resources,controlPlane.config,litellm.enabled,litellm.config.
Contrôles de sécurité
- Authentification par JWT OIDC, clé API tenant avec scopes, ou mTLS SPIFFE ; le tenant est résolu depuis l'identifiant, jamais depuis le corps.
- Autorisation par décisions OpenFGA et OPA ; chaque mutation et décision écrit une ligne
eai.audit_eventschaînée par hash. - Les secrets ne sont jamais renvoyés par l'API ; TLS partout ; les politiques réseau Cilium restreignent qui peut joindre le service.
Observabilité
- Métriques :
eai_model_capability_resolver_requests_total,eai_model_capability_resolver_errors_total,eai_model_capability_resolver_latency_seconds(Prometheus). - Traces : spans OpenTelemetry
eai.models.*avec propagation W3Ctraceparent, spans IA envoyés à Opik. - Actions d'audit :
model.create,model.update,model.delete,model.status,policy.evaluate.
Aspects FinOps
Le coût d'exploitation est imputé par tenant et projet via les labels des Usage Events et les requêtes de ressources Kubernetes ; pas de coût direct par appel.
Modes de défaillance et résilience
- Réplicas sans état derrière l'API Gateway, PostgreSQL HA pour l'état ; la sonde
GET /readyzretire les pods défaillants. - Système externe indisponible : échec fermé pour les décisions de sécurité, code d'erreur explicite, aucun contournement silencieux.
Adoption autonome (client d'un client)
Un client d'un client peut adopter cette brique seule avec le sous-client SDK Python client.models (TS eai.models, Java client.models(), Go client.Models). Déploiement minimal : Control Plane API + PostgreSQL + Keycloak (ou clés API), le Control Plane charge tous les domaines de services/control-plane/src/eai/domains ; les composants optionnels du chart se coupent dans deploy/helm/enterprise-ai/values.yaml (vllm.enabled, qdrant.enabled, nats.enabled, workers.enabled).
Tests d'acceptation
- Un tenant ne peut ni lire ni modifier les ressources d'un autre tenant (test RLS).
- Chaque mutation produit un événement d'audit vérifiable par
GET /v1/audit/verify. - Les erreurs sont renvoyées en anglais et en français selon
Accept-Language.
Matrice des modalités (LLM_-005)
Voir la matrice générée : une capacité sans test d'adaptateur y est « déclaré, non vérifié », jamais « vérifié ». L'effort de raisonnement n'est jamais converti silencieusement (422 effort_not_supported, LLM_-006).