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
This installs the core SDK along with instrumentation for OpenAI and Anthropic.
For other providers, install the corresponding instrumentation package:
pip install moda-claude-agent-sdk # Claude Agent SDK (Claude Code, custom agents)
Quick Start
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()
Initialization is synchronous in Python. Call moda.init(...) once at startup before making LLM calls to ensure instrumentation is active.
Prompt Management
Use moda prompts sync to version prompt files, then render them through the SDK:
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 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:
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:
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):
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):
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
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:
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:
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
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:
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:
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 provides access to multiple LLM providers through a unified API. Since OpenRouter uses an OpenAI-compatible interface, it works automatically with the Moda SDK:
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!"}]
)
OpenRouter model names use the format provider/model-name. See the OpenRouter models page for all available models.
Verifying Data in Moda
After setting up, verify that data is flowing correctly:
- Make a few LLM calls
- Call
moda.flush() to ensure data is sent
- Check the Moda dashboard, conversations should appear within seconds
- 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