Skip to content
reaatechREAATECH

@reaatech/agent-budget-otel-bridge

npm v0.1.0

Extracts GenAI usage metrics from OpenTelemetry span attributes to automatically record spend entries against budget scopes. It provides a `SpanListener` class that integrates with a `BudgetController` to process span data as they complete.

@reaatech/agent-budget-otel-bridge

npm version License: MIT CI

Status: Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.

OpenTelemetry span-to-spend bridge that converts GenAI spans into budget-tracked spend entries in real time. Drop it into any OTel-instrumented agent and every LLM call is automatically recorded against your budgets — no manual record() calls needed.

Installation

terminal
npm install @reaatech/agent-budget-otel-bridge
# or
pnpm add @reaatech/agent-budget-otel-bridge

Feature Overview

  • Automatic spend recording — every GenAI span becomes a SpendEntry pushed to the BudgetController
  • OTel GenAI attribute extraction — reads gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.request.model, gen_ai.system, llm.cost.total_usd
  • Custom scope extraction — supply your own scopeExtractor to map spans to budget scopes
  • Default scope extractor — reads budget.scope_type and budget.scope_key from span attributes
  • Non-blocking — span processing is fire-and-forget; the bridge never adds latency to your agent
  • Optional peer dependency@opentelemetry/api is optional; the bridge works with any OTel-compatible span data

Quick Start

typescript
import { SpanListener } from '@reaatech/agent-budget-otel-bridge';
import { BudgetController } from '@reaatech/agent-budget-engine';
import { SpendStore } from '@reaatech/agent-budget-spend-tracker';
import { BudgetScope } from '@reaatech/agent-budget-types';
 
const store = new SpendStore();
const controller = new BudgetController({ spendTracker: store });
 
const listener = new SpanListener({ controller });
 
// When an OTel span ends, feed it to the listener
function onSpanEnd(span: ReadableSpan) {
  listener.onSpanEnd(span.attributes);
}

With an OTel Span Processor

typescript
import { SpanListener } from '@reaatech/agent-budget-otel-bridge';
import { NodeTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
 
const listener = new SpanListener({ controller });
const provider = new NodeTracerProvider();
 
provider.addSpanProcessor({
  onEnd(span) {
    listener.onSpanEnd(span.attributes);
  },
  forceFlush: () => Promise.resolve(),
  shutdown: () => Promise.resolve(),
});

API Reference

SpanListener

MethodDescription
onSpanEnd(attributes, overrides?)Process span attributes and record a spend entry. Returns true if a spend entry was created.

Constructor

typescript
new SpanListener(options: {
  controller: BudgetController;
  scopeExtractor?: (attributes: Record<string, unknown>) => {
    scopeType: BudgetScope;
    scopeKey: string;
  } | null;
})

The default scopeExtractor reads these span attributes:

AttributeDescription
budget.scope_typetask, user, session, or org
budget.scope_keyThe scope identifier (e.g., user-42)

The listener extracts these standard GenAI OTel attributes:

AttributeUsed For
gen_ai.usage.input_tokensToken count
gen_ai.usage.output_tokensToken count
gen_ai.request.modelModel ID
gen_ai.systemProvider name mapping
llm.cost.total_usdRecorded cost

Usage Patterns

Span Attribute Overrides

typescript
listener.onSpanEnd(span.attributes, {
  cost: 0.15,
  modelId: 'claude-sonnet-4',
});

Integration with OpenTelemetry SDK

typescript
import { SpanListener } from '@reaatech/agent-budget-otel-bridge';
import { BudgetController } from '@reaatech/agent-budget-engine';
import { SpendStore } from '@reaatech/agent-budget-spend-tracker';
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
 
const store = new SpendStore();
const controller = new BudgetController({ spendTracker: store });
const listener = new SpanListener({ controller });
 
await controller.defineBudget({
  scopeType: BudgetScope.User,
  scopeKey: 'user-42',
  limit: 10.0,
  policy: { softCap: 0.8, hardCap: 1.0 },
});
 
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()));
provider.addSpanProcessor({
  onEnd: (span) => listener.onSpanEnd(span.attributes),
  forceFlush: () => Promise.resolve(),
  shutdown: () => Promise.resolve(),
});
provider.register();

Creating a Custom Scope Extractor

typescript
const listener = new SpanListener({
  controller,
  scopeExtractor: (attributes) => {
    const userId = attributes['myapp.user_id'] as string;
    const orgId = attributes['myapp.org_id'] as string;
 
    if (userId) {
      return { scopeType: BudgetScope.User, scopeKey: userId };
    }
    if (orgId) {
      return { scopeType: BudgetScope.Org, scopeKey: orgId };
    }
    return null;
  },
});

License

MIT