Skip to main content

Overview

The Moda Node.js SDK (moda-ai) provides automatic instrumentation for your LLM applications with built-in conversation threading. Every LLM call is automatically tracked with a moda.conversation_id that groups multi-turn conversations together.

Installation

npm install moda-ai
Also install the LLM clients you want to use:
# For OpenAI
npm install openai

# For Anthropic
npm install @anthropic-ai/sdk

Quick Start

import { Moda } from 'moda-ai';
import OpenAI from 'openai';

await Moda.init('YOUR_MODA_API_KEY');

// Set conversation ID for your session (recommended)
Moda.conversationId = 'session_' + sessionId;

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

await Moda.flush();
Moda.init(...) is async. If you await it, initialization completes before your first LLM call (guaranteed instrumentation). If you skip await, startup is non-blocking but the very first call could occur before patching finishes.

Conversation Tracking

For production use, explicitly set a conversation ID to group related LLM calls. This gives you full control over how conversations are grouped in your Moda dashboard:
import { Moda } from 'moda-ai';

// Property-style API (recommended)
Moda.conversationId = 'support_ticket_123';

await client.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'I need help with my order' }]
});

await client.chat.completions.create({
  model: 'gpt-4o',
  messages: [
    { role: 'user', content: 'I need help with my order' },
    { role: 'assistant', content: 'I\'d be happy to help...' },
    { role: 'user', content: 'Order #12345' }
  ]
});

// Both calls share the same conversation_id
Moda.conversationId = null;  // clear when done

Setting User ID

Associate LLM calls with specific users for per-user analytics:
Moda.userId = 'user_12345';

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

Moda.userId = null;  // clear when done

Scoped Context with Callbacks

For callback-based scoping (useful in request handlers or async contexts):
import { Moda, withConversationId, withUserId, withContext } from 'moda-ai';

// Group specific calls under a custom conversation ID
await withConversationId('support-ticket-123', async () => {
  await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'I need help' }]
  });
});

// Attach user attribution
await withUserId('user-456', async () => {
  await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Hello' }]
  });
});

// Set both at once
await withContext('conv-123', 'user-456', async () => {
  await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Hello' }]
  });
  // All calls here use both IDs
});

Reading Current Context

import { getContext, getEffectiveContext, getGlobalContext } from 'moda-ai';

// Get context from the current scope only
const localContext = getContext();

// Get combined context (global + scoped, scoped takes precedence)
const effectiveContext = getEffectiveContext();

// Get only the globally set context
const globalContext = getGlobalContext();

console.log(`Conversation: ${effectiveContext.conversationId}`);
console.log(`User: ${effectiveContext.userId}`);

Method-Style API

The method-style API is also supported for backwards compatibility:
Moda.setConversationId('my-session-123');
Moda.setUserId('user-456');
// ... make calls ...
Moda.clearConversationId();
Moda.clearUserId();

Automatic Fallback (Simple Chatbots Only)

If you don’t set a conversation ID, the SDK automatically computes a stable one by hashing:
  • The first user message in the conversation
  • The system prompt (if present)
This only works when you pass the full message history with each API call:
import { Moda } from 'moda-ai';
import OpenAI from 'openai';

Moda.init('YOUR_MODA_API_KEY');
const client = new OpenAI();

// Turn 1
let messages = [{ role: 'user', content: 'What is TypeScript?' }];
const r1 = await client.chat.completions.create({ model: 'gpt-4o', messages });

// Turn 2 - automatically has the same conversation_id
messages.push({ role: 'assistant', content: r1.choices[0].message.content });
messages.push({ role: 'user', content: 'How do I install it?' });
const r2 = await client.chat.completions.create({ model: 'gpt-4o', messages });

// Both calls share the same conversation_id because "What is TypeScript?"
// is still the first user message in both calls
await Moda.flush();
Agent frameworks require explicit conversation IDs. The automatic fallback does NOT work with agent frameworks like LangChain, Claude Agent SDK, CrewAI, AutoGPT, or similar tools.

Why Automatic Detection Fails with Agents

Agent frameworks typically don’t pass the full message history with each LLM call. Each agent iteration usually passes only:
  • The system prompt (with context baked in)
  • Tool results from the previous step
  • A continuation prompt
This means each iteration has a different first user message, resulting in different conversation IDs:
// Agent iteration 1: user query
messages = [{ role: 'user', content: 'What are my top clusters?' }]  // conv_abc123

// Agent iteration 2: tool result
messages = [{ role: 'user', content: 'Tool returned: {...}' }]  // conv_xyz789 - DIFFERENT!

// Agent iteration 3: reasoning
messages = [{ role: 'user', content: 'Based on the data...' }]  // conv_def456 - DIFFERENT!

Solution for Agent Applications

Always wrap your agent execution with an explicit conversation ID:
// Set conversation ID before running the agent
Moda.conversationId = 'agent_session_' + sessionId;

// All internal LLM calls made by the agent will share this ID
const agent = new LangChainAgent();
await agent.run('What are my top clusters?');

// Or using callback-based scoping
await withConversationId('agent_session_' + sessionId, async () => {
  const result = await myAgent.execute(userQuery);
  return result;
});

Moda.conversationId = null;  // clear when done
For production applications, explicit conversation IDs are recommended as they provide:
  • Predictable grouping regardless of message content
  • Correct grouping for agent-based applications
  • Integration with your existing session/thread identifiers
  • Easier debugging and correlation with your application logs

Streaming Support

The SDK fully supports streaming responses:
const stream = await client.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Count to 5' }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
// Streaming responses are automatically tracked

Anthropic Support

Works the same way with Anthropic’s Claude:
import { Moda } from 'moda-ai';
import Anthropic from '@anthropic-ai/sdk';

Moda.init('YOUR_MODA_API_KEY');
const anthropic = new Anthropic();

Moda.conversationId = 'claude_session_123';

const response = await anthropic.messages.create({
  model: 'claude-3-haiku-20240307',
  max_tokens: 1024,
  system: 'You are a helpful assistant.',
  messages: [{ role: 'user', content: 'Hello!' }]
});

await Moda.flush();

Vercel AI SDK

The Moda SDK integrates with the Vercel AI SDK via its built-in telemetry support. Use Moda.getVercelAITelemetry() to get a telemetry configuration for the experimental_telemetry option:
import { Moda } from 'moda-ai';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

await Moda.init('YOUR_MODA_API_KEY');
Moda.conversationId = 'session_123';

const result = await generateText({
  model: openai('gpt-4o'),
  prompt: 'Write a haiku about coding',
  experimental_telemetry: Moda.getVercelAITelemetry(),
});
For full setup instructions, streaming, structured output, tool use, and provider-specific examples, see the dedicated Vercel AI SDK guide.

OpenRouter Support

OpenRouter provides access to multiple LLM providers through a unified API. Since OpenRouter uses an OpenAI-compatible interface, it works automatically with the Moda SDK:
import { Moda } from 'moda-ai';
import OpenAI from 'openai';

Moda.init('YOUR_MODA_API_KEY');

// Configure OpenAI client to use OpenRouter
const openrouter = new OpenAI({
  baseURL: 'https://openrouter.ai/api/v1',
  apiKey: 'YOUR_OPENROUTER_API_KEY',
  defaultHeaders: {
    'HTTP-Referer': 'https://your-app.com',  // Optional: for rankings
    'X-Title': 'Your App Name',               // Optional: for rankings
  },
});

Moda.conversationId = 'openrouter_session_123';

// Use any model available on OpenRouter
const response = await openrouter.chat.completions.create({
  model: 'anthropic/claude-3.5-sonnet',  // Or any OpenRouter model
  messages: [{ role: 'user', content: 'Hello!' }]
});

// Also works with OpenAI models via OpenRouter
const gptResponse = await openrouter.chat.completions.create({
  model: 'openai/gpt-4o',
  messages: [{ role: 'user', content: 'Hello!' }]
});
OpenRouter model names use the format provider/model-name. See the OpenRouter models page for all available models.

Manual Tracing

For LLM providers that aren’t automatically instrumented (direct API calls, custom providers, proxied requests), use Moda.withLLMCall() to manually trace calls:
import { Moda } from 'moda-ai';

await Moda.init('YOUR_MODA_API_KEY');
Moda.conversationId = 'session_123';

const messages = [{ role: 'user', content: 'Hello!' }];

const result = await Moda.withLLMCall(
  { vendor: 'openrouter', type: 'chat' },
  async ({ span }) => {
    // Report the request
    span.reportRequest({ model: 'anthropic/claude-3-sonnet', messages });

    // Make your API call
    const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ model: 'anthropic/claude-3-sonnet', messages }),
    });
    const data = await response.json();

    // Report the response
    span.reportResponse({
      model: data.model,
      usage: data.usage,
      completions: data.choices,
    });

    return data;
  }
);

Span Helper Methods

MethodDescription
span.reportRequest({ model, messages, conversationId?, userId? })Set request attributes before the LLM call
span.reportResponse({ model?, usage?, completions? })Set response attributes after the LLM call
span.rawSpanAccess the underlying span object for advanced use
The usage object accepts both OpenAI-style (prompt_tokens, completion_tokens) and Anthropic-style (input_tokens, output_tokens) fields.

Using with Sentry (or Other Tracing SDKs)

The Moda SDK automatically detects and coexists with other tracing SDKs like Sentry. When an existing tracing setup is detected, Moda integrates with it seamlessly instead of creating a separate one.

Sentry v8+ Integration

Initialize Sentry first, then Moda:
import * as Sentry from '@sentry/node';
import { Moda } from 'moda-ai';
import OpenAI from 'openai';

// 1. Initialize Sentry FIRST
Sentry.init({
  dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
  tracesSampleRate: 1.0,
});

// 2. Initialize Moda SECOND (detects Sentry automatically)
await Moda.init('YOUR_MODA_API_KEY', {
  debug: true, // Shows confirmation that Moda detected Sentry
});

// 3. Use OpenAI normally - spans go to BOTH Sentry and Moda
const openai = new OpenAI();
const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Hello!' }],
});

// 4. Cleanup - Moda shutdown preserves Sentry
await Moda.flush();
await Moda.shutdown(); // Only shuts down Moda's processor
With debug mode enabled, you should see a log message confirming that Moda detected your existing tracing setup and is sharing it.

How It Works

When another tracing SDK (like Sentry) is already initialized, Moda automatically detects it and shares the same tracing pipeline. This means:
  • LLM call data is sent to both Moda and your existing tracing tool
  • Moda.shutdown() only stops Moda, leaving your other SDK unaffected
  • You can re-initialize Moda after shutdown

Supported SDKs

Moda coexists with any compatible tracing SDK:
  • Sentry v8+
  • Datadog APM
  • New Relic
  • Honeycomb

Configuration

import { Moda } from 'moda-ai';

Moda.init('YOUR_MODA_API_KEY', {
  // Enable/disable the SDK (default: true)
  enabled: true,

  // Environment name shown in dashboard
  environment: 'production',

  // Custom ingest endpoint (optional)
  baseUrl: 'https://moda-ingest.modas.workers.dev/v1/traces',

  // Enable debug logging
  debug: false,

  // Batch size for telemetry export (default: 100)
  batchSize: 100,

  // Flush interval in milliseconds (default: 5000)
  flushInterval: 5000,
});

Testing the SDK

Local Development

Test with debug mode to see what’s being captured:
import { Moda, computeConversationId, generateRandomConversationId, isValidConversationId } from 'moda-ai';

Moda.init('test_key', {
  debug: true,
  enabled: false,  // Disable export for local testing
});

// Test conversation ID computation
const messages = [{ role: 'user', content: 'Hello' }];
const convId = computeConversationId(messages);
console.log('Conversation ID:', convId);

// Generate a random conversation ID
const randomId = generateRandomConversationId();
console.log('Random ID:', randomId);

// Validate a conversation ID
const isValid = isValidConversationId(convId);
console.log('Is valid:', isValid);

Graceful Shutdown

Always flush before your application exits:
process.on('SIGTERM', async () => {
  await Moda.flush();
  await Moda.shutdown();
  process.exit(0);
});

Data Captured

The SDK captures:
AttributeDescription
moda.conversation_idStable ID grouping multi-turn conversations
moda.user_idUser identifier (when set)
llm.vendorLLM provider (e.g., “openai”, “anthropic”)
llm.request.typeRequest type (e.g., “chat”, “completion”)
llm.request.modelRequested model name
llm.response.modelActual model used in response
llm.promptsUser and system messages
llm.completionsAssistant responses
llm.usage.prompt_tokensInput token count
llm.usage.completion_tokensOutput token count
llm.usage.total_tokensTotal token count
llm.usage.reasoning_tokensReasoning token count (extended thinking)

API Reference

Moda Object

Method/PropertyDescription
Moda.init(apiKey, options?)Initialize the SDK
Moda.flush()Force flush pending telemetry
Moda.shutdown()Shutdown and release resources
Moda.isInitialized()Check initialization status
Moda.getTracer()Get the tracer for custom spans
Moda.conversationIdGet/set global conversation ID (property)
Moda.userIdGet/set global user ID (property)
Moda.setConversationId(id)Set global conversation ID (method)
Moda.clearConversationId()Clear global conversation ID
Moda.setUserId(id)Set global user ID (method)
Moda.clearUserId()Clear global user ID
Moda.withLLMCall(options, callback)Manually trace an LLM call for non-instrumented providers
Moda.getVercelAITelemetry(options?)Get telemetry config for Vercel AI SDK
Moda.createModaSpanProcessor(options)Create a standalone span processor for advanced tracing setups
Moda.createModaProvider(options)Create a standalone tracing provider (bypasses external providers)
Moda.registerInstrumentations()Register OpenAI/Anthropic instrumentations manually

Context Functions

FunctionDescription
withConversationId(id, callback)Run callback with scoped conversation ID
withUserId(id, callback)Run callback with scoped user ID
withContext(convId, userId, callback)Run callback with both IDs scoped
getContext()Get context from the current scope only
getEffectiveContext()Get combined context (global + local)
getGlobalContext()Get only the globally set context

Conversation ID Utilities

FunctionDescription
computeConversationId(messages, systemPrompt?, explicitId?)Compute conversation ID from messages
generateRandomConversationId()Generate a random conversation ID
isValidConversationId(id)Validate conversation ID format

Named Exports

All functions are available as named exports for functional-style usage:
import {
  // Initialization
  init, flush, shutdown, isInitialized, getTracer,
  // Context management
  setConversationId, clearConversationId, setUserId, clearUserId,
  withConversationId, withUserId, withContext,
  getContext, getEffectiveContext, getGlobalContext,
  // Conversation ID utilities
  computeConversationId, generateRandomConversationId, isValidConversationId,
  // Manual tracing
  withLLMCall,
  // Vercel AI SDK integration
  getVercelAITelemetry,
  // Advanced tracing setup
  createModaSpanProcessor,
  createModaProvider,
  registerInstrumentations,
} from 'moda-ai';

Troubleshooting

Conversation IDs not grouping correctly?
  • If using an agent framework: You MUST use explicit Moda.conversationId - automatic detection does not work with agents
  • Use explicit Moda.conversationId instead of relying on auto-compute
  • If using auto-compute, ensure the full message history is passed with each API call
  • Check if system prompts are changing between calls
Data not appearing in Moda?
  • Call await Moda.flush() before your program exits
  • Check that your API key is correct
  • Enable debug mode: Moda.init('key', { debug: true })
TypeScript errors?
  • Ensure you have @types/node installed
  • The SDK requires Node.js >= 18.0.0

Requirements

  • Node.js >= 18.0.0
  • TypeScript >= 5.0 (for type definitions)