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
| Method | Description |
|---|---|
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 |
Wrap your client (recommended)
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.
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
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.
// 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
});
| Attribute | Description |
|---|---|
feature | Feature label ('chat', 'search'…). Group cost with costs.byFeature(). |
userId | Your end-user id. Powers costs.forUser() and per-user margins. |
agentId | AI agent id. Slice with costs.byAgent() / attribution.agents(). |
workflowId | Workflow id. Slice with costs.byWorkflow() / attribution.workflows(). |
metadata | Free-form string tags (team, customer, environment). Each key becomes a dimension for attribution.byDimension(). |
dimensions | Numeric custom dimensions ({ energy_kwh: 0.42 }). Aggregate with dimensions.numericSummary(). |
experiment | A/B experiment { id, variant } (both required). Compare cost and margin across variants. |
sessionId | The 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). |
sessionType | Freeform session category ("ticket", "support-chat"). Constant per session; compare with attribution.sessionComparison(). |
spanId | This 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. |
parentSpanId | The 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.
// 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.
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.
// 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();