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
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
Setting Conversation ID (Recommended)
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
| Method | Description |
|---|
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.rawSpan | Access 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:
| Attribute | Description |
|---|
moda.conversation_id | Stable ID grouping multi-turn conversations |
moda.user_id | User identifier (when set) |
llm.vendor | LLM provider (e.g., “openai”, “anthropic”) |
llm.request.type | Request type (e.g., “chat”, “completion”) |
llm.request.model | Requested model name |
llm.response.model | Actual model used in response |
llm.prompts | User and system messages |
llm.completions | Assistant responses |
llm.usage.prompt_tokens | Input token count |
llm.usage.completion_tokens | Output token count |
llm.usage.total_tokens | Total token count |
llm.usage.reasoning_tokens | Reasoning token count (extended thinking) |
API Reference
Moda Object
| Method/Property | Description |
|---|
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.conversationId | Get/set global conversation ID (property) |
Moda.userId | Get/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
| Function | Description |
|---|
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
| Function | Description |
|---|
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)