Skip to main content

H-02 Cost ledger

Brick H of the Enterprise AI Platform. Technology: PostgreSQL (eai.usage_events, source of truth, RLS) + ClickHouse usage_events (analytics). French version: ../../fr/bricks/H-02-cost-ledger.md.

Role and responsibilities​

  • Store every Usage Event once (idempotent on event_id) with estimated, measured and billed cost.
  • Reconcile measured costs with provider invoices and exports (OpenAI, Anthropic, AWS CUR, Azure, GCP, generic CSV).
  • Aggregate by tenant, project, agent, model, provider, usage type, day.

Position and interactions​

  • Execution path: event ingestion (sync API and batch), reconciliation workflow.
  • Sources: LiteLLM callbacks, local GPU accounting (H-04), media workers, Tool Gateway, connectors, sandbox, async operations.
  • Reconciliation matches provider_request_id first, then provider + model + day buckets.

Open source and developed parts​

  • Open source reused: PostgreSQL, ClickHouse.
  • Developed in the platform: Ledger service, invoice parsers, matching and variance report.

Data model​

Control Plane tables (schema eai): eai.usage_events, ClickHouse usage_events, eai.recon_imports, eai.fx_rates, eai.price_cards.

Every tenant table has tenant_id uuid NOT NULL and a row level security policy on current_setting('app.tenant_id', true)::uuid. ClickHouse usage_events is the analytics replica of eai.usage_events (ReplacingMergeTree keyed by event_id).

Contract: contracts/v1/usage-event.schema.json (schema_version 1.0). Usage is always reported with contracts/v1/usage-event.schema.json, audit with contracts/v1/audit-event.schema.json.

Estimated, measured and billed cost​

Every Usage Event carries three amounts in cost (Usage Event contract, cost.estimated, cost.measured, cost.billed, each a decimal string with a currency or null):

AmountWhenSourceUsed for
Estimated costBefore executionPricing engine (H-03) applied to predicted units (prompt tokens counted, max output tokens, image count, video seconds)Budget control, quota reservation (H-01), approval thresholds
Measured costAfter executionPricing engine applied to units really consumed, as reported by the provider response, LiteLLM callback, worker or GPU samplerShowback, quotas commit, dashboards
Billed costAfter reconciliationProvider invoices and exports imported in /v1/finops/reconciliation/importsFinance, variance analysis, chargeback

Rules applied by the pricing engine and the ledger:

  • No double counting of reasoning tokens. pricing.reasoning_billing records how the provider bills them: included_in_output (reasoning tokens are already inside output_tokens, they are stored in units.reasoning_tokens for analysis only and are not priced again), separate (priced with their own rate), not_billed, not_exposed.
  • Cached tokens. cached_input_tokens, cache_read_tokens and cache_write_tokens are priced with their own rates of the price card; cached input tokens are subtracted from full price input tokens when the provider reports them inside input_tokens.
  • Price applicable at call date. The price card is the one whose effective period contains occurred_at; pricing.pricing_version, pricing.price_card_id and pricing.effective_at are frozen in the event, so a later price change never rewrites history.
  • Currency and FX. Amounts keep the provider currency; conversion to the tenant reporting currency uses eai.fx_rates of the call date (GET/PUT /v1/finops/fx-rates), and the rate used is stored for audit.
  • Local model allocation policy. Local models (vLLM, Diffusers, faster-whisper, pyannote, Piper, video GPU workers) have no invoice: their cost comes from an allocation policy (eai.allocation_policies) combining GPU hours, infrastructure overhead, estimated energy, storage and real usage (GPU-seconds from DCGM samples, tokens served), referenced by pricing.allocation_policy_id.
  • Event fields. Each event records the tariff applicable at call date, currency, provider, model, region, tenant, agent, session and task (pricing, provider, model_id, region, tenant_id, agent_id, session_id, task_id), plus project, environment, run, request, provider request and trace ids for correlation.
  • LiteLLM is a usage source, not the only accounting source. LiteLLM callbacks cover LLM, embeddings and some media calls through providers. Local GPU inference, media workers, tool calls, connectors, sandbox executions and asynchronous operations (batch jobs, video generation) emit their own Usage Events directly to /v1/finops/usage or /v1/finops/usage/batch.

API endpoints​

MethodPath
POST/v1/finops/usage
POST/v1/finops/usage/batch
GET/v1/finops/usage
GET/v1/finops/summary
POST/v1/finops/reconciliation/imports
POST/v1/finops/reconciliation/run
GET/v1/finops/reconciliation/report

POST /v1/finops/usage​

Request:

{
"schema_version": "1.0",
"event_id": "use_01J",
"occurred_at": "2026-10-29T10:00:01Z",
"tenant_id": "8f0c2a4e-1b7d-4c55-9a51-2f3e6d7c9b10",
"agent_id": "agt_ap",
"session_id": "ses_9",
"task_id": "tsk_3",
"usage_type": "llm",
"provider": "openai",
"model_id": "gpt-4o-mini",
"region": "eu-west",
"units": {
"input_tokens": 812,
"cached_input_tokens": 512,
"output_tokens": 240,
"reasoning_tokens": 120
},
"pricing": {
"pricing_version": "openai-2026-10-01",
"currency": "USD",
"effective_at": "2026-10-01T00:00:00Z",
"reasoning_billing": "included_in_output"
},
"cost": {
"estimated": {
"amount": "0.000310",
"currency": "USD"
},
"measured": null,
"billed": null
},
"source": "litellm"
}

Response:

{
"event_id": "use_01J",
"cost": {
"estimated": {
"amount": "0.000310",
"currency": "USD"
},
"measured": {
"amount": "0.000228",
"currency": "USD"
},
"billed": null
},
"duplicate": false
}

POST /v1/finops/usage/batch​

Request:

{
"events": [
"<Usage Event>",
"<Usage Event>"
]
}

Response:

{
"accepted": 2,
"duplicates": 0,
"rejected": []
}

GET /v1/finops/usage​

Response:

{
"items": [
{
"event_id": "use_01J",
"usage_type": "llm",
"cost": {
"measured": {
"amount": "0.000228",
"currency": "USD"
}
}
}
],
"next_cursor": null
}

GET /v1/finops/summary​

Response:

{
"group_by": "agent",
"currency": "EUR",
"items": [
{
"key": "agt_ap",
"estimated": "41.20",
"measured": "38.75",
"billed": "39.02"
}
]
}

POST /v1/finops/reconciliation/imports​

Request:

{
"source": "openai",
"filename": "openai-costs-2026-09.csv",
"content": "date,model,cost_usd\n2026-09-01,gpt-4o-mini,12.40\n"
}

Response:

{
"id": "rim_2",
"status": "parsed",
"lines": 1840,
"total": {
"amount": "1932.11",
"currency": "USD"
}
}

POST /v1/finops/reconciliation/run​

Request:

{
"from": "2026-09-01",
"to": "2026-09-30"
}

Response:

{
"status": "completed",
"matched_events": 182033,
"unmatched_lines": 4
}

GET /v1/finops/reconciliation/report​

Response:

{
"period": "2026-09",
"currency": "EUR",
"estimated": "1820.40",
"measured": "1771.02",
"billed": "1783.55",
"variance_measured_vs_billed": "0.71%",
"alerts": []
}

SDK examples​

Python​

from enterprise_ai import EnterpriseAI

client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_live_xxx", tenant="acme", locale="en")

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.ingest_usage({"usage_type": "llm", "provider": "openai", "model_id": "gpt-4o-mini", "units": {"input_tokens": 812, "output_tokens": 240}})
client.finops.ingest_usage_batch(events=events)
client.finops.usage(agent_id="agt_ap", limit=100)
client.finops.summary(group_by="agent")
client.finops.import_invoice("openai", open("openai-2026-09.csv").read(), filename="openai-2026-09.csv")
client.finops.reconcile(start="2026-09-01", end="2026-09-30")
client.finops.reconciliation_report(start="2026-09-01", end="2026-09-30")

TypeScript​

import { EnterpriseAI } from "@enterprise-ai/sdk";

const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "en" });
await client.finops.usage.ingest({ usage_type: "llm", provider: "openai", model_id: "gpt-4o-mini", units: { input_tokens: 812, output_tokens: 240 } });
await client.finops.usage.ingestBatch(events);
await client.finops.usage.list({ agent_id: "agt_ap", limit: 100 });
await client.finops.summary({ group_by: "agent" });
await client.finops.reconciliation.createImport({ source: "openai", filename: "openai-2026-09.csv", content: csvText });
await client.finops.reconciliation.run({ from: "2026-09-01", to: "2026-09-30" });
await client.finops.reconciliation.report({ from: "2026-09-01", to: "2026-09-30" });

Java​

EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.locale(Locale.ENGLISH)
.build();
client.finops().ingestUsage(Map.of("usage_type", "llm", "provider", "openai", "model_id", "gpt-4o-mini", "units", Map.of("input_tokens", 812, "output_tokens", 240)));
client.finops().ingestUsageBatch(Map.of("events", events));
client.finops().listUsage(Map.of("agent_id", "agt_ap", "limit", 100));
client.finops().summary(Map.of("group_by", "agent"));
client.finops().importReconciliation(Map.of("source", "openai", "filename", "openai-2026-09.csv", "content", "date,model,cost\n2026-09-01,gpt-4o-mini,12.40\n"));
client.finops().runReconciliation(Map.of("from", "2026-09-01", "to", "2026-09-30"));
client.finops().reconciliationReport(Map.of("from", "2026-09-01", "to", "2026-09-30"));

Go​

client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")), eai.WithTenant("acme"), eai.WithLocale("en"))
if err != nil { log.Fatal(err) }
ctx := context.Background()
_, err = client.FinOps.IngestUsage(ctx, map[string]any{"usage_type": "llm", "provider": "openai", "model_id": "gpt-4o-mini", "units": map[string]any{"input_tokens": 812, "output_tokens": 240}})
_, err = client.FinOps.IngestUsageBatch(ctx, map[string]any{"events": events})
_, err = client.FinOps.ListUsage(ctx, eai.WithQuery(map[string]string{"agent_id": "agt_ap"}), eai.WithLimit(100))
_, err = client.FinOps.Summary(ctx, eai.WithQuery(map[string]string{"group_by": "agent"}))
_, err = client.FinOps.ImportReconciliation(ctx, map[string]any{"provider": "openai", "filename": "openai-2026-09.csv", "content": "date,model,cost\n2026-09-30,gpt-4o-mini,12.40\n"})
_, err = client.FinOps.RunReconciliation(ctx, map[string]any{"from": "2026-09-01", "to": "2026-09-30"})
_, err = client.FinOps.ReconciliationReport(ctx, eai.WithQuery(map[string]string{"from": "2026-09-01", "to": "2026-09-30"}))

// 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​

Environment variables: EAI_DATABASE_URL, EAI_CLICKHOUSE_URL.

Helm values keys (deploy/helm/enterprise-ai/values.yaml): controlPlane.config.EAI_CLICKHOUSE_URL, controlPlane.migration.

Security​

  • Usage events are append only; corrections are new events with labels.correction_of.
  • finops:read scope for readers, finops role for imports.
  • Tenant resolved from the credential, never from the body; PostgreSQL row level security on every tenant table.
  • Every mutation and policy decision writes a hash chained audit event (G-11).
  • Errors use the standard envelope with message_i18n en and fr.

Observability​

  • Ingestion rate, duplicates, unmatched invoice lines, variance per provider.
  • W3C traceparent propagated, X-Request-Id returned, spans carry eai.tenant_id, eai.project, eai.agent_id, eai.run_id.

FinOps units​

  • The ledger is the accounting source; Opik traces and LiteLLM spend logs are inputs, not the ledger.

Failure modes and resilience​

  • Duplicate event_id -> 200 with duplicate=true, no double counting.
  • ClickHouse lag -> PostgreSQL remains authoritative.

Standalone adoption​

A client can push its own Usage Events (other AI tools) to get one consolidated AI cost view.

Acceptance tests​

  1. Re-posting an event with the same Idempotency-Key returns the same result.
  2. An OpenAI export for a month reconciles with variance below the tenant variance_threshold_pct threshold (eai.finops_settings).

Implementation roadmap