@reaatech/otel-cost-exporter
Status: Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.
OpenTelemetry-native LLM cost metrics exporter. Converts GenAI semantic convention spans into real-time USD cost metrics and exports them via Prometheus, OTLP, or JSON. Ships with bundled pricing tables for every major LLM provider — zero maintenance required.
Installation
npm install @reaatech/otel-cost-exporter
# or
pnpm add @reaatech/otel-cost-exporter
Feature Overview
OTel-native — reads GenAI semantic convention spans, emits standard Counter metrics with model and provider labels
Zero-pricing-maintenance — bundled tables for OpenAI, Anthropic, Google, AWS Bedrock, and Azure updated on patch releases
Two deployment modes — in-process SpanProcessor for the Node.js SDK, or standalone collector service via OTLP
Three export formats — Prometheus (pull), OTLP (push), JSON (stdout/debug)
Configurable fallback pricing — default price for unknown models prevents gaps in cost tracking
Custom overrides — merge custom YAML pricing tables to override or extend any provider
Granular labels — model, provider, plus any custom labels from configuration
Prompt caching support — separate cost tracking for Anthropic cache read and cache creation tokens
Config hot-reload — file watcher with debounced reload for zero-downtime configuration changes
Privacy-first — processes only metadata; never inspects or logs LLM content, prompts, or responses
Dual ESM/CJS output — works with import and require
Quick Start
In-Process Span Processor
import { NodeSDK } from "@opentelemetry/sdk-node" ;
import { metrics } from "@opentelemetry/api" ;
import {
loadConfig,
createProcessorFactory,
createCostSpanProcessor,
createMetricsBuilder,
} from "@reaatech/otel-cost-exporter" ;
const config = await loadConfig ();
// Build the cost processor pipeline
const factory = createProcessorFactory (config);
const costProcessor = await factory. createProcessor ();
// Create metrics
const meter = metrics. getMeter ( "otel-cost-exporter" );
const metricsBuilder = createMetricsBuilder (meter, config.metrics.prefix);
// Wire into the OTel SDK
const costSpanProcessor = createCostSpanProcessor ({
costProcessor,
metricsBuilder,
});
const sdk = new NodeSDK ({
spanProcessors: [costSpanProcessor],
});
await sdk. start ();
Collector Service
import { loadConfig, createCollectorService } from "@reaatech/otel-cost-exporter" ;
const config = await loadConfig ();
const service = await createCollectorService (config);
await service. start (); // OTLP receiver on :4317, Prometheus on :8888
process. on ( "SIGTERM" , () => service. shutdown ());
API Reference
Span Processing
Export Description createSpanProcessor(deps)Creates a span processor that calculates costs for each span createBatchProcessor(inner, options?)Wraps a processor with batch buffering and timeout-based flushing createProcessorFactory(config)Factory that wires pricing tables, normalization, caching, and the cost calculator
SpanProcessor
Method Description processSpan(span: CostSpan)Synchronously process a single span — returns ProcessResult processSpans(spans: CostSpan[])Process multiple spans in parallel — returns ProcessResult[] shutdown()Gracefully flush any buffered spans
BatchProcessorOptions
Property Type Default Description maxBatchSizenumber100Flush after accumulating this many spans batchTimeoutMsnumber5000Flush after this many ms of inactivity logger{ warn(...) }No-op Optional logger for flush errors
OTel SDK Integration
Export Description createCostSpanProcessor(options)OTel SpanProcessor adapter — extracts GenAI attributes on span end spanToCostSpan(span)Converts an OTel ReadableSpan to a CostSpan for cost calculation
CostSpanProcessorOptions
Property Type Description costProcessorSpanProcessorThe cost processor (from createSpanProcessor or createBatchProcessor) metricsBuilder?MetricsBuilderRecords costs to OTel counters logger?objectOptional Pino-compatible logger onSpanRecorded?(span: CostSpan) => voidCallback fired after each span is processed
Metrics
Export Description createMetricsBuilder(meter, prefix)Creates a metrics recorder that emits OTel counter metrics METRIC_INPUT_COSTllm.cost.input_tokens_usdMETRIC_OUTPUT_COSTllm.cost.output_tokens_usdMETRIC_TOTAL_COSTllm.cost.total_usd
MetricsBuilder
Method Description recordCost(result, extraLabels?)Record a CostResult to all three counters with labels
Export Formats
Export Description createPrometheusExporter(options?)Pull-based Prometheus exporter on configurable port createOtlpExporter(options?)Push-based OTLP HTTP exporter createJsonExporter(options?)Stdout JSON exporter for debugging
Configuration
Export Description loadConfig(path?)Load and merge config from YAML file and environment variables createConfigService(initial, configPath?, logger?)Configuration service with atomic snapshot reads and file-watching hot-reload DEFAULT_CONFIGBuilt-in default configuration object
ConfigService
Method Description getSnapshot()Return current config (atomic read) reload()Re-load config from the filesystem startWatching()Watch the config file for changes (debounced, 500ms) stopWatching()Stop file watcher
Collector
Export Description createCollectorService(config)Standalone OTLP pipeline service with health checks createHealthServer()Health check HTTP server with liveness, readiness, and debug endpoints
Collector Endpoints
Method Path Description POST/v1/tracesOTLP JSON trace ingestion GET/healthLiveness probe GET/readyReadiness probe GET/debugUptime, spans processed/dropped, pricing version GET/debug/pricingPer-provider model counts GET/debug/cacheCache hit/miss stats
Configuration Reference
Configuration is resolved by merging three layers (last wins):
Built-in defaults (DEFAULT_CONFIG)
YAML configuration file (via --config flag or loadConfig(path))
Environment variables (OTEL_COST_*)
Config Shape
Section Key Type Default Description pricingcustomTablePathstring?— Path to custom YAML pricing overrides pricingdefaultPricenumber?— Fallback USD/1M tokens for unknown models metricsprefixstringllm_costPrefix for emitted metric names metricslabelsRecord<string, string>{}Custom labels attached to all metrics exportformatprometheus" | "otlp" | "jsonprometheusExport format exportintervalstring60sPush interval for OTLP/JSON exportendpointstring?— OTLP collector endpoint exporthealthPortnumber8889Health/debug HTTP server port loggingleveldebug" | "info" | "warn" | "errorinfoLog level loggingformatjson" | "textjsonLog format
Usage Patterns
Custom Pricing Overrides
import { loadConfig } from "@reaatech/otel-cost-exporter" ;
const config = await loadConfig ( "./otel-cost-exporter.yaml" );
# otel-cost-exporter.yaml
pricing :
customTablePath : /etc/otel/custom-pricing.yaml
defaultPrice : 2.0
metrics :
prefix : llm_cost
labels :
environment : production
region : us-east-1
export :
format : prometheus
healthPort : 8889
logging :
level : info
format : json
Config Hot-Reload
import { DEFAULT_CONFIG, createConfigService } from "@reaatech/otel-cost-exporter" ;
const svc = createConfigService (DEFAULT_CONFIG, "./otel-cost-exporter.yaml" );
svc. startWatching (); // Will reload on file changes (debounced 500ms)
// Get current config atomically
const config = svc. getSnapshot ();
// Cleanup
svc. stopWatching ();
Span → CostSpan Adapter
import { spanToCostSpan } from "@reaatech/otel-cost-exporter" ;
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base" ;
// In your custom SpanProcessor.onEnd():
function onEnd (otelSpan : ReadableSpan ) : void {
const costSpan = spanToCostSpan (otelSpan);
if (costSpan) {
// Ready for cost calculation
const result = costProcessor. processSpan (costSpan);
metricsBuilder. recordCost (result.cost);
}
}
Related Packages
@reaatech/otel-cost-exporter-core — Domain types, Zod schemas, and semconv constants
@reaatech/otel-cost-exporter-pricing — Pricing table management with bundled provider data
@reaatech/otel-cost-exporter-calculator — Token cost calculator with model normalization and caching
@reaatech/otel-cost-exporter-cli — CLI for running the collector and managing pricing
License
MIT