H-03 Moteur de tarification
Brique H de la plateforme Enterprise AI. Technologie : moteur de tarification multi-fournisseurs développé, sur eai.price_cards (versionnées, dates d'effet) + eai.fx_rates. Version anglaise : ../../en/bricks/H-03-pricing-engine.md.
Rôle et responsabilités
- Tarifer toute unité du contrat Usage Event : tokens (entrée, entrée en cache, écriture/lecture de cache, sortie, raisonnement), images, secondes vidéo, secondes audio, caractères, sessions, appels d'outils, GPU-secondes, stockage.
- Choisir la carte tarifaire en vigueur à la date de l'appel (occurred_at), pas la carte actuelle.
Position et interactions
- Chemin d'exécution : synchrone (estimation, calcul du coût mesuré).
- Appelé par POST /v1/finops/estimate, POST /v1/gateway/estimate et par le registre à l'ingestion.
Part open source et part développée
- Open source réutilisé : La table de coûts LiteLLM comme simple point de départ (importée, puis versionnée et revue).
- Développé dans la plateforme : Moteur complet : prix par paliers, remises batch, prix régionaux, modes de facturation du raisonnement, conversion de devises en arithmétique décimale.
Modèle de données
Tables du Control Plane (schéma eai) : eai.price_cards, eai.fx_rates, eai.allocation_policies, eai.usage_events.
Chaque table de tenant a tenant_id uuid NOT NULL et une politique de sécurité au niveau des lignes sur current_setting('app.tenant_id', true)::uuid. ClickHouse usage_events est la réplique analytique de eai.usage_events (ReplacingMergeTree par event_id).
Contrat : contracts/v1/usage-event.schema.json (schema_version 1.0). L'usage est toujours remonté avec contracts/v1/usage-event.schema.json, l'audit avec contracts/v1/audit-event.schema.json.
Coût estimé, mesuré et facturé
Chaque Usage Event porte trois montants dans cost (contrat Usage Event, cost.estimated, cost.measured, cost.billed, chacun une chaîne décimale avec devise ou null) :
| Montant | Quand | Source | Usage |
|---|---|---|---|
| Coût estimé | Avant exécution | Moteur de tarification (H-03) appliqué aux unités prévues (tokens du prompt comptés, tokens de sortie max, nombre d'images, secondes vidéo) | Contrôle budgétaire, réservation de quota (H-01), seuils d'approbation |
| Coût mesuré | Après exécution | Moteur de tarification appliqué aux unités réellement consommées, telles que remontées par la réponse du fournisseur, le callback LiteLLM, le worker ou l'échantillonneur GPU | Showback, confirmation des quotas, tableaux de bord |
| Coût facturé | Après rapprochement | Factures et exports fournisseurs importés dans /v1/finops/reconciliation/imports | Finance, analyse des écarts, refacturation |
Règles appliquées par le moteur de tarification et le registre :
- Pas de double comptage des tokens de raisonnement.
pricing.reasoning_billingindique comment le fournisseur les facture :included_in_output(les tokens de raisonnement sont déjà inclus dansoutput_tokens, ils sont stockés dansunits.reasoning_tokenspour analyse seulement et ne sont pas tarifés une seconde fois),separate(tarifés avec leur propre prix),not_billed,not_exposed. - Tokens en cache.
cached_input_tokens,cache_read_tokensetcache_write_tokenssont tarifés avec leurs propres prix de la carte tarifaire ; les tokens d'entrée en cache sont retranchés des tokens d'entrée plein tarif lorsque le fournisseur les inclut dansinput_tokens. - Prix applicable à la date de l'appel. La carte tarifaire retenue est celle dont la période de validité contient
occurred_at;pricing.pricing_version,pricing.price_card_idetpricing.effective_atsont figés dans l'événement, un changement de prix ultérieur ne réécrit donc jamais l'historique. - Devise et taux de change. Les montants gardent la devise du fournisseur ; la conversion vers la devise de reporting du tenant utilise
eai.fx_ratesà la date de l'appel (GET/PUT /v1/finops/fx-rates), et le taux utilisé est conservé pour l'audit. - Politique d'allocation des modèles locaux. Les modèles locaux (vLLM, Diffusers, faster-whisper, pyannote, Piper, workers vidéo GPU) n'ont pas de facture : leur coût vient d'une politique d'allocation (
eai.allocation_policies) combinant heures GPU, surcoût d'infrastructure, énergie estimée, stockage et usage réel (GPU-secondes issues des échantillons DCGM, tokens servis), référencée parpricing.allocation_policy_id. - Champs de l'événement. Chaque événement enregistre le tarif applicable à la date de l'appel, la devise, le fournisseur, le modèle, la région, le tenant, l'agent, la session et la tâche (
pricing,provider,model_id,region,tenant_id,agent_id,session_id,task_id), plus les identifiants de projet, environnement, run, requête, requête fournisseur et trace pour la corrélation. - LiteLLM est une source d'usage, pas la seule source comptable. Les callbacks LiteLLM couvrent les appels LLM, embeddings et certains médias via les fournisseurs. L'inférence GPU locale, les workers média, les appels d'outils, les connecteurs, les exécutions sandbox et les opérations asynchrones (jobs batch, génération vidéo) émettent leurs propres Usage Events directement vers
/v1/finops/usageou/v1/finops/usage/batch.
Endpoints d'API
| Méthode | Chemin |
|---|---|
| POST | /v1/finops/price-cards |
| GET | /v1/finops/price-cards |
| POST | /v1/finops/price-cards/{id}/retire |
| GET | /v1/finops/fx-rates |
| PUT | /v1/finops/fx-rates |
| POST | /v1/finops/estimate |
| POST | /v1/gateway/estimate |
POST /v1/finops/price-cards
Requête :
{
"provider": "openai",
"model_pattern": "gpt-4o-mini",
"region": "global",
"usage_type": "llm",
"currency": "USD",
"effective_from": "2026-10-01T00:00:00Z",
"reasoning_billing": "included_in_output",
"prices": {
"input_tokens": "0.15",
"cached_input_tokens": "0.075",
"output_tokens": "0.60"
}
}
Réponse :
{
"id": "pc_31",
"pricing_version": "openai-2026-10-01",
"status": "active"
}
GET /v1/finops/price-cards
Réponse :
{
"items": [
{
"id": "pc_31",
"provider": "openai",
"model_id": "gpt-4o-mini",
"effective_at": "2026-10-01T00:00:00Z"
}
],
"next_cursor": null
}
POST /v1/finops/price-cards/{id}/retire
Requête :
{
"retired_at": "2026-12-01T00:00:00Z"
}
Réponse :
{
"id": "pc_31",
"status": "retired"
}
GET /v1/finops/fx-rates
Réponse :
{
"base": "EUR",
"rates": [
{
"currency": "USD",
"rate": "1.0832",
"as_of": "2026-10-29"
}
]
}
PUT /v1/finops/fx-rates
Requête :
{
"rates": [
{
"base": "EUR",
"quote": "USD",
"rate": "1.0832",
"as_of": "2026-10-29T00:00:00Z",
"source": "ecb"
}
]
}
Réponse :
{
"updated": 1
}
POST /v1/finops/estimate
Requête :
{
"usage_type": "llm",
"provider": "openai",
"model_id": "gpt-4o-mini",
"units": {
"input_tokens": 12000,
"output_tokens": 2000
}
}
Réponse :
{
"estimated": {
"amount": "0.0028",
"currency": "EUR"
},
"pricing_version": "openai-2026-10-01",
"fx_rate": "1.0832",
"assumptions": [
"reasoning tokens counted inside output_tokens"
]
}
POST /v1/gateway/estimate
Requête :
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "..."
}
],
"max_tokens": 2000
}
Réponse :
{
"estimated": {
"amount": "0.0028",
"currency": "EUR"
},
"input_tokens": 12000
}
Exemples SDK
Python
from enterprise_ai import EnterpriseAI
client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_live_xxx", tenant="acme", locale="fr")
with client.quotas.reservation(cost="1.80", scope={"agent": "agt_ap"}) as r:
result = client.gateway.video(model="video-gen-gpu", prompt="Teaser", seconds=15)
r.commit(cost="1.62") # released automatically on exception
estimate = client.finops.estimate("openai", "gpt-4o-mini", input_tokens=12000, output_tokens=2000)
client.finops.create_price_card({"provider": "openai", "model_pattern": "gpt-4o-mini", "currency": "USD", "effective_from": "2026-10-01T00:00:00Z"})
client.finops.list_price_cards(provider="openai")
client.finops.retire_price_card("pc_31")
client.finops.fx_rates()
client.finops.set_fx_rates({"USD": "0.92", "GBP": "1.17"})
client.finops.estimate("openai", "gpt-4o-mini", input_tokens=12000, output_tokens=2000)
client.gateway.estimate(model="gpt-4o-mini", max_tokens=2000)
TypeScript
import { EnterpriseAI } from "@enterprise-ai/sdk";
const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "fr" });
await client.finops.priceCards.create({ provider: "openai", model_pattern: "gpt-4o-mini", currency: "USD", effective_from: "2026-10-01T00:00:00Z" });
await client.finops.priceCards.list({ provider: "openai" });
await client.finops.priceCards.retire("pc_31");
await client.finops.fxRates.list();
await client.finops.fxRates.put({ rates: { USD: "1.08" } });
await client.finops.estimate({ provider: "openai", usage_type: "llm", model_id: "gpt-4o-mini", units: { input_tokens: 12000, output_tokens: 2000 } });
await client.gateway.estimate({ model: "gpt-4o-mini", maxTokens: 2000 });
Java
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.locale(Locale.FRENCH)
.build();
client.finops().createPriceCard(Map.of("provider", "openai", "model_pattern", "gpt-4o-mini", "currency", "USD", "effective_from", "2026-10-01T00:00:00Z"));
client.finops().listPriceCards(Map.of("provider", "openai"));
client.finops().retirePriceCard("pc_31");
client.finops().fxRates();
client.finops().setFxRates(Map.of("rates", List.of(Map.of("base", "EUR", "quote", "USD", "rate", 1.08, "as_of", "2026-10-01T00:00:00Z"))));
client.finops().estimate(Map.of("provider", "openai", "usage_type", "llm", "model_id", "gpt-4o-mini", "units", Map.of("input_tokens", 12000, "output_tokens", 2000)));
client.gateway().estimate(Map.of("model", "gpt-4o-mini", "max_tokens", 2000));
Go
client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")), eai.WithTenant("acme"), eai.WithLocale("fr"))
if err != nil { log.Fatal(err) }
ctx := context.Background()
_, err = client.FinOps.CreatePriceCard(ctx, map[string]any{"provider": "openai", "model_pattern": "gpt-4o-mini", "usage_type": "llm", "currency": "USD", "effective_from": "2026-10-01T00:00:00Z", "prices": map[string]any{"input_per_mtok": "0.15", "output_per_mtok": "0.60"}})
_, err = client.FinOps.ListPriceCards(ctx, eai.WithQuery(map[string]string{"provider": "openai"}))
_, err = client.FinOps.RetirePriceCard(ctx, map[string]any{"price_card_id": "pc_31"})
_, err = client.FinOps.FxRates(ctx, nil)
_, err = client.FinOps.SetFxRates(ctx, map[string]any{"rates": map[string]any{"USD": "0.92"}})
_, err = client.FinOps.Estimate(ctx, map[string]any{"usage_type": "llm", "provider": "openai", "model_id": "gpt-4o-mini", "units": map[string]any{"input_tokens": 12000, "output_tokens": 2000}})
_, err = client.Gateway.Estimate(ctx, map[string]any{"model": "gpt-4o-mini", "max_tokens": 2000})
// usage package: emit a Usage Event from a local worker
emitter := usage.NewEmitter(client, usage.Options{})
defer emitter.Close(ctx)
ev, err := usage.NewEvent("acme", "gpu_local", "worker").Model("local", "llama-70b-local", "", "").Unit("gpu_seconds", 12.4).Unit("energy_wh", 1.7).Build()
if err == nil { err = emitter.Emit(ev) }
// gatewaymw: reserve, call, commit around any http.Handler
http.Handle("/llm", gatewaymw.New(gatewaymw.Config{Client: client, Action: func(r *http.Request) string { return "llm.invoke" }, Reserve: func(r *http.Request) map[string]any { return map[string]any{"cost": "0.50", "tokens": 4000} }})(llmHandler))
Configuration
Variables d'environnement : EAI_DATABASE_URL.
Clés de valeurs Helm (deploy/helm/enterprise-ai/values.yaml) : controlPlane.config, controlPlane.secrets.
Sécurité
- Les modifications de cartes tarifaires exigent le rôle finops et sont auditées ; les cartes retirées restent pour les calculs historiques.
- Tenant déduit de l'identifiant d'accès, jamais du corps ; sécurité au niveau des lignes PostgreSQL sur chaque table de tenant.
- Chaque mutation et décision de politique écrit un événement d'audit chaîné par hachage (G-11).
- Les erreurs utilisent l'enveloppe standard avec message_i18n en et fr.
Observabilité
- Les événements tarifés sans carte (pricing_version=unknown) déclenchent une alerte.
- traceparent W3C propagé, X-Request-Id renvoyé, les spans portent eai.tenant_id, eai.project, eai.agent_id, eai.run_id.
Unités FinOps
- Les montants sont des chaînes décimales avec devise ; aucun flottant.
Modes de défaillance et résilience
- Aucune carte pour la date -> coût mesuré nul, événement signalé, jamais de prix deviné.
Adoption autonome
Utilisable seul comme API de tarification pour la refacturation interne.
Tests d'acceptation
- Un événement daté d'avant un changement de prix utilise l'ancienne carte.
- reasoning_billing=included_in_output n'ajoute pas une seconde fois les tokens de raisonnement.