SDK Reference · Tracking

Tracking usage

track() is the atom of Bear Lumen: one call, one cost event, and the only thing that ever reaches the API. Pass it a provider response and it auto-detects OpenAI, Anthropic, Bedrock, Gemini, Mistral, or Ollama, extracts the token usage, and queues a usage event for background delivery. Everything else in the SDK, from wrap() to spans, is context that makes your track events smarter.

Methods

MethodDescription
bear.track(response, options?)Auto-detect a provider response and record token usage
bear.track(stream, options?)Wrap a streaming response; await .result for final usage
bear.track(null, { model, ... })Manually track a non-LLM service or pre-calculated cost
bear.flush()Optional. Force a flush now without stopping the worker (e.g. to make just-tracked events queryable immediately).
bear.shutdown()Flush the queue and stop the worker. Required before a short-lived process exits; wire to graceful shutdown in long-running services.
bear.ping()Verify connectivity and API key

Most integrations should not call track()per LLM call. Wrap the provider client once and every call through it is tracked automatically, no matter who makes the call: your code, an agent framework's internal loop, or a retry. Manual track() remains the low-level escape hatch and the way to record non-LLM services.

TypeScript
import OpenAI from 'openai';

// Wrap your provider client ONCE and every call through it is tracked
// automatically: provider, model, tokens, cost. No per-call track() needed.
// Wrapped calls also inherit the ambient span context (see Tracing agentic
// work), so trees and sessions come for free once you add spans.
const openai = bear.wrap(new OpenAI());

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Hello!' }],
});
// Already tracked. The response object is untouched; use it as normal.

Track a response

TypeScript
import OpenAI from 'openai';
const openai = new OpenAI();

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Hello!' }],
});

// Hand the raw provider response to Bear Lumen. It auto-detects the provider,
// extracts token usage, and queues a usage event for background delivery.
// track() returns immediately (the event flushes in the background).
const result = bear.track(response);
console.log(result.model, result.inputTokens, result.outputTokens);
// -> gpt-4o 12 8   (TrackResult is a local echo of what was queued)

Add attribution & custom attributes

A bare track() already records the model, tokens, and cost. Pass optional attributes and each one becomes a dimension you can slice later: by end user, feature, agent, workflow, A/B variant, or any string tag you invent (team, customer, environment). This is the link between writing and reading. You tag once at track() time, then slice in the cost, attribution, and margin queries on Reading costs.

TypeScript
// Every attribute below is OPTIONAL. Each one becomes a slice you can query
// later: by end user, feature, agent, workflow, A/B variant, or any string tag
// you invent. You tag once at write time, then slice at read time.
const result = bear.track(response, {
  feature: 'chat',                    // group cost by product feature
  userId: user.id,                    // your end-user id -> costs.forUser(), margins
  agentId: 'support-agent',           // attribute to an AI agent
  workflowId: 'ticket-triage',        // attribute to a workflow
  metadata: {                         // free-form string tags -> custom dimensions
    team: 'growth',
    customer: 'acme-corp',
  },
  dimensions: { energy_kwh: 0.42 },   // numeric dimensions you can aggregate
  experiment: { id: 'prompt-v2', variant: 'b' }, // A/B attribution
});
AttributeDescription
featureFeature label ('chat', 'search'…). Group cost with costs.byFeature().
userIdYour end-user id. Powers costs.forUser() and per-user margins.
agentIdAI agent id. Slice with costs.byAgent() / attribution.agents().
workflowIdWorkflow id. Slice with costs.byWorkflow() / attribution.workflows().
metadataFree-form string tags (team, customer, environment). Each key becomes a dimension for attribution.byDimension().
dimensionsNumeric custom dimensions ({ energy_kwh: 0.42 }). Aggregate with dimensions.numericSummary().
experimentA/B experiment { id, variant } (both required). Compare cost and margin across variants.
sessionIdThe run / trace this event belongs to. Usually set for you by an enclosing span; pass it explicitly when not using spans (e.g. one id per multi-turn conversation).
sessionTypeFreeform session category ("ticket", "support-chat"). Constant per session; compare with attribution.sessionComparison().
spanIdThis call’s node in the call tree. Set automatically by spans; manual form is the escape hatch when a span cannot wrap the call site.
parentSpanIdThe caller’s span id (the tree edge). Set automatically by spans; requires spanId, which requires sessionId.

Multi-call work belongs in a span

The session and span attributes above are usually not set by hand. When one unit of work spans several calls (an agent run, a pipeline, a conversation turn), wrap it in a span and every tracked call inside inherits the session, the tree position, and your metadata labels automatically. Spans also carry outcomes, budgets, and retry identity. See Tracing agentic work.

Track a streaming response

For streams, track() returns a pass-through wrapper. Iterate it normally, then await its .result for the final usage once the stream is fully consumed.

TypeScript
// Streaming: track() returns a pass-through wrapper. Iterate as normal,
// then await .result for the final usage once the stream is consumed.
const stream = bear.track(response, { model: 'anthropic.claude-3-5-sonnet' });
for await (const chunk of stream) {
  // chunks pass through unchanged
}
const result = await stream.result;
console.log(result.inputTokens, result.outputTokens);
// -> 15 210   (final usage, available once the stream is fully consumed)

Track manually

No LLM response to hand over? Track non-token services (text-to-speech, images, GPU seconds) or a pre-calculated cost by passing null with explicit options. A model is required in this form.

TypeScript
import { Provider } from '@bearlumen/node-sdk';

// No LLM response? Track non-token services (TTS, images, GPU seconds) or a
// pre-calculated cost by passing null with explicit options. model is required.
bear.track(null, {
  model: 'eleven_multilingual_v2',
  provider: Provider.ELEVENLABS,
  feature: 'narration',
  units: { characters: 1500 },
});

Flush before exit

Flush before a short-lived process exits

track() queues events in memory and a background worker flushes them on a timer, so you never call flush after every track. You do need shutdown() once before a short-lived process exits (scripts, serverless, CLIs), or the last in-memory batch is lost. In a long-running server, wire shutdown() to your termination signal. flush() is optional: it forces a flush now without stopping the worker.

TypeScript
// The background worker auto-flushes on a timer, so you do NOT call this after
// every track(). You DO need it once before a short-lived process exits
// (scripts, serverless, CLIs), or the last in-memory batch is lost.
await bear.shutdown(); // flush the queue AND stop the worker (graceful exit)

// Long-running server: wire it to your termination signal.
process.on('SIGTERM', () => bear.shutdown());

// flush() is optional: force a flush now WITHOUT stopping, e.g. to make a
// just-tracked event queryable immediately in a test or demo.
await bear.flush();