LLM Observability SDK

AI observability SDK with cost tracking, trace analysis, and performance monitoring.

Installation

npm install @usetransactional/llm-node

Quick Start

Initialize the SDK once at application startup, then instrument your LLM calls with traces and generations:

import { initLlmOps, getLlmOps } from '@usetransactional/llm-node';
 
// Initialize once at startup
initLlmOps({
  dsn: 'https://pk_abc123@api.usetransactional.com/observability/42',
});
 
const llmOps = getLlmOps();
 
// Create a trace for a user interaction
const trace = llmOps.trace({
  name: 'chat-completion',
  input: { message: 'Explain quantum computing' },
  userId: 'user-123',
  tags: ['chat', 'physics'],
});
 
// Record an LLM generation within the trace
const generation = llmOps.generation({
  name: 'openai-response',
  traceId: trace.id,
  modelName: 'gpt-4o',
  input: [{ role: 'user', content: 'Explain quantum computing' }],
  modelParameters: { temperature: 0.7, maxTokens: 1000 },
});
 
// After receiving the LLM response
generation.end({
  output: 'Quantum computing uses quantum bits...',
  promptTokens: 12,
  completionTokens: 150,
  totalTokens: 162,
});
 
trace.end({
  output: { response: 'Quantum computing uses quantum bits...' },
  status: 'success',
});

Initialization

The SDK uses a singleton pattern. Initialize it once at application startup before making any observations.

initLlmOps(config)

Initializes the global LLM observability instance. Must be called before any other SDK methods.

import { initLlmOps } from '@usetransactional/llm-node';
 
initLlmOps({
  dsn: 'https://pk_abc123@api.usetransactional.com/observability/42',
  debug: true,
});

getLlmOps()

Returns the initialized singleton instance. Throws an error if called before initLlmOps().

import { getLlmOps } from '@usetransactional/llm-node';
 
const llmOps = getLlmOps();

isInitialized()

Returns true if the SDK has been initialized, false otherwise. Useful for conditional instrumentation.

import { isInitialized } from '@usetransactional/llm-node';
 
if (isInitialized()) {
  const llmOps = getLlmOps();
  // record observations
}

resetLlmOps()

Resets the singleton instance. Primarily used for testing to ensure a clean state between test runs.

import { resetLlmOps } from '@usetransactional/llm-node';
 
afterEach(() => {
  resetLlmOps();
});

Configuration

All configuration options for initLlmOps():

OptionTypeRequiredDefaultDescription
dsnstringYes*-Full DSN URL. Format: https://<publicKey>@<host>/observability/<projectId>
publicKeystringYes*-Public API key. Alternative to DSN
projectIdstringYes*-Project ID. Alternative to DSN
baseUrlstringYes*-API base URL. Alternative to DSN
enabledbooleanNotrueEnable or disable the SDK. When false, all methods are no-ops
batchSizenumberNo100Number of events to batch before flushing
flushIntervalnumberNo5000Milliseconds between automatic flushes
debugbooleanNofalseEnable debug logging to the console

*Either provide dsn or all three of publicKey, projectId, and baseUrl.

Using a DSN (recommended):

initLlmOps({
  dsn: 'https://pk_abc123@api.usetransactional.com/observability/42',
});

Using explicit configuration:

initLlmOps({
  publicKey: 'pk_abc123',
  projectId: '42',
  baseUrl: 'https://api.usetransactional.com',
});

Tracing

Traces represent a complete unit of work, such as a user request or an agent loop. Every observation (generation, span, event) is associated with a trace.

Creating a Trace

const llmOps = getLlmOps();
 
const trace = llmOps.trace({
  name: 'customer-support-agent',
  input: { question: 'How do I reset my password?' },
  userId: 'user-456',
  metadata: {
    environment: 'production',
    version: '1.2.0',
  },
  tags: ['support', 'auth'],
});

Parameters:

ParameterTypeRequiredDescription
namestringYesDescriptive name for the trace
inputunknownNoInput data that initiated the trace
userIdstringNoID of the user who triggered the trace
metadataRecord<string, unknown>NoArbitrary metadata to attach to the trace
tagsstring[]NoTags for filtering and categorization

TraceHandle

The object returned by llmOps.trace() with the following properties and methods:

Property / MethodTypeDescription
idstringUnique trace identifier
end(data?)(data?: { output?, status? }) => voidEnd the trace with optional output and status
error(err)(err: Error) => voidRecord an error and end the trace with error status
// Successful completion
trace.end({
  output: { answer: 'Go to Settings > Security > Reset Password' },
  status: 'success',
});
 
// Error completion
try {
  // ... LLM call
} catch (err) {
  trace.error(err);
}

Updating a Trace

Update a trace after creation to add additional context:

llmOps.updateTrace(trace.id, {
  metadata: { resolvedBy: 'agent-v2' },
  tags: ['resolved'],
});

Observations

Observations are the individual steps within a trace. There are three types: generations, spans, and events.

generation()

Records an LLM call with model details, token usage, and cost tracking.

const generation = llmOps.generation({
  name: 'summarize-article',
  traceId: trace.id,
  modelName: 'gpt-4o',
  input: [
    { role: 'system', content: 'Summarize the following article.' },
    { role: 'user', content: articleText },
  ],
  modelParameters: {
    temperature: 0.3,
    maxTokens: 500,
  },
});
 
// After receiving the response
generation.end({
  output: 'The article discusses...',
  promptTokens: 2048,
  completionTokens: 120,
  totalTokens: 2168,
});

span()

Records a generic operation such as a tool call, retrieval step, or processing stage.

const span = llmOps.span({
  name: 'vector-search',
  traceId: trace.id,
  input: { query: 'password reset instructions', topK: 5 },
});
 
// After the operation completes
span.end({
  output: { results: documents, count: 5 },
});

event()

Records a point-in-time event such as a user action or system occurrence. Events have no duration.

llmOps.event({
  name: 'user-feedback',
  traceId: trace.id,
  input: { rating: 5, comment: 'Very helpful!' },
});

ObservationHandle

The object returned by generation(), span(), and event():

Property / MethodTypeDescription
idstringUnique observation identifier
end(data?)(data?: { output?, promptTokens?, completionTokens?, totalTokens? }) => voidEnd the observation with optional output and token usage
error(err)(err: Error) => voidRecord an error on the observation

Updating an Observation

llmOps.updateObservation(generation.id, {
  metadata: { cached: true },
});

Vercel AI SDK Integration

The SDK provides first-class wrappers for the Vercel AI SDK to automatically capture traces and token usage.

wrapAiSdk(fn)

Wraps generateText or similar functions from the Vercel AI SDK:

import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getLlmOps } from '@usetransactional/llm-node';
 
const llmOps = getLlmOps();
 
const wrappedGenerateText = llmOps.wrapAiSdk(generateText);
 
const result = await wrappedGenerateText({
  model: openai('gpt-4o'),
  prompt: 'Explain the theory of relativity in simple terms.',
});
 
console.log(result.text);

The wrapper automatically captures the model name, input, output, token usage, and latency as a generation observation within a trace.

wrapStreamText(fn)

Wraps streamText for streaming responses:

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getLlmOps } from '@usetransactional/llm-node';
 
const llmOps = getLlmOps();
 
const wrappedStreamText = llmOps.wrapStreamText(streamText);
 
const result = await wrappedStreamText({
  model: openai('gpt-4o'),
  prompt: 'Write a poem about observability.',
});
 
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

Token usage and the full output are captured automatically when the stream completes.

LangChain Integration

Use the TransactionalCallbackHandler to automatically instrument all LangChain calls.

import { ChatOpenAI } from '@langchain/openai';
import { TransactionalCallbackHandler } from '@usetransactional/llm-node/langchain';
 
const handler = new TransactionalCallbackHandler({
  sessionId: 'session-abc',
  userId: 'user-123',
});
 
const model = new ChatOpenAI({
  modelName: 'gpt-4o',
  temperature: 0.7,
  callbacks: [handler],
});
 
const response = await model.invoke('What is the capital of France?');
console.log(response.content);

The callback handler automatically creates traces and generations for every LangChain LLM call, chain run, and tool invocation. Constructor options:

OptionTypeRequiredDescription
sessionIdstringNoSession identifier to group related traces
userIdstringNoUser identifier to associate with traces

Lifecycle

Flushing Events

Force an immediate flush of all queued events:

await llmOps.flush();

Graceful Shutdown

Flush all pending events and release resources. Call this before your application exits:

process.on('SIGTERM', async () => {
  await llmOps.shutdown();
  process.exit(0);
});

In serverless environments (AWS Lambda, Vercel Functions), call flush() at the end of each invocation to ensure all events are sent before the function freezes:

export async function handler(event) {
  const llmOps = getLlmOps();
  // ... your logic
  await llmOps.flush();
  return response;
}

Batching

The SDK automatically batches events for efficient network usage. Events are queued in memory and flushed to the Transactional API when either condition is met:

  • The queue reaches batchSize events (default: 100)
  • The flushInterval timer elapses (default: 5000ms)

This means under normal load, events are sent in batches every 5 seconds. Under high throughput, batches are sent as soon as 100 events accumulate. You can tune these values based on your application's needs:

initLlmOps({
  dsn: 'https://pk_abc123@api.usetransactional.com/observability/42',
  batchSize: 50,       // Flush sooner under high throughput
  flushInterval: 2000, // Flush every 2 seconds
});

Types

The SDK exports all types for full TypeScript support:

import type {
  TraceHandle,
  ObservationHandle,
  ObservationType,
  ObservationLevel,
  TraceStatus,
} from '@usetransactional/llm-node';
TypeDescription
TraceHandleObject returned by llmOps.trace() with id, end(), and error()
ObservationHandleObject returned by generation(), span(), and event() with id, end(), and error()
ObservationTypeEnum: GENERATION, SPAN, EVENT
ObservationLevelSeverity level for observations: DEBUG, DEFAULT, WARNING, ERROR
TraceStatusTrace completion status: success, error