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

> Automatic LLM analytics with conversation threading

## Overview

The Moda Python SDK provides automatic instrumentation for your LLM applications with built-in conversation threading. Every LLM call is automatically tracked with a stable `moda.conversation_id` that groups multi-turn conversations together.

## Installation

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

This installs the core SDK along with instrumentation for OpenAI and Anthropic.

For other providers, install the corresponding instrumentation package:

```bash theme={"dark"}
pip install moda-claude-agent-sdk  # Claude Agent SDK (Claude Code, custom agents)
```

## Quick Start

```python theme={"dark"}
import moda
from openai import OpenAI

moda.init("YOUR_MODA_API_KEY")

# Set conversation ID for your session (recommended)
moda.conversation_id = "session_" + session_id

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

moda.flush()
```

<Note>
  Initialization is synchronous in Python. Call `moda.init(...)` once at startup before making LLM calls to ensure instrumentation is active.
</Note>

## Prompt Management

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

```python theme={"dark"}
rendered = moda.prompt("support.triage").render({
    "ticket": {"text": user_message},
})

moda.conversation_id = conversation_id
moda.user_id = user_id

client.chat.completions.create(
    model="gpt-4o",
    messages=rendered["messages"],
)
```

`moda.prompt(...).render(...)` attaches prompt metadata to OpenTelemetry spans, including `moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`, and `moda.prompt_version_id`.

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:

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

moda.init("YOUR_MODA_API_KEY")

# Property-style (recommended)
moda.conversation_id = "support_ticket_123"

client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "I need help"}]
)

client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "I need help"},
        {"role": "assistant", "content": "I'd be happy to help..."},
        {"role": "user", "content": "Order #12345"}
    ]
)

# Both calls share the same conversation_id
moda.conversation_id = None  # clear when done
```

### Setting User ID

Associate LLM calls with specific users for per-user analytics:

```python theme={"dark"}
moda.user_id = "user_12345"

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

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

### Scoped Context Managers

For scoped context (useful in request handlers):

```python theme={"dark"}
from moda import set_conversation_id, set_user_id

# Group specific calls under a custom conversation ID
with set_conversation_id("support-ticket-123"):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "I need help"}]
    )

# Attach user attribution
with set_user_id("user-456"):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hello"}]
    )
```

### Direct Setters

For cases where context managers aren't suitable (e.g., setting once at request start):

```python theme={"dark"}
from moda import set_conversation_id_value, set_user_id_value

# Set values that persist until cleared
set_conversation_id_value("session-123")
set_user_id_value("user-456")

# Make multiple calls...
response1 = client.chat.completions.create(...)
response2 = client.chat.completions.create(...)

# Clear when done
set_conversation_id_value(None)
set_user_id_value(None)
```

### Reading Current Context

```python theme={"dark"}
from moda import get_conversation_id, get_user_id

# Get current values
current_conv = get_conversation_id()
current_user = get_user_id()
print(f"Conversation: {current_conv}, User: {current_user}")
```

### Computing Conversation ID

You can manually compute a conversation ID from messages:

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

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
]

# Returns conv_[16-char-hash] based on system prompt + first user message
conv_id = compute_conversation_id(messages)
```

### Automatic Fallback

If you don't set a conversation ID, the SDK automatically computes a stable one based on:

* The first user message in the conversation
* The system prompt (if present)

This works well for prototyping and simple use cases:

```python theme={"dark"}
import moda
from openai import OpenAI

moda.init("YOUR_MODA_API_KEY")
client = OpenAI()

# Turn 1
messages = [{"role": "user", "content": "What is Python?"}]
response = client.chat.completions.create(model="gpt-4o", messages=messages)

# Turn 2 - automatically has the same conversation_id
messages.append({"role": "assistant", "content": response.choices[0].message.content})
messages.append({"role": "user", "content": "How do I install it?"})
response = client.chat.completions.create(model="gpt-4o", messages=messages)

# Both calls share the same conversation_id in Moda
```

For production applications, explicit conversation IDs are recommended as they provide:

* Predictable grouping regardless of message content
* Integration with your existing session/thread identifiers
* Easier debugging and correlation with your application logs

## Configuration

### Environment Variables

| Variable        | Description                                            |
| --------------- | ------------------------------------------------------ |
| `MODA_API_KEY`  | Your Moda API key                                      |
| `MODA_BASE_URL` | Custom ingest endpoint (optional)                      |
| `MODA_HEADERS`  | Custom headers sent with telemetry requests (optional) |

### Programmatic Configuration

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

moda.init(
    api_key="YOUR_MODA_API_KEY",
    app_name="my-chatbot",           # Optional: name your application
    endpoint="https://custom.endpoint/v1/traces"  # Optional: custom endpoint
)
```

### Advanced Configuration

The SDK supports additional configuration options:

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

moda.init(
    api_key="YOUR_MODA_API_KEY",
    app_name="my-app",

    # Enable/disable instrumentation
    enabled=True,

    # Send spans immediately instead of batching
    disable_batch=False,

    # Additional resource attributes
    resource_attributes={
        "deployment.environment": "production",
    },

    # Filter which providers are instrumented (see below)
    instruments=None,                 # Set of instruments to enable (None = all)
    block_instruments=None,           # Set of instruments to disable

    # Additional headers sent with telemetry requests
    headers={},
)
```

#### Filtering Instruments

Control which LLM providers are instrumented:

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

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

# Instrument everything except Anthropic
moda.init(
    api_key="YOUR_KEY",
    block_instruments={Instruments.ANTHROPIC}
)
```

The `Instruments` enum supports a wide range of providers and frameworks:

| Category         | Instruments                                                                                                                                                                         |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LLM Providers    | `OPENAI`, `ANTHROPIC`, `COHERE`, `MISTRAL`, `GROQ`, `OLLAMA`, `BEDROCK`, `VERTEXAI`, `TOGETHER`, `REPLICATE`, `SAGEMAKER`, `WATSONX`, `GOOGLE_GENERATIVEAI`, `WRITER`, `ALEPHALPHA` |
| Frameworks       | `LANGCHAIN`, `LLAMA_INDEX`, `HAYSTACK`, `CREWAI`, `OPENAI_AGENTS`, `MCP`, `AGNO`                                                                                                    |
| Vector Databases | `CHROMA`, `MILVUS`, `PINECONE`, `QDRANT`, `WEAVIATE`, `LANCEDB`, `MARQO`                                                                                                            |

## Supported Providers

The SDK automatically instruments:

| Provider         | Package                                      |
| ---------------- | -------------------------------------------- |
| OpenAI           | `moda-openai` (included with moda-ai)        |
| Anthropic        | `moda-anthropic` (included with moda-ai)     |
| Claude Agent SDK | `moda-claude-agent-sdk` (install separately) |

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

```python theme={"dark"}
import moda
from openai import OpenAI

moda.init("YOUR_MODA_API_KEY")

# Configure OpenAI client to use OpenRouter
openrouter = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="YOUR_OPENROUTER_API_KEY",
    default_headers={
        "HTTP-Referer": "https://your-app.com",  # Optional: for rankings
        "X-Title": "Your App Name",               # Optional: for rankings
    },
)

moda.conversation_id = "openrouter_session_123"

# Use any model available on OpenRouter
response = 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
gpt_response = 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>

## Verifying Data in Moda

After setting up, verify that data is flowing correctly:

1. Make a few LLM calls
2. Call `moda.flush()` to ensure data is sent
3. Check the Moda dashboard, conversations should appear within seconds
4. Verify that multi-turn conversations share the same conversation ID

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

### Core Functions

| Function                        | Description                   |
| ------------------------------- | ----------------------------- |
| `moda.init(api_key, **options)` | Initialize the SDK            |
| `moda.flush()`                  | Force flush pending telemetry |

### Context Properties

| Property               | Description                    |
| ---------------------- | ------------------------------ |
| `moda.conversation_id` | Get/set global conversation ID |
| `moda.user_id`         | Get/set global user ID         |

### Context Managers

| Function                  | Description                                |
| ------------------------- | ------------------------------------------ |
| `set_conversation_id(id)` | Context manager for scoped conversation ID |
| `set_user_id(id)`         | Context manager for scoped user ID         |

### Direct Functions

| Function                            | Description                                 |
| ----------------------------------- | ------------------------------------------- |
| `set_conversation_id_value(id)`     | Set conversation ID without context manager |
| `set_user_id_value(id)`             | Set user ID without context manager         |
| `get_conversation_id()`             | Get current conversation ID                 |
| `get_user_id()`                     | Get current user ID                         |
| `compute_conversation_id(messages)` | Compute ID from message history             |

## Troubleshooting

**Conversation IDs not grouping correctly?**

* Use explicit `moda.conversation_id` instead of relying on auto-compute
* If using auto-compute, ensure the first user message stays the same across turns
* Check if system prompts are changing between calls

**Data not appearing in Moda?**

* Call `moda.flush()` before your program exits
* Check that your API key is correct
* Verify network connectivity to the ingest endpoint

**Import errors?**

* Make sure you installed `moda-ai`
* Check that your Python version is 3.10 or higher
