Skip to content
reaatech

@reaatech/otel-cost-exporter

npm v0.1.0

An OpenTelemetry span processor that reads GenAI semantic convention spans, calculates real-time USD costs using bundled pricing tables for major LLM providers, and exports the cost metrics via Prometheus, OTLP, or JSON.

@reaatech/otel-cost-exporter

npm version License: MIT CI

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

terminal
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

typescript
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

typescript
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

ExportDescription
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

MethodDescription
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

PropertyTypeDefaultDescription
maxBatchSizenumber100Flush after accumulating this many spans
batchTimeoutMsnumber5000Flush after this many ms of inactivity
logger{ warn(...) }No-opOptional logger for flush errors

OTel SDK Integration

ExportDescription
createCostSpanProcessor(options)OTel SpanProcessor adapter — extracts GenAI attributes on span end
spanToCostSpan(span)Converts an OTel ReadableSpan to a CostSpan for cost calculation

CostSpanProcessorOptions

PropertyTypeDescription
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

ExportDescription
createMetricsBuilder(meter, prefix)Creates a metrics recorder that emits OTel counter metrics
METRIC_INPUT_COSTllm.cost.input_tokens_usd
METRIC_OUTPUT_COSTllm.cost.output_tokens_usd
METRIC_TOTAL_COSTllm.cost.total_usd

MetricsBuilder

MethodDescription
recordCost(result, extraLabels?)Record a CostResult to all three counters with labels

Export Formats

ExportDescription
createPrometheusExporter(options?)Pull-based Prometheus exporter on configurable port
createOtlpExporter(options?)Push-based OTLP HTTP exporter
createJsonExporter(options?)Stdout JSON exporter for debugging

Configuration

ExportDescription
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

MethodDescription
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

ExportDescription
createCollectorService(config)Standalone OTLP pipeline service with health checks
createHealthServer()Health check HTTP server with liveness, readiness, and debug endpoints

Collector Endpoints

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

  1. Built-in defaults (DEFAULT_CONFIG)
  2. YAML configuration file (via --config flag or loadConfig(path))
  3. Environment variables (OTEL_COST_*)

Config Shape

SectionKeyTypeDefaultDescription
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

typescript
import { loadConfig } from "@reaatech/otel-cost-exporter";
 
const config = await loadConfig("./otel-cost-exporter.yaml");
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

typescript
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

typescript
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);
  }
}

License

MIT