> ## 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 SDK for Node.js

> Install moda-ai to automatically capture openai and @anthropic-ai/sdk calls with conversation and user context.

The Moda SDK for Node.js (`moda-ai`) auto-instruments the `openai` and `@anthropic-ai/sdk` client libraries: after `await Moda.init(...)`, chat completions and messages — including streamed responses and tool calls — are captured and sent to Moda without changes to your provider code. This page covers installation, configuration, provider coverage, context APIs, manual capture, and running alongside an existing OpenTelemetry setup.

## Prerequisites

* Node.js 18 or later (TypeScript 5.0 or later for type definitions)
* A Moda API key, created at **Settings → Ingestion keys** (see [Authentication](/administration/authentication))
* The `openai` (4.0+) or `@anthropic-ai/sdk` (0.18+) package your app already uses

## Set up

<Steps>
  <Step title="Install the SDK">
    ```bash theme={"dark"}
    npm install moda-ai
    ```
  </Step>

  <Step title="Initialize and make a call">
    Call `Moda.init(apiKey, options)` once at startup and await it. Create your provider clients after init.

    ```typescript app.ts theme={"dark"}
    import { Moda } from 'moda-ai';
    import OpenAI from 'openai';

    await Moda.init(process.env.MODA_API_KEY!, {
      environment: 'production',
    });

    const client = new OpenAI();

    Moda.conversationId = 'session_8f2a';
    Moda.userId = 'user_1042';

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

    await Moda.flush();
    ```
  </Step>

  <Step title="Verify in the dashboard">
    Open **Conversations** in the [dashboard](https://moda.dev/dashboard). Within minutes you should see a conversation with ID `session_8f2a` containing the user message and the assistant response, with the model name and token counts attached.
  </Step>
</Steps>

<Warning>
  `Moda.init()` is async and must be awaited, and it must run before you create provider clients. If init is not awaited, calls made before initialization completes are not captured. The API key argument is required — unlike the Python SDK, `Moda.init()` does not read `MODA_API_KEY` from the environment, and it throws `[Moda] API key is required` if the key is missing.
</Warning>

## Configuration

### `Moda.init()` options

```typescript theme={"dark"}
await Moda.init(process.env.MODA_API_KEY!, {
  environment: 'staging',
  debug: false,
  batchSize: 100,
  flushInterval: 5000,
});
```

| Option          | Type      | Default                                           | Description                                                            |
| --------------- | --------- | ------------------------------------------------- | ---------------------------------------------------------------------- |
| `baseUrl`       | `string`  | `https://moda-ingest.modas.workers.dev/v1/traces` | Full URL of the ingestion traces endpoint.                             |
| `environment`   | `string`  | `'production'`                                    | Environment tag: `development`, `staging`, or `production`.            |
| `enabled`       | `boolean` | `true`                                            | Set `false` to disable the SDK entirely (no data is captured or sent). |
| `debug`         | `boolean` | `false`                                           | Verbose logging; also switches to immediate (unbatched) span export.   |
| `batchSize`     | `number`  | `100`                                             | Maximum spans per export batch.                                        |
| `flushInterval` | `number`  | `5000`                                            | Milliseconds between automatic exports of batched spans.               |

### Lifecycle

| Call                    | Behavior                                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `await Moda.flush()`    | Force-export all pending spans. Call before the process exits or a serverless invocation returns. |
| `await Moda.shutdown()` | Flush and release resources. You can call `Moda.init()` again afterward.                          |
| `Moda.isInitialized()`  | Returns whether the SDK is active.                                                                |

Calling `Moda.init()` a second time while initialized is a no-op. `flush()` and `shutdown()` are safe no-ops before init.

## What gets instrumented

The SDK instruments exactly two client libraries:

* **`openai`** — `chat.completions.create`, including streaming and tool calls.
* **`@anthropic-ai/sdk`** — `messages.create` and `messages.stream`, including tool use and extended thinking.

| Provider                                        | Support                                                                                                                                    |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| OpenAI chat completions                         | Supported, including streaming                                                                                                             |
| OpenAI embeddings                               | Not supported — use the [Python SDK](/ingestion/python), [OpenTelemetry](/ingestion/opentelemetry), or the [HTTP API](/ingestion/http-api) |
| Anthropic Messages                              | Supported, including streaming and extended thinking                                                                                       |
| OpenRouter and other OpenAI-compatible gateways | Supported through the `openai` client with a custom `baseURL` — see below                                                                  |
| Amazon Bedrock                                  | Not supported in Node.js — use [OpenTelemetry](/ingestion/opentelemetry) or the [HTTP API](/ingestion/http-api)                            |
| Vercel AI SDK                                   | Supported via `Moda.getVercelAITelemetry()` — see [Vercel AI SDK](/ingestion/vercel-ai-sdk)                                                |
| Vapi                                            | Supported via `Moda.processVapiEndOfCallReport()` — see [Vapi](/ingestion/providers/vapi)                                                  |
| OpenClaw                                        | Supported via the OpenClaw helpers — see [OpenClaw](/ingestion/providers/openclaw)                                                         |
| Anything else                                   | Use [manual capture](#manual-capture-with-withllmcall)                                                                                     |

### Anthropic example

```typescript claude.ts theme={"dark"}
import { Moda } from 'moda-ai';
import Anthropic from '@anthropic-ai/sdk';

await Moda.init(process.env.MODA_API_KEY!);

const anthropic = new Anthropic();

Moda.conversationId = 'session_8f2a';

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

await Moda.flush();
```

### OpenRouter and OpenAI-compatible endpoints

Any endpoint reached through the `openai` client with a custom `baseURL` is captured — requests, responses, streaming, and token usage. The SDK records these calls with vendor `openai`.

```typescript openrouter.ts theme={"dark"}
import { Moda } from 'moda-ai';
import OpenAI from 'openai';

await Moda.init(process.env.MODA_API_KEY!);

const openrouter = new OpenAI({
  baseURL: 'https://openrouter.ai/api/v1',
  apiKey: process.env.OPENROUTER_API_KEY,
});

Moda.conversationId = 'session_8f2a';

const response = await openrouter.chat.completions.create({
  model: 'anthropic/claude-3-haiku',
  messages: [{ role: 'user', content: 'Hello!' }],
});

await Moda.flush();
```

## Conversation and user context

Set a conversation ID to group related calls into one conversation, and a user ID to attribute them to a user.

### Global properties and setters

```typescript theme={"dark"}
import { Moda } from 'moda-ai';

// Property style
Moda.conversationId = 'support_ticket_123';
Moda.userId = 'user_1042';
// ... make LLM calls ...
Moda.conversationId = null; // clear when done
Moda.userId = null;

// Method style (equivalent)
Moda.setConversationId('support_ticket_123');
Moda.setUserId('user_1042');
Moda.clearConversationId();
Moda.clearUserId();
```

Global context applies to every call in the process. In servers that handle concurrent requests, use the scoped functions instead.

### Scoped context

`withConversationId`, `withUserId`, and `withContext` run a callback with context stored in `AsyncLocalStorage`: the scoped value overrides the global one, follows the callback across `await` boundaries, is isolated from parallel requests, restores the previous value on exit, and can be nested.

```typescript server.ts theme={"dark"}
import { Moda, withContext } from 'moda-ai';
import OpenAI from 'openai';

await Moda.init(process.env.MODA_API_KEY!);
const client = new OpenAI();

async function handleRequest(sessionId: string, userId: string, question: string) {
  return withContext(`session_${sessionId}`, userId, async () => {
    const response = await client.chat.completions.create({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: question }],
    });
    return response.choices[0].message.content;
  });
}
```

```typescript theme={"dark"}
import { withConversationId, withUserId } from 'moda-ai';

// Scope only the conversation ID
await withConversationId('session_123', async () => {
  await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Hello!' }],
  });
});

// Scope only the user ID
await withUserId('user_456', async () => {
  await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Hello!' }],
  });
});
```

### Reading context

```typescript theme={"dark"}
import { getContext, getEffectiveContext, getGlobalContext } from 'moda-ai';

getContext();          // scoped (AsyncLocalStorage) context only
getGlobalContext();    // global context only
getEffectiveContext(); // combined; scoped values take precedence
```

<Warning>
  Set the conversation ID before the first model call of a session — from your session, thread, or run ID — so every call in the session lands in one conversation. Set `Moda.conversationId` or wrap the run in `withConversationId`.
</Warning>

## Manual capture with `withLLMCall`

For providers that are not auto-instrumented (direct HTTP calls, custom gateways), wrap the call with `Moda.withLLMCall({ vendor, type }, callback)`. Call `span.reportRequest` before the call and `span.reportResponse` after it; errors thrown in the callback propagate normally.

```typescript manual.ts theme={"dark"}
import { Moda } from 'moda-ai';

await Moda.init(process.env.MODA_API_KEY!);
Moda.conversationId = 'session_8f2a';

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

const result = await Moda.withLLMCall(
  { vendor: 'custom', type: 'chat' },
  async ({ span }) => {
    span.reportRequest({ model: 'my-model', messages });

    const response = await fetch('https://llm.example.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.PROVIDER_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ model: 'my-model', messages }),
    });
    const data = await response.json();

    span.reportResponse({
      model: data.model,
      usage: data.usage,
      completions: data.choices,
    });
    return data;
  },
);

await Moda.flush();
```

| Helper                                                              | Description                                                                                                                      |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `span.reportRequest({ model, messages, conversationId?, userId? })` | Record the request. Global or scoped context is used when `conversationId` and `userId` are omitted.                             |
| `span.reportResponse({ model?, usage?, completions? })`             | Record the response. `usage` accepts both `prompt_tokens`/`completion_tokens`/`total_tokens` and `input_tokens`/`output_tokens`. |
| `span.rawSpan`                                                      | The underlying OpenTelemetry span for custom attributes.                                                                         |

## Vercel AI SDK

Pass `Moda.getVercelAITelemetry()` as `experimental_telemetry` on AI SDK calls:

```typescript theme={"dark"}
import { Moda } from 'moda-ai';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

await Moda.init(process.env.MODA_API_KEY!);
Moda.conversationId = 'session_8f2a';

const result = await generateText({
  model: openai('gpt-4o'),
  prompt: 'Write a haiku about the ocean.',
  experimental_telemetry: Moda.getVercelAITelemetry(),
});

await Moda.flush();
```

<Note>
  Conversation and user context are snapshotted at the moment `getVercelAITelemetry()` is called. Set `Moda.conversationId` (or enter `withConversationId`) before calling it, not after.
</Note>

See [Vercel AI SDK](/ingestion/vercel-ai-sdk) for streaming, structured output, tools, and the full option list (`recordInputs`, `recordOutputs`, `functionId`, `metadata`).

## Using with an existing OpenTelemetry setup

If another SDK has already registered a global `TracerProvider` — for example Sentry v8+ or dd-trace — `Moda.init()` detects it and adds Moda's span processor to that provider instead of creating its own. Both pipelines receive the same spans, and `Moda.shutdown()` removes only Moda's processor, leaving the other SDK untouched.

Initialize the other SDK first, then Moda:

```typescript sentry.ts theme={"dark"}
import * as Sentry from '@sentry/node';
import { Moda } from 'moda-ai';
import OpenAI from 'openai';

// 1. Initialize Sentry first
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
});

// 2. Initialize Moda second — it attaches to Sentry's provider
await Moda.init(process.env.MODA_API_KEY!, { debug: true });
// debug log: [Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it

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

await Moda.flush();
await Moda.shutdown(); // removes only Moda's span processor
```

### Escape hatches

If the external provider samples out or filters Moda's spans, create a standalone Moda provider that bypasses it:

```typescript theme={"dark"}
import { Moda } from 'moda-ai';

Moda.createModaProvider({ apiKey: process.env.MODA_API_KEY! });
await Moda.registerInstrumentations();
```

`createModaProvider` accepts `apiKey`, `baseUrl`, `debug`, `batchSize`, and `flushInterval`. For fully custom OpenTelemetry setups, `createModaSpanProcessor(options)` returns a span processor (same options) that you can add to your own `TracerProvider`.

## Flushing and serverless

Spans are batched and exported every `flushInterval` milliseconds (default 5000). Unexported spans are lost when the process exits or a serverless runtime freezes, so:

* **Serverless functions** — `await Moda.flush()` before each invocation returns:

  ```typescript handler.ts theme={"dark"}
  import { Moda, withConversationId } from 'moda-ai';
  import OpenAI from 'openai';

  await Moda.init(process.env.MODA_API_KEY!);
  const client = new OpenAI();

  export async function handler(event: { sessionId: string; question: string }) {
    const result = await withConversationId(`session_${event.sessionId}`, () =>
      client.chat.completions.create({
        model: 'gpt-4o',
        messages: [{ role: 'user', content: event.question }],
      }),
    );

    await Moda.flush(); // spans are batched — flush before the runtime freezes
    return result.choices[0].message.content;
  }
  ```

* **Long-running servers** — flush and shut down on termination:

  ```typescript theme={"dark"}
  process.on('SIGTERM', async () => {
    await Moda.flush();
    await Moda.shutdown();
    process.exit(0);
  });
  ```

## Troubleshooting

| Symptom                                                      | Fix                                                                                                                                                                                          |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[Moda] API key is required` thrown at startup               | Pass the key explicitly: `await Moda.init(process.env.MODA_API_KEY!)`. `Moda.init()` does not read `MODA_API_KEY` from the environment.                                                      |
| Early calls are missing                                      | `await Moda.init(...)` before creating provider clients and before the first LLM call. Calls made before initialization completes are not captured.                                          |
| Nothing appears in the dashboard                             | `await Moda.flush()` before the process exits or the serverless invocation returns. Enable `debug: true` to see export activity.                                                             |
| Embedding calls are not captured                             | Expected: the Node.js SDK does not instrument OpenAI embeddings. Use the [Python SDK](/ingestion/python), [OpenTelemetry](/ingestion/opentelemetry), or the [HTTP API](/ingestion/http-api). |
| One session shows up as many conversations                   | Set `Moda.conversationId` or wrap the run in `withConversationId` before the first model call of the session.                                                                                |
| Spans missing in Moda while using Sentry or another OTel SDK | The external provider may sample out Moda's spans. Use `Moda.createModaProvider(...)` plus `Moda.registerInstrumentations()` to bypass it.                                                   |
| Context set inside a request leaks or disappears             | Use `withConversationId` / `withUserId` / `withContext` in concurrent handlers instead of the global setters; scoped context is isolated per async chain.                                    |

## Next steps

* [Vercel AI SDK](/ingestion/vercel-ai-sdk) — full guide for apps built on the `ai` package.
* [OpenRouter](/ingestion/providers/openrouter) — provider-specific setup details.
* [Reliability](/ingestion/reliability) — delivery guarantees, limits, and error handling.
* [Data model](/concepts/data-model) — how captured messages become conversations and signals.
