Aller au contenu principal

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) :

MontantQuandSourceUsage
Coût estiméAvant exécutionMoteur 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écutionMoteur 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 GPUShowback, confirmation des quotas, tableaux de bord
Coût facturéAprès rapprochementFactures et exports fournisseurs importés dans /v1/finops/reconciliation/importsFinance, 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_billing indique comment le fournisseur les facture : included_in_output (les tokens de raisonnement sont déjà inclus dans output_tokens, ils sont stockés dans units.reasoning_tokens pour 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_tokens et cache_write_tokens sont 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 dans input_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_id et pricing.effective_at sont 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 par pricing.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/usage ou /v1/finops/usage/batch.

Endpoints d'API​

MéthodeChemin
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​

  1. Un événement daté d'avant un changement de prix utilise l'ancienne carte.
  2. reasoning_billing=included_in_output n'ajoute pas une seconde fois les tokens de raisonnement.

Feuille de route d'implémentation