> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moda.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Moda Node.js SDK

> Automatic LLM analytics with conversation threading for TypeScript/Node.js

## 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

```bash theme={"dark"}
npm install moda-ai
```

Also install the LLM clients you want to use:

```bash theme={"dark"}
# For OpenAI
npm install openai

# For Anthropic
npm install @anthropic-ai/sdk
```

## Quick Start

```typescript theme={"dark"}
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();
```

<Note>
  `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.
</Note>

## Prompt Management

Use `moda prompts sync` to version prompt files, then render them through the SDK:

```typescript theme={"dark"}
const rendered = Moda.prompt("support.triage").render({
  ticket: { text: userMessage },
});

Moda.conversationId = conversationId;
Moda.userId = userId;

await client.chat.completions.create({
  model: "gpt-4o",
  messages: rendered.messages,
});
```

`Moda.prompt(...).render(...)` attaches prompt metadata to the active Moda context. Automatic OpenAI and Anthropic instrumentation then emits `moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`, and `moda.prompt_version_id` on the span.

For concurrent request handlers, scope prompt metadata explicitly:

```typescript theme={"dark"}
await Moda.withPrompt(rendered, async () => {
  await client.chat.completions.create({
    model: "gpt-4o",
    messages: rendered.messages,
  });
});
```

See [Prompt Management](/prompt-management/overview) for the code-first sync workflow.

## 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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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):

```typescript theme={"dark"}
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

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:**

```typescript theme={"dark"}
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();
```

<Warning>
  **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.
</Warning>

### 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:

```typescript theme={"dark"}
// 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:

```typescript theme={"dark"}
// 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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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](https://ai-sdk.dev) via its built-in telemetry support. Use `Moda.getVercelAITelemetry()` to get a telemetry configuration for the `experimental_telemetry` option:

```typescript theme={"dark"}
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(),
});
```

<Info>
  For full setup instructions, streaming, structured output, tool use, and provider-specific examples, see the dedicated [Vercel AI SDK](/ingestion/vercel-ai-sdk) guide.
</Info>

## OpenRouter Support

[OpenRouter](https://openrouter.ai) provides access to multiple LLM providers through a unified API. Since OpenRouter uses an OpenAI-compatible interface, it works automatically with the Moda SDK:

```typescript theme={"dark"}
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!' }]
});
```

<Tip>
  OpenRouter model names use the format `provider/model-name`. See the [OpenRouter models page](https://openrouter.ai/models) for all available models.
</Tip>

## Manual Tracing

For LLM providers that aren't automatically instrumented (direct API calls, custom providers, proxied requests), use `Moda.withLLMCall()` to manually trace calls:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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
```

<Info>
  With debug mode enabled, you should see a log message confirming that Moda detected your existing tracing setup and is sharing it.
</Info>

### 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

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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)
