Costs without a unit of account
Opik already collects LLM monetary costs, in two shapes:
-
A pre-computed total from gen_ai.usage.cost, routed to the span's
totalEstimatedCost:
// apps/opik-backend/.../domain/mapping/otel/GenAIMappingRules.java
.rule("gen_ai.usage.cost").source(SOURCE)
.outcome(OpenTelemetryMappingRule.Outcome.COST).spanType(SpanType.llm).build(),
guarded by genAiUsageCostExtractedIntoTotalEstimatedCost in
OpenTelemetryMapperTest.java (the fix for the earlier bug where a double cost
was "routed there and silently dropped").
-
gen_ai.cost.* attributes (total_cost / input_cost / output_cost) captured
into span metadata (OpenTelemetryMapperTest.java).
In both paths the value is a bare number. Nothing records what currency it is in. For
anyone who self-hosts Opik, ingests traces billed in a non-USD currency, or compares
cost across deployments, the number is uninterpretable and cross-deployment cost
reconciliation is impossible.
The standard direction
The OpenTelemetry GenAI semantic-conventions working group is standardizing this
exactly (there is no OTel convention for GenAI cost today; each vendor rolls its own).
The open proposal
add per-operation cost conventions (gen_ai.usage.cost.*) #443
defines:
gen_ai.usage.cost.amount (double) — the monetary cost at recording time
gen_ai.usage.cost.currency (string) — the ISO 4217 code, conditionally required when amount is set
gen_ai.usage.cost.source (enum provider | local) — provenance
So the attribute a trace should carry beside gen_ai.usage.cost is
gen_ai.usage.cost.currency, and the OTel GenAI mapper is the natural place to map it.
Suggested scope
Consider ingesting gen_ai.usage.cost.currency alongside the existing
gen_ai.usage.cost handling in GenAIMappingRules.java (map it to COST/METADATA so
the unit rides with the amount), and keeping the value additive — no change to the
totalEstimatedCost number or to gen_ai.cost.* behavior. A test alongside
genAiUsageCostExtractedIntoTotalEstimatedCost asserting that a span with
gen_ai.usage.cost + gen_ai.usage.cost.currency retains both the amount and its unit
would lock it in.
Why it helps
- Cost becomes interpretable: the number names its unit of account (USD today, any ISO
4217 code later) without changing the amount logic.
- Brings the backend's handling in line with the emerging OTel GenAI standard, where
currency is required whenever an amount is set.
- Matters to any per-trace cost-accounting or billing workflow that consumes Opik
spans.
Happy to open a PR on the backend mapper with tests if this scope looks right.
Costs without a unit of account
Opik already collects LLM monetary costs, in two shapes:
A pre-computed total from
gen_ai.usage.cost, routed to the span'stotalEstimatedCost:guarded by
genAiUsageCostExtractedIntoTotalEstimatedCostinOpenTelemetryMapperTest.java(the fix for the earlier bug where a double costwas "routed there and silently dropped").
gen_ai.cost.*attributes (total_cost/input_cost/output_cost) capturedinto span metadata (
OpenTelemetryMapperTest.java).In both paths the value is a bare number. Nothing records what currency it is in. For
anyone who self-hosts Opik, ingests traces billed in a non-USD currency, or compares
cost across deployments, the number is uninterpretable and cross-deployment cost
reconciliation is impossible.
The standard direction
The OpenTelemetry GenAI semantic-conventions working group is standardizing this
exactly (there is no OTel convention for GenAI cost today; each vendor rolls its own).
The open proposal
add per-operation cost conventions (gen_ai.usage.cost.*) #443
defines:
gen_ai.usage.cost.amount(double) — the monetary cost at recording timegen_ai.usage.cost.currency(string) — the ISO 4217 code, conditionally required when amount is setgen_ai.usage.cost.source(enumprovider | local) — provenanceSo the attribute a trace should carry beside
gen_ai.usage.costisgen_ai.usage.cost.currency, and the OTel GenAI mapper is the natural place to map it.Suggested scope
Consider ingesting
gen_ai.usage.cost.currencyalongside the existinggen_ai.usage.costhandling inGenAIMappingRules.java(map it to COST/METADATA sothe unit rides with the amount), and keeping the value additive — no change to the
totalEstimatedCostnumber or togen_ai.cost.*behavior. A test alongsidegenAiUsageCostExtractedIntoTotalEstimatedCostasserting that a span withgen_ai.usage.cost+gen_ai.usage.cost.currencyretains both the amount and its unitwould lock it in.
Why it helps
4217 code later) without changing the amount logic.
currency is required whenever an amount is set.
spans.
Happy to open a PR on the backend mapper with tests if this scope looks right.