Official TypeScript/Node.js SDK for Moda LLM observability with automatic conversation threading
npm install moda-aiOfficial TypeScript/Node.js SDK for Moda LLM observability with automatic conversation threading.
- Automatic Instrumentation: Zero-config tracing for OpenAI and Anthropic clients
- Conversation Threading: Groups multi-turn conversations together
- Streaming Support: Full support for streaming responses
- User Tracking: Associate LLM calls with specific users
- OpenTelemetry Native: Built on OpenTelemetry for standard-compliant telemetry
``bash`
npm install moda-ai
`typescript
import { Moda } from 'moda-ai';
import OpenAI from 'openai';
// Initialize once at application startup
Moda.init('moda_your_api_key');
// Set conversation ID for your session (recommended)
Moda.conversationId = 'session_' + sessionId;
// All OpenAI calls are now automatically tracked
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello!' }],
});
// Flush before exit
await Moda.flush();
`
For production use, explicitly set a conversation ID to group related LLM calls:
`typescript
// Property-style API (recommended)
Moda.conversationId = 'support_ticket_123';
await openai.chat.completions.create({ ... });
await openai.chat.completions.create({ ... });
// Both calls share the same conversation_id
Moda.conversationId = null; // clear when done
// Method-style API (also supported)
Moda.setConversationId('support_ticket_123');
await openai.chat.completions.create({ ... });
Moda.clearConversationId();
`
Associate LLM calls with specific users:
`typescript
Moda.userId = 'user_12345';
await openai.chat.completions.create({ ... });
Moda.userId = null; // clear when done
// Or use method-style
Moda.setUserId('user_12345');
await openai.chat.completions.create({ ... });
Moda.clearUserId();
`
For callback-based scoping (useful in async contexts):
`typescript
import { withConversationId, withUserId, withContext } from 'moda-ai';
// Scoped conversation ID
await withConversationId('my_session_123', async () => {
await openai.chat.completions.create({ ... });
await openai.chat.completions.create({ ... });
// Both calls use 'my_session_123'
});
// Scoped user ID
await withUserId('user_456', async () => {
await openai.chat.completions.create({ ... });
});
// Both at once
await withContext('conv_123', 'user_456', async () => {
// ...
});
`
If you don't set a conversation ID, the SDK automatically computes one by hashing the first user message and system prompt. This only works for simple chatbots where you pass the full message history with each API call:
`typescript
// Turn 1
let messages = [{ role: 'user', content: 'Hi, help with TypeScript' }];
const r1 = await openai.chat.completions.create({ model: 'gpt-4', messages });
// Turn 2 - automatically linked to Turn 1
messages.push({ role: 'assistant', content: r1.choices[0].message.content });
messages.push({ role: 'user', content: 'How do I read a file?' });
const r2 = await openai.chat.completions.create({ model: 'gpt-4', messages });
// Both turns have the SAME conversation_id because "Hi, help with TypeScript"
// is still the first user message in both calls
`
LLM APIs are stateless. Each API call must include the full conversation history. The SDK extracts the first user message from the messages array and hashes it to create a stable conversation ID across turns.
Agent frameworks (LangChain, Claude Agent SDK, CrewAI, AutoGPT, etc.) do NOT pass full message history. Each agent iteration typically passes only:
- 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:
`typescript
// Agent iteration 1
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
messages = [{ role: 'user', content: 'Based on the data...' }] // conv_def456 - DIFFERENT!
`
For agent-based applications, you MUST use explicit conversation IDs:
`typescript
// Wrap your entire agent execution
Moda.conversationId = 'agent_session_' + sessionId;
const agent = new LangChainAgent();
await agent.run('What are my top clusters?'); // All internal LLM calls share same ID
Moda.conversationId = null;
`
Works the same way with Anthropic's Claude:
`typescript
import Anthropic from '@anthropic-ai/sdk';
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!' }],
});
`
The SDK fully supports streaming responses:
`typescript
const stream = await openai.chat.completions.create({
model: 'gpt-4',
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
`
The Moda SDK automatically detects and coexists with other OpenTelemetry-based SDKs like Sentry. When an existing TracerProvider is detected, Moda adds its SpanProcessor to the existing provider instead of creating a new one.
Sentry v8+ uses OpenTelemetry internally for tracing. Initialize Sentry first, then Moda:
`typescript
import * as Sentry from '@sentry/node';
import { Moda } from 'moda-ai';
import OpenAI from 'openai';
// 1. Initialize Sentry FIRST (sets up OpenTelemetry TracerProvider)
Sentry.init({
dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
tracesSampleRate: 1.0,
});
// 2. Initialize Moda SECOND (detects Sentry's provider automatically)
await Moda.init('moda_your_api_key', {
debug: true, // Shows: "[Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it"
});
// 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-mini',
messages: [{ role: 'user', content: 'Hello!' }],
});
// 4. Cleanup - Moda shutdown preserves Sentry
await Moda.flush();
await Moda.shutdown(); // Only shuts down Moda's processor, Sentry continues working
`
When Moda detects an existing TracerProvider (e.g., from Sentry):
- Moda adds its SpanProcessor to the existing provider
- Both SDKs receive the same spans with identical trace IDs
- Moda.shutdown() only removes Moda's processor, preserving the other SDK
- You can re-initialize Moda after shutdown
With debug: true, you should see:``
[Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it
You should NOT see:
``
Error: Attempted duplicate registration of tracer provider
This coexistence works with any SDK that uses OpenTelemetry's TracerProvider:
- Sentry v8+
- Datadog APM
- New Relic
- Honeycomb
- Custom OpenTelemetry setups
If Sentry filters out LLM spans (only shows HTTP/DB spans), use Moda.createModaProvider to create a separate provider that bypasses Sentry's sampling:
`javascript
// instrument.js - load AFTER Sentry.init()
import { Moda } from 'moda-ai';
if (process.env.MODA_API_KEY) {
// Create Moda's own provider (doesn't affect Sentry)
Moda.createModaProvider({ apiKey: process.env.MODA_API_KEY });
// Register OpenAI/Anthropic instrumentations
Moda.registerInstrumentations();
}
`
This approach:
- ✅ Bypasses Sentry's span sampling/filtering
- ✅ Sentry continues working normally for HTTP/DB/errors
- ✅ Moda receives all LLM spans independently
- ✅ Two separate pipelines, no interference
`typescript
Moda.init('moda_api_key', {
// Base URL for telemetry ingestion
baseUrl: 'https://ingest.moda.so/v1/traces',
// Environment name (shown in dashboard)
environment: 'production',
// Enable/disable the SDK
enabled: true,
// Enable debug logging
debug: false,
// Batch size for telemetry export
batchSize: 100,
// Flush interval in milliseconds
flushInterval: 5000,
});
`
`typescript
// Initialize the SDK
Moda.init(apiKey: string, options?: ModaInitOptions): void
// Force flush pending telemetry
Moda.flush(): Promise
// Shutdown and release resources
Moda.shutdown(): Promise
// Check initialization status
Moda.isInitialized(): boolean
// Property-style context (recommended)
Moda.conversationId: string | null // get/set
Moda.userId: string | null // get/set
// Method-style context (also supported)
Moda.setConversationId(id: string): void
Moda.clearConversationId(): void
Moda.setUserId(id: string): void
Moda.clearUserId(): void
`
`typescript
import { withConversationId, withUserId, withContext } from 'moda-ai';
// Scoped conversation ID
await withConversationId('conv_123', async () => {
// All LLM calls here use 'conv_123'
});
// Scoped user ID
await withUserId('user_456', async () => {
// All LLM calls here are associated with 'user_456'
});
// Both at once
await withContext('conv_123', 'user_456', async () => {
// ...
});
`
`typescript
import { computeConversationId, generateRandomConversationId } from 'moda-ai';
// Compute conversation ID from messages (same algorithm SDK uses)
const id = computeConversationId(messages, systemPrompt);
// Generate a random conversation ID
const randomId = generateRandomConversationId();
`
Always flush before your application exits:
`typescript`
process.on('SIGTERM', async () => {
await Moda.flush();
await Moda.shutdown();
process.exit(0);
});
- Node.js >= 18.0.0
- TypeScript >= 5.0 (for type definitions)
Install the LLM clients you want to use:
`bashFor OpenAI
npm install openai
MIT