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

> Install moda-ai to automatically capture OpenAI and Anthropic calls with conversation and user context.

The Moda SDK for Python (`moda-ai`) auto-instruments the OpenAI and Anthropic client libraries: after one `moda.init()` call, every LLM request, response, streamed completion, and tool call is captured and sent to Moda without changes to your provider code. This page covers installation, configuration, provider coverage, and conversation context.

## Prerequisites

* Python 3.10 or later
* A Moda API key, created at **Settings → Ingestion keys** (see [Authentication](/administration/authentication))
* The `openai` or `anthropic` package your app already uses

## Set up

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

    The package installs as `moda-ai` but imports as `moda`. Instrumentation for OpenAI and Anthropic is bundled — no extra packages are needed for these two providers.
  </Step>

  <Step title="Initialize and make a call">
    Call `moda.init()` once at startup, before your app makes LLM calls. Initialization is synchronous.

    ```python app.py theme={"dark"}
    import os

    import moda
    from openai import OpenAI

    moda.init(api_key=os.environ["MODA_API_KEY"])

    client = OpenAI()

    moda.conversation_id = "session_8f2a"
    moda.user_id = "user_1042"

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hello!"}],
    )
    print(response.choices[0].message.content)

    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>

## Configuration

### `moda.init()` options

```python theme={"dark"}
import moda

moda.init(
    api_key="YOUR_MODA_API_KEY",
    app_name="support-bot",
    resource_attributes={"deployment.environment": "staging"},
)
```

| Option                | Type               | Default                                           | Description                                                                                                              |
| --------------------- | ------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `api_key`             | `str`              | `None`                                            | Your Moda API key. Falls back to the `MODA_API_KEY` environment variable.                                                |
| `app_name`            | `str`              | script name                                       | Application name attached to all telemetry (OTel `service.name`).                                                        |
| `endpoint`            | `str`              | `https://moda-ingest.modas.workers.dev/v1/traces` | Ingestion endpoint URL.                                                                                                  |
| `debug`               | `bool`             | `False`                                           | Verbose logging; also disables batching so spans are sent immediately.                                                   |
| `enabled`             | `bool`             | `True`                                            | Set `False` to disable instrumentation entirely (no data is captured or sent).                                           |
| `disable_batch`       | `bool`             | `False`                                           | Send each span immediately instead of batching. Useful for short-lived scripts.                                          |
| `headers`             | `dict`             | `{}`                                              | Custom exporter headers. When set, they replace the default `Authorization: Bearer` header.                              |
| `resource_attributes` | `dict`             | `{}`                                              | Extra resource attributes. Set `deployment.environment` here to tag events as `development`, `staging`, or `production`. |
| `instruments`         | `set[Instruments]` | `None` (all)                                      | Allowlist of instrumentations to enable. See [Selecting instrumentations](#selecting-instrumentations).                  |
| `block_instruments`   | `set[Instruments]` | `None`                                            | Instrumentations to disable.                                                                                             |

<Accordion title="Advanced OpenTelemetry options">
  For custom OpenTelemetry pipelines, `moda.init()` also accepts `exporter` (a custom `SpanExporter`), `processor` (a `SpanProcessor` or list of them), `propagator`, `sampler`, and `span_postprocess_callback`. When `exporter` or `processor` is set, spans are exported through your components instead of Moda's default OTLP exporter.
</Accordion>

### Environment variables

| Variable        | Description                                                           |
| --------------- | --------------------------------------------------------------------- |
| `MODA_API_KEY`  | Your Moda API key.                                                    |
| `MODA_BASE_URL` | Overrides the ingestion endpoint.                                     |
| `MODA_HEADERS`  | Custom exporter headers; replaces the default `Authorization` header. |

<Warning>
  Environment variables take precedence over arguments passed to `moda.init()`. If `MODA_API_KEY` or `MODA_BASE_URL` is set in the process environment, it overrides the `api_key` and `endpoint` arguments.
</Warning>

## What gets instrumented

`moda.init()` patches the provider client libraries. Anything your app does through them is captured, including streaming.

**OpenAI** (`openai` package, sync and async clients):

* Chat Completions — `create` and `parse`, including streaming and tool calls
* Responses API — create, retrieve, cancel
* Legacy Completions
* Embeddings
* Image generation
* Assistants API (assistants, threads, runs)
* Realtime API — sessions opened through `beta.realtime.connect()`

**Anthropic** (`anthropic` package, sync and async clients):

* Messages — `create` and `stream`, including tool use and extended thinking
* Beta Messages
* Legacy Completions
* The `AnthropicBedrock` client, so Claude called through Amazon Bedrock is captured

### Provider detection for OpenAI-compatible endpoints

When you point the OpenAI client at a different `base_url`, the SDK detects the vendor from the URL and records it as the provider:

| `base_url` contains          | Provider recorded                                                                              |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `openai.azure.com`           | `Azure`                                                                                        |
| `openrouter.ai`              | `OpenRouter` (the `provider/` prefix is stripped from model names)                             |
| `amazonaws.com` or `bedrock` | `AWS` (cross-region model IDs such as `us.anthropic...` are normalized to the base model name) |
| `googleapis.com` or `vertex` | `Google`                                                                                       |
| anything else                | `openai`                                                                                       |

Other OpenAI-compatible endpoints (for example Groq) are still fully captured — requests, responses, and token usage — but are recorded with provider `openai`.

```python openrouter.py theme={"dark"}
import os

import moda
from openai import OpenAI

moda.init(api_key=os.environ["MODA_API_KEY"])

openrouter = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
)

moda.conversation_id = "session_8f2a"

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

moda.flush()
```

### Supported providers

| Provider                                           | Support                                                                                                                                |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI                                             | Supported — instrumentation bundled with `moda-ai`                                                                                     |
| Anthropic                                          | Supported — instrumentation bundled with `moda-ai`                                                                                     |
| Azure OpenAI                                       | Supported through the OpenAI client with an Azure `base_url`                                                                           |
| OpenRouter                                         | Supported through the OpenAI client with the OpenRouter `base_url` — see [OpenRouter](/ingestion/providers/openrouter)                 |
| Claude on Amazon Bedrock                           | Supported through the `AnthropicBedrock` client — see [Bedrock](/ingestion/providers/bedrock)                                          |
| Bedrock via `boto3`                                | Requires installing `opentelemetry-instrumentation-bedrock` separately                                                                 |
| Claude Agent SDK                                   | Supported through the separate `moda-claude-agent-sdk` package — see [Claude Agent SDK](/ingestion/claude-agent-sdk)                   |
| Vapi                                               | Supported via `moda.process_vapi_end_of_call_report` — see [Vapi](/ingestion/providers/vapi)                                           |
| Other frameworks (LangChain, LlamaIndex, and more) | The SDK activates them only if you install the matching `opentelemetry-instrumentation-<name>` package; these packages are not bundled |

## 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. Context is stored in `contextvars`, so it is safe in async code and threaded request handlers.

### Module properties

```python theme={"dark"}
import moda

moda.conversation_id = "support_ticket_123"
moda.user_id = "user_1042"

# ... make LLM calls ...

moda.conversation_id = None  # clear when done
moda.user_id = None
```

### Context managers

`set_conversation_id` and `set_user_id` scope the context to a block and restore the previous value on exit — use them in concurrent request handlers:

```python handler.py theme={"dark"}
import os

import moda
from openai import OpenAI

moda.init(api_key=os.environ["MODA_API_KEY"])
client = OpenAI()


def handle_request(session_id: str, user_id: str, question: str) -> str:
    with moda.set_conversation_id(f"session_{session_id}"):
        with moda.set_user_id(user_id):
            response = client.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": question}],
            )
    return response.choices[0].message.content
```

### Setters and getters

```python theme={"dark"}
from moda import (
    set_conversation_id_value,
    set_user_id_value,
    get_conversation_id,
    get_user_id,
)

set_conversation_id_value("session_123")   # persists until cleared
set_user_id_value("user_456")

print(get_conversation_id())  # "session_123"
print(get_user_id())          # "user_456"

set_conversation_id_value(None)  # clear
set_user_id_value(None)
```

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

## Selecting instrumentations

By default all instrumentations are enabled, but each activates only when its target package is installed. Restrict them with `instruments` (allowlist) or `block_instruments` (blocklist):

```python theme={"dark"}
import moda
from moda import Instruments

# Only instrument OpenAI
moda.init(api_key="YOUR_MODA_API_KEY", instruments={Instruments.OPENAI})
```

```python theme={"dark"}
import moda
from moda import Instruments

# Everything except the HTTP-library instrumentations
moda.init(
    api_key="YOUR_MODA_API_KEY",
    block_instruments={Instruments.REQUESTS, Instruments.URLLIB3},
)
```

Available `Instruments` values:

| Category         | Values                                                                                                                                                                                              |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LLM providers    | `OPENAI`, `ANTHROPIC`, `BEDROCK`, `COHERE`, `GOOGLE_GENERATIVEAI`, `GROQ`, `MISTRAL`, `OLLAMA`, `REPLICATE`, `SAGEMAKER`, `TOGETHER`, `VERTEXAI`, `WATSONX`, `WRITER`, `ALEPHALPHA`, `TRANSFORMERS` |
| Agent frameworks | `AGNO`, `CLAUDE_AGENT_SDK`, `CREWAI`, `HAYSTACK`, `LANGCHAIN`, `LLAMA_INDEX`, `MCP`, `OPENAI_AGENTS`                                                                                                |
| Vector stores    | `CHROMA`, `LANCEDB`, `MARQO`, `MILVUS`, `PINECONE`, `QDRANT`, `WEAVIATE`                                                                                                                            |
| Infrastructure   | `PYMYSQL`, `REDIS`, `REQUESTS`, `URLLIB3`                                                                                                                                                           |

Only the OpenAI and Anthropic instrumentation packages ship with `moda-ai`. All other values take effect only if you install the corresponding `opentelemetry-instrumentation-<name>` package yourself.

## Flushing

Spans are batched and exported in the background. Call `moda.flush()` to force-export pending spans:

* Always flush before a script or worker exits — unexported spans are lost when the process ends.
* In long-running servers, flushing on shutdown is enough; the batch exporter sends data continuously while the process runs.
* For very short-lived processes, pass `disable_batch=True` to `moda.init()` so each span is sent immediately.

There is no separate shutdown function in the Python SDK; `moda.flush()` is the only lifecycle call you need.

## Troubleshooting

| Symptom                                           | Fix                                                                                                                                                                |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Nothing appears in the dashboard                  | Call `moda.flush()` before the process exits. Confirm `moda.init()` runs before your first LLM call and that the API key is valid.                                 |
| `Error: Missing Moda API key` printed at startup  | Pass `api_key` to `moda.init()` or set `MODA_API_KEY`. The SDK prints this error and stays disabled rather than raising.                                           |
| An `api_key` or `endpoint` argument seems ignored | `MODA_API_KEY` and `MODA_BASE_URL` environment variables override init arguments. Unset the variable to use the argument.                                          |
| One session shows up as many conversations        | Set `moda.conversation_id` (or the `set_conversation_id` context manager) before the first model call of the session.                                              |
| A gateway provider shows as `openai`              | Expected: URL-based detection covers Azure, OpenRouter, AWS, and Google. Other OpenAI-compatible endpoints (for example Groq) are recorded with provider `openai`. |
| `ModuleNotFoundError: No module named 'moda'`     | Install `moda-ai` (the import name is `moda`) and confirm Python 3.10 or later.                                                                                    |

## Next steps

* [Claude Agent SDK](/ingestion/claude-agent-sdk) — instrument agents built on `claude-agent-sdk`.
* [OpenAI](/ingestion/providers/openai) and [Anthropic](/ingestion/providers/anthropic) — provider-specific setup details.
* [Data model](/concepts/data-model) — how captured messages become conversations and signals.
* [Reliability](/ingestion/reliability) — delivery guarantees and limits.
