# CLI Source: https://docs.moda.dev/data-api/cli Query your conversation analytics from the terminal ## Overview The Moda CLI gives you direct terminal access to your conversation analytics. Query dashboards, search conversations, investigate frustrations, and debug tool failures without leaving your shell. ## Prerequisites * Node.js 18+ * A Moda API key ([get one here](https://moda.dev/settings)) ## Installation ```bash theme={"dark"} npm install -g @moda-ai/cli ``` Or use without installing: ```bash theme={"dark"} npx -p @moda-ai/cli moda overview ``` ### Agent Skill Install the [Agent Skill](https://github.com/ModaLabs/moda-cli) so your AI assistant knows how to use the CLI: ```bash theme={"dark"} npx skills add ModaLabs/moda-cli ``` Set your API key: ```bash theme={"dark"} export MODA_API_KEY="moda_sk_your_key_here" ``` ## Commands ### overview Get a high-level dashboard of your conversation analytics. ```bash theme={"dark"} moda overview moda overview --days-back=30 ``` | Flag | Default | Description | | ------------- | ------- | ---------------------------------- | | `--days-back` | 7 | Number of days to look back (1-90) | Returns total conversations, trend percentage, frustration rate, tool failure summary, top clusters, and recent activity. ### clusters Browse the topic cluster hierarchy. ```bash theme={"dark"} moda clusters # Root-level categories moda clusters --parent-id=node-abc # Drill into a category moda clusters --time-range=7d # Filter by time ``` | Flag | Default | Description | | -------------- | ------- | -------------------------------------------- | | `--parent-id` | — | Parent node ID to drill down (omit for root) | | `--time-range` | `all` | `all`, `1h`, `3d`, `7d`, `24h`, `30d`, `90d` | ### cluster-conversations List conversations belonging to a specific cluster. ```bash theme={"dark"} moda cluster-conversations moda cluster-conversations node-abc --limit=20 ``` | Argument / Flag | Default | Description | | --------------- | ------------ | ------------------- | | `` | **required** | Cluster node ID | | `--limit` | 10 | Max results (1-100) | | `--offset` | 0 | Pagination offset | ### conversations Search and filter conversations. The `--world-state` and `--outcome` flags let you find conversations by what the agent has learned (world-state slots and durable user profile) and by how they went. ```bash theme={"dark"} moda conversations --search="error" moda conversations --time-range=24h --environment=production moda conversations --user-id=user_123 --limit=5 # Positive conversations that mention "enterprise" in their world state moda conversations --world-state="enterprise" --outcome=positive # Multiple keywords are ANDed; attach each result's world state moda conversations --world-state="refund,billing" --include-world-state ``` | Flag | Default | Description | | ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--search` | — | Full-text search in summaries | | `--cluster-id` | — | Filter by cluster node ID | | `--user-id` | — | Filter by user ID | | `--time-range` | `all` | `all`, `1h`, `3d`, `24h`, `7d`, `30d`, `90d` | | `--environment` | `all` | `all`, `development`, `staging`, `production` | | `--world-state` | — | Keyword(s) matched (case-insensitive substring) against world-state content — both segment slots and carried-in durable profile slots. Comma-separated terms are ANDed (all must appear). | | `--outcome` | `any` | `any`, `positive`, or `negative`. `positive` = a high blended segment score (graph PRM) and no frustration; `negative` = a low blended segment score or a frustrated conversation. | | `--include-world-state` | off | Attach each result's world-state summary (slots + segment summaries). | | `--limit` | 20 | Max results (1-100) | | `--offset` | 0 | Pagination offset | World-state search is keyword-based, not `key=value`: slot key naming isn't standardized across agents, so a keyword matches anywhere in a conversation's world-state content (slot keys, values, and durable profile). PRM scoring is segment-grain: `--outcome` judges conversations that have per-segment scores on a weighted blend of their segment closing scores (unit-count weighted, recency-decayed, lifecycle-status weighted). Conversations without segment scores — history from before scoring moved to segment grain — are judged on the legacy whole-conversation closing score. The step-scores endpoint (`GET /v1/data/conversations/:id/step-scores`) returns `segments[]` (per-terminal-segment score curves, newest scoring pass per segment) and `rollup` (the weighted blend, `null` until the segment lane has scored the conversation); its top-level `steps[]` curve comes from the retired whole-conversation lane and is populated only for those pre-cutover conversations. ### search Search message anchors with keyword, semantic, or hybrid retrieval. Results always include `conversation_id`, `message_index`, `snippet`, and `score`. Unit-backed tenants may also return `unit_id`, `content_block_index`, `block_type`, `tool_name`, and `chunk_index`. ```bash theme={"dark"} moda search "refund policy" moda search "database timeout" --mode=semantic --time-range=7d moda search "tool failed" --include-tool-io ``` | Argument / Flag | Default | Description | | ------------------- | ------------ | --------------------------------------------------------------------------------------- | | `` | **required** | Text to retrieve | | `--mode` | `hybrid` | `keyword`, `semantic`, or `hybrid` | | `--user-id` | — | Scope to one user | | `--time-range` | `all` | `all`, `1h`, `3d`, `24h`, `7d`, `30d`, `90d` | | `--include-tool-io` | off | Include substantive tool-use and tool-result units; degenerate content remains excluded | | `--limit` | 20 | Max results (1-100) | ### world-state Get a single conversation's world state — the structured slots, open threads, and the event log of how they were learned. ```bash theme={"dark"} moda world-state moda world-state --summary-only moda world-state --event-limit=500 ``` | Argument / Flag | Default | Description | | ------------------- | ------------ | ----------------------------------------------------- | | `` | **required** | Conversation ID | | `--summary-only` | off | Return segment summaries only (omit the event stream) | | `--event-limit` | 2000 | Max world-state events to return (1-5000) | ### context Get a window of messages from a conversation. ```bash theme={"dark"} moda context moda context --msg-index=5 moda context --window=3 ``` | Argument / Flag | Default | Description | | ------------------- | ------------ | -------------------------------------- | | `` | **required** | Conversation ID | | `--msg-index` | middle | Message index to center on (0-indexed) | | `--window` | 2 | Messages before and after center (1-5) | Returns messages centered around the specified index: window messages before + center message + window messages after. ### frustrations Get user frustration detections with inline evidence. ```bash theme={"dark"} moda frustrations moda frustrations --days-back=14 --limit=20 ``` | Flag | Default | Description | | ------------- | ------- | ---------------------------------- | | `--days-back` | 7 | Number of days to look back (1-90) | | `--limit` | 10 | Max results (1-20) | | `--offset` | 0 | Pagination offset | Each result includes frustration score, trajectory, primary cause, user quotes, signal breakdown, and inline conversation context. ### tool-failures Get tool failure overview. ```bash theme={"dark"} moda tool-failures moda tool-failures --days-back=30 ``` | Flag | Default | Description | | ------------- | ------- | ---------------------------------- | | `--days-back` | 7 | Number of days to look back (1-90) | ### tool-failure-detail Get detailed failure info for a specific tool. ```bash theme={"dark"} moda tool-failure-detail moda tool-failure-detail search --subtype=SEARCH_NO_RESULTS ``` | Argument / Flag | Default | Description | | --------------- | ------------ | ---------------------------------- | | `` | **required** | Name of the tool to inspect | | `--subtype` | — | Filter by error subtype | | `--days-back` | 7 | Number of days to look back (1-90) | | `--limit` | 5 | Max examples (1-20) | | `--offset` | 0 | Pagination offset | ### feedback Flag wrong or missing data (a cluster label that doesn't fit, a frustration that doesn't match its transcript, an empty result that shouldn't be) or a CLI quirk. Feedback is tenant-scoped and goes straight to the Moda team. ```bash theme={"dark"} moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id= moda feedback "search finds nothing for a conversation I can open" --category=missing_data --conversation-id= ``` | Argument / Flag | Default | Description | | ------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `""` | **required** | What looked wrong and why (max 4000 chars) | | `--category` | `other` | `bad_cluster_label`, `mismatched_frustration`, `missing_data`, `noisy_data`, `wrong_tool_failure`, `incorrect_loop`, `api_quirk`, `other` | | `--severity` | `low` | `info`, `low`, `medium`, `high` | | `--conversation-id` | — | Attach the conversation you were inspecting | | `--cluster-id` | — | Attach a cluster node id | | `--tool-name` | — | Attach a tool name | | `--run-id` | — | Attach a run id | The CLI nudges callers toward this command: successful agent envelopes carry a `meta.tip`, and interactive terminals print a one-line tip after each successful command (hide it with `MODA_CLI_TIPS=0`). ### prompts Manage code-first prompt versions from your repo. ```bash theme={"dark"} moda prompts init moda prompts status moda prompts diff moda prompts sync moda prompts sync --watch moda prompts promote support.triage --label=prod --version=pver_abc123 ``` | Command | Effect | | --------------------------- | -------------------------------------------------------------------- | | `moda prompts init` | Creates `.moda/prompts.yml` | | `moda prompts status` | Read-only local status | | `moda prompts diff` | Read-only local diff/status | | `moda prompts sync` | Uploads changed prompt versions and writes `.moda/prompts.lock.json` | | `moda prompts sync --watch` | Watches local prompt files and syncs when hashes change | | `moda prompts promote` | Moves a `dev`, `staging`, or `prod` label to a synced version | Runtime calls should use `Moda.prompt(...).render(...)` or `moda.prompt(...).render(...)` so spans include prompt metadata. See [Prompt Management](/prompt-management/overview). ## Common Workflows ### Daily Health Check ```bash theme={"dark"} moda overview --days-back=1 moda frustrations --days-back=1 --limit=5 moda tool-failures --days-back=1 ``` ### Debugging Frustrated Users ```bash theme={"dark"} # Find frustrated conversations moda frustrations --days-back=7 # Get context around the frustration point moda context --msg-index= ``` ### Investigating Tool Failures ```bash theme={"dark"} # Which tools are breaking? moda tool-failures # What errors? See examples with context moda tool-failure-detail ``` ### Exploring User Intents ```bash theme={"dark"} # What categories exist? moda clusters # Drill into a category moda clusters --parent-id=node-abc # See example conversations moda cluster-conversations node-abc --limit=10 ``` ## Output All commands output JSON to stdout. Pipe to `jq` for filtering: ```bash theme={"dark"} moda overview | jq '.frustrations' moda frustrations | jq '.frustrations[].primary_cause' moda tool-failures | jq '.tools[] | {name: .tool_name, failures: .failure_count}' ``` ## Environment Variables | Variable | Required | Default | Description | | --------------- | -------- | ------------------ | --------------------------------- | | `MODA_API_KEY` | Yes | — | Your Moda API key (`moda_sk_...`) | | `MODA_BASE_URL` | No | `https://moda.dev` | Base URL for the Data API | ## Troubleshooting Export your API key before running commands: ```bash theme={"dark"} export MODA_API_KEY="moda_sk_your_key_here" ``` * Verify your API key at [moda.dev/settings](https://moda.dev/settings) * Try `--days-back=30` for a wider time range * Ensure conversations are being ingested (see [Quickstart](/quickstart)) The CLI connects to `https://moda.dev` by default. Override with `MODA_BASE_URL` if needed. Install globally: `npm install -g @moda-ai/cli`. Or use npx: ```bash theme={"dark"} npx -p @moda-ai/cli moda overview ``` # Data API Source: https://docs.moda.dev/data-api/overview Programmatic access to your conversation analytics ## Overview The Moda Data API provides read access to your conversation analytics data. Use it to programmatically query conversations, monitor user frustration, investigate tool failures, and explore topic clusters. ## Access Methods Query your analytics directly from the terminal. Search conversations, investigate frustrations, debug tool failures. Direct HTTP access to all Data API endpoints. Authenticate with your Moda API key. ## Available Data | Category | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------ | | **Overview** | Dashboard KPIs, top clusters, recent activity | | **Conversations** | Search, filter, and retrieve conversation context | | **World State** | Search conversations by world-state/profile keywords; inspect a conversation's slots, threads, and event log | | **Clusters** | Browse topic cluster hierarchy | | **Frustrations** | User frustration detections with evidence | | **Tool Failures** | Tool failure overview and per-tool detail | ## Authentication All Data API access requires a Moda API key. Get yours from [Settings](https://moda.dev/settings). The API key is passed as an `x-api-key` header for REST requests, or as the `MODA_API_KEY` environment variable for the CLI. The Data API is read-only. To send conversation data to Moda, see the [Ingestion](/ingestion/overview) docs. # Use Cases Source: https://docs.moda.dev/data-api/use-cases Example workflows for the Moda Data API ## Monitoring User Frustration Use the frustration tools to identify and investigate unhappy users. **Workflow:** 1. Start with `moda_get_overview` to see the frustration rate 2. Use `moda_get_frustrations` to get specific frustrated conversations with evidence 3. Drill into a conversation with `moda_get_conversation_context` to see the full context **Example prompt for Claude Code:** > "Check if any users were frustrated in the last 7 days and show me the details" ## Investigating Tool Failures Identify which tools are failing and why. **Workflow:** 1. Call `moda_get_tool_failures` to see which tools have the most failures 2. Pick a tool and call `moda_get_tool_failure_detail` to see error subtypes and examples 3. Use the inline conversation context in each example to understand the failure scenario **Example prompt for Claude Code:** > "Which tools are failing the most? Show me examples of the top failing tool" ## Exploring Conversation Clusters Understand what topics your users are discussing. **Workflow:** 1. Call `moda_get_clusters` to see root-level topic categories 2. Drill into a category by passing its `node_id` as `parent_id` 3. Use `moda_get_cluster_conversations` to see specific conversations in a cluster **Example prompt for Claude Code:** > "What are the main topics users are asking about? Drill into the biggest cluster" ## Finding Task Clusters by Intent Search the latest task hierarchy with typo-tolerant and semantic retrieval: ```bash theme={"dark"} curl --get "https://moda.dev/api/v1/data/task-clusters/search" \ --header "x-api-key: $MODA_API_KEY" \ --data-urlencode "q=customers trying to get their money back" \ --data-urlencode "mode=hybrid" \ --data-urlencode "limit=20" ``` `mode=hybrid` combines boosted BM25, fuzzy label matching, and ANN search over each cluster's task centroid. Search candidates come from the completed run in the tenant's latest full-corpus Turbopuffer Atlas index; scoped harness/time-window runs are excluded. Moda then hydrates the canonical node and ancestor path from ClickHouse. Use `mode=fuzzy` for typo-tolerant lexical search only, or `mode=semantic` to prefer conceptual matches. A missing, incomplete, or unavailable Atlas index automatically falls back to bounded ClickHouse fuzzy and cosine search. Each match includes its ancestor path so clients can open the correct branch directly. ## Searching Conversations Find specific conversations by content, user, or time range. **Workflow:** 1. Use `moda_search_conversations` with a `search` term for full-text search 2. Filter by `environment` to focus on production vs. development 3. Use `moda_get_conversation_context` to read specific conversations **Example prompt for Claude Code:** > "Find all production conversations mentioning 'timeout errors' from the last 24 hours" ## Finding Conversations by World State The agent's **world state** captures what it has learned in a conversation — structured slots plus a durable per-user profile. Search across it with keywords to surface conversations that involve a particular fact, goal, or user attribute, and combine it with `--outcome` to focus on good or bad runs. ```bash theme={"dark"} # Positive (successful, non-frustrated) conversations whose world state mentions "enterprise" moda conversations --world-state="enterprise" --outcome=positive # AND multiple keywords, and attach the matched world state to each result moda conversations --world-state="refund,billing" --include-world-state # Drill into one conversation's slots, open threads, and how they were learned moda world-state ``` Keyword matching is a case-insensitive substring over the full world-state content (segment slots **and** carried-in durable profile), so you don't need to know the exact slot key — useful while slot naming is still evolving. **Example prompt for Claude Code:** > "Find positive conversations where the user is on an enterprise plan, then show me the world state for the first one" # Living Harness CI Source: https://docs.moda.dev/harness/ci-rescan Keep your synced harness graph current by re-scanning on every merge to main ## Overview `moda init` maps your repo's agents, prompts, and tools into a cited harness graph and syncs it to Moda. That map is a snapshot: as your team ships new agents, renames tools, or rewires prompts, the synced graph drifts from the code. The **living harness** workflow closes that gap. A small GitHub Actions workflow re-runs the hosted harness analysis on every merge to `main`, approves the refreshed report, and syncs the updated graph to Moda — so the harness Moda serves always matches what is deployed. Each rescan runs three CLI commands: ```bash theme={"dark"} moda harness analyze --remote --yes # re-analyze on Moda's servers (safe source snapshot upload) moda harness approve --yes # approve the refreshed report non-interactively moda harness sync --from-report # sync the cited graph to Moda ``` Analysis runs server-side on Moda's infrastructure under a Moda-held LLM key — the workflow needs no LLM credentials of its own, only your Moda API key. ## Setup ### 1. Add the repository secrets In your repo, add these [GitHub Actions secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions): * `MODA_API_KEY` (required) — a Moda API key (`moda_sk_...`) from [Settings → API Keys](https://moda.dev/settings) * `MODA_TENANT_ID` (optional) — pins the workspace when the key spans multiple organizations ### 2. Generate the workflow The easiest path is `moda init`, which offers the workflow as an optional setup-plan item, or the explicit flag: ```bash theme={"dark"} moda init --harness-rescan ``` To only rescan when agent-relevant paths change, scope it (this implies `--harness-rescan`): ```bash theme={"dark"} moda init --harness-rescan-paths='src/agents/**,prompts/**' ``` Either form writes `.github/workflows/moda-harness-rescan.yml`, pinned to the CLI version that generated it. ### 3. Or drop the workflow in by hand Copy this into `.github/workflows/moda-harness-rescan.yml`, replacing `` with the current `@moda-ai/cli` release (pin a version rather than `@latest` so CI runs a release you have reviewed): ```yaml .github/workflows/moda-harness-rescan.yml theme={"dark"} name: Moda Harness Rescan on: push: branches: [main] # Optional: scope rescans to agent-relevant paths, e.g. # paths: # - 'src/agents/**' workflow_dispatch: # Least privilege: this job reads the checked-out repo, uploads a safe # source snapshot to Moda over the API key, and syncs the refreshed harness # graph; it never writes back to the repository. permissions: contents: read # One in-flight rescan at a time; newer pushes supersede older runs. concurrency: group: moda-harness-rescan cancel-in-progress: true jobs: moda-harness-rescan: runs-on: ubuntu-latest env: MODA_API_KEY: ${{ secrets.MODA_API_KEY }} MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - name: Re-analyze the harness on Moda run: npx -y --package=@moda-ai/cli@ moda harness analyze --remote --yes - name: Approve the refreshed harness report run: npx -y --package=@moda-ai/cli@ moda harness approve --yes - name: Sync the harness graph to Moda run: npx -y --package=@moda-ai/cli@ moda harness sync --from-report ``` ## Behavior notes * **Merge-to-main only.** The workflow never runs on `pull_request`, so unmerged agent changes are never synced to your workspace. Use the `workflow_dispatch` trigger for an out-of-band rescan. * **Path scoping.** Add `on.push.paths` globs so only pushes touching agent-relevant code trigger a rescan. Pushes that touch none of the listed paths skip the workflow entirely. * **Superseding runs.** The `concurrency` group cancels an in-flight rescan when a newer push lands; the latest merge always wins. * **What leaves your CI.** `harness analyze --remote` uploads a safe source snapshot to Moda for analysis; the snapshot is ephemeral and deleted after the run. LLM credentials stay on Moda's side. * **Failure isolation.** The workflow only talks to Moda; it has read-only repo permissions and cannot push commits, so a failed rescan never blocks or mutates your codebase. Re-run it from the Actions tab or wait for the next merge. # Welcome to Moda Source: https://docs.moda.dev/index Discovery-first observability for AI agents. Cluster every production conversation, surface emerging intents, close the loop. ## What is Moda? Moda is observability for AI agents. Add a few lines of code to your existing OpenAI, Anthropic, or framework calls, and Moda automatically clusters every production conversation, detects behavior signals and tool failures, and surfaces the intents your agent has never seen before. Most agent failures aren't bugs. They're intents you didn't know your users had. Moda finds them first. Install the SDK, add two lines of code, and see your first cluster in the dashboard. ## Why use Moda Works with OpenAI, Anthropic, AWS Bedrock, OpenRouter, Azure, and Vercel AI SDK. Switch models without touching the integration. Every conversation is clustered into a hierarchical taxonomy that reorganizes itself as your traffic shifts. See the intents your agent has never seen before, ranked by growth, before any of them have a canonical answer. Same data the dashboard renders is available via the CLI and the Data API. ## How it works 1. **Install the Moda SDK** in your application. 2. **Initialize with your API key.** The SDK captures LLM calls in the background, no manual instrumentation. 3. **View clusters, behaviors, and failures** in the [Moda dashboard](https://app.moda.dev), or query them from your terminal. ```bash Python theme={"dark"} pip install moda-ai ``` ```bash Node.js theme={"dark"} npm install moda-ai ``` ```python Python theme={"dark"} import moda from openai import OpenAI moda.init("YOUR_MODA_API_KEY") client = OpenAI(api_key="YOUR_OPENAI_KEY") response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello, how are you?"}] ) moda.flush() ``` ```javascript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import OpenAI from 'openai'; Moda.init('YOUR_MODA_API_KEY'); const client = new OpenAI({ apiKey: 'YOUR_OPENAI_KEY' }); const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello, how are you?' }], }); await Moda.flush(); ``` ## Next steps Step-by-step guide to your first integration. All the ways to send data to Moda. Query every cluster, behavior signal, and failure from your terminal or your CI. What teams build on top of the Moda Data API. # Claude Code Skill Source: https://docs.moda.dev/ingestion/claude-code-skill Let AI integrate Moda into your codebase automatically ## What It Does The Moda Claude Code skill teaches AI assistants how to correctly integrate Moda's LLM observability SDK into any TypeScript or Python project. Instead of guessing at configuration, the AI detects your project stack and applies the right integration pattern automatically. The skill handles: * Detecting your framework and LLM provider * Installing the correct SDK (`moda-ai` for both npm and PyPI) * Placing `init()` at the right entry point for your framework * Choosing the correct conversation ID strategy * Adding `flush()` in cleanup hooks * Configuring Vercel AI SDK telemetry ## Prerequisites * [Claude Code](https://docs.anthropic.com/en/docs/claude-code) must be installed and available in your terminal * A Moda API key (get one from the [Moda dashboard](https://app.moda.dev)) ## Installation The skill files are available in the [Moda GitHub repository](https://github.com/modaflows/moda). You can install them per-project or globally. ```bash Per-Project (Recommended) theme={"dark"} # From your project root mkdir -p .claude/skills cp -r path/to/moda-setup/ .claude/skills/moda-setup/ ``` ```bash Global (All Projects) theme={"dark"} # Available in every Claude Code session mkdir -p ~/.claude/skills cp -r path/to/moda-setup/ ~/.claude/skills/moda-setup/ ``` ```bash From GitHub theme={"dark"} # Clone directly from the Moda repository git clone https://github.com/modaflows/moda.git /tmp/moda cp -r /tmp/moda/moda-setup/ .claude/skills/moda-setup/ rm -rf /tmp/moda ``` ## Usage Once installed, the skill activates automatically when you ask Claude Code to: * "Add Moda observability to this project" * "Integrate LLM analytics" * "Set up conversation tracking for my AI calls" * "Add tracing for OpenAI/Anthropic calls" You can also invoke it directly: ``` /moda-setup ``` ## What It Handles ### Framework Detection The skill inspects your project files to detect: | File | Detects | | ------------------------------------- | --------------------------------------- | | `package.json` | Node.js framework, AI SDK, LLM provider | | `requirements.txt` / `pyproject.toml` | Python framework, LLM provider | | `next.config.*` | Next.js | | `app/layout.tsx` | Next.js App Router | | `instrumentation.ts` | Existing instrumentation setup | ### Conversation Threading The skill automatically chooses the right conversation ID strategy: | Pattern | Strategy | | ------------------ | ------------------------------------------- | | Simple chatbot | Auto-detection (hash-based) | | Web server | Scoped context per request | | Agent framework | Explicit ID (auto doesn't work with agents) | | Multi-agent system | Shared parent task ID | Agent frameworks like LangChain, CrewAI, and AutoGPT require explicit conversation IDs. The automatic fallback does NOT work because each agent iteration constructs different messages. ### Correct Placement The skill knows where to place initialization for each framework: | Framework | `init()` Location | `flush()` Location | | -------------- | -------------------------- | ------------------------- | | Next.js | `instrumentation.ts` | Route handler `waitUntil` | | Express | Server startup | `SIGTERM` handler | | FastAPI | Lifespan event | Lifespan cleanup | | Flask | App factory / module level | `teardown_appcontext` | | Generic script | Top of file | Before exit | ## Supported Frameworks * Next.js (App Router + Vercel AI SDK) * Express * Generic Node.js scripts * Vercel AI SDK * OpenAI SDK * Anthropic SDK * FastAPI * Flask * LangChain * CrewAI * Generic Python scripts * OpenAI SDK * Anthropic SDK ## Example Session Here's what happens when you ask Claude Code to integrate Moda into a Next.js project: ``` You: Add Moda analytics to this project Claude: I'll integrate Moda into your Next.js + Vercel AI SDK project. 1. Installing the SDK: npm install moda-ai 2. Creating instrumentation.ts for initialization: [creates instrumentation.ts with Moda.init()] 3. Updating your route handler: [adds Moda.conversationId and experimental_telemetry to your existing chat route] 4. Adding MODA_API_KEY to .env.local: [adds the environment variable] Done! Your LLM calls will now be tracked in Moda with automatic conversation threading. Get your API key from the Moda dashboard and add it to .env.local. ``` ## Related Full Python SDK documentation Full TypeScript/Node.js SDK documentation Vercel AI SDK integration guide Get started with Moda in 5 minutes # Direct API Source: https://docs.moda.dev/ingestion/direct-api Send LLM conversation data directly to the Moda API across any channel ## Overview Send data directly to the Moda ingest API. This supports LLM conversations across any channel: * Standard chat completions (OpenAI, Anthropic, etc.) * Chat/messaging platforms (Slack, Discord, etc.) * Email conversations * Voice and video call transcripts * Tool/function calls within conversations * Custom integrations and languages without Moda SDK support All events are processed by Moda and appear in your dashboard alongside SDK-ingested data. ## Endpoint ``` POST https://moda-ingest.modas.workers.dev/v1/ingest ``` ## Authentication Include your Moda API key in the `Authorization` header: ```bash theme={"dark"} -H "Authorization: Bearer YOUR_MODA_API_KEY" ``` ## Request format You can set the `environment` at the request level to apply to all events: ```json theme={"dark"} { "environment": "staging", "events": [...] } ``` Send an array of events in the request body: ```json theme={"dark"} { "events": [ { "conversation_id": "conv-123", "role": "user", "message": "What is the capital of France?", "timestamp": "2024-01-15T10:30:00Z" }, { "conversation_id": "conv-123", "role": "assistant", "message": "The capital of France is Paris.", "timestamp": "2024-01-15T10:30:01Z", "input_tokens": 12, "output_tokens": 8, "model": "gpt-4o", "provider": "openai" } ] } ``` ## Event fields ### Required fields | Field | Type | Description | | ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- | | `conversation_id` | string | Unique ID for the conversation | | `role` | string | One of: `user`, `assistant`, `system` | | `message` or `content` | string (or array for `content`) | The message content. Provide either `message` (string) or `content` (string or array of content blocks). | ### Optional fields | Field | Type | Description | | ------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------- | | `timestamp` | string | ISO 8601 timestamp (defaults to now) | | `trace_id` | string | For linking related events (defaults to conversation\_id) | | `user_id` | string | Identifier for the end user | | `input_tokens` | number | Number of input/prompt tokens used | | `output_tokens` | number | Number of output/completion tokens used | | `reasoning_tokens` | number | Tokens used for extended thinking (Claude models) | | `model` | string | Model name (e.g., `gpt-4o`, `claude-3-opus`) | | `provider` | string | Provider name (e.g., `openai`, `anthropic`) | | `environment` | string | Environment name: `development`, `staging`, or `production` (defaults to `production`). Can also be set at the request level. | | `prompt_id` | string | Prompt template ID (for prompt management tracking) | | `prompt_name` | string | Prompt template name | | `prompt_version` | string | Prompt template version | | `content_blocks` | array | Structured content blocks (see below) | When using `content` as an array of content blocks, text is automatically extracted from `text`-type blocks for search and analytics. If both `message` and `content` are provided, `message` takes precedence. ### Content blocks For conversations with tool use, extended thinking, or images, include structured content blocks: | Block Type | Fields | Description | | ------------- | ------------------------------------ | ----------------------------------- | | `text` | `text` | Plain text content | | `thinking` | `text` | Model reasoning (extended thinking) | | `tool_use` | `tool_name`, `tool_use_id`, `input` | Tool/function call | | `tool_result` | `tool_use_id`, `content`, `is_error` | Tool response | | `image` | `source` | Image (base64 or URL) | Example with tool use: ```json theme={"dark"} { "events": [{ "conversation_id": "conv-123", "role": "assistant", "message": "Let me search for that.", "content_blocks": [ {"type": "text", "text": "Let me search for that."}, {"type": "tool_use", "tool_name": "web_search", "tool_use_id": "toolu_abc", "input": {"query": "latest news"}} ] }] } ``` Example with extended thinking: ```json theme={"dark"} { "events": [{ "conversation_id": "conv-123", "role": "assistant", "message": "The answer is 42.", "reasoning_tokens": 150, "output_tokens": 10, "content_blocks": [ {"type": "thinking", "text": "Let me think through this step by step..."}, {"type": "text", "text": "The answer is 42."} ] }] } ``` ## Channel-specific event formats LLM conversations happen across many channels beyond standard chat completions. Use the `messageType` field to send conversations from messaging platforms, email, voice calls, and to log tool invocations. You can mix these with standard events in the same request. ### Channel messages LLM-powered conversations in chat platforms or messaging systems. ```json theme={"dark"} { "messageType": "channel", "id": "msg-123", "conversationId": "thread-456", "message": "Hello, how can I help you today?", "role": "assistant", "userId": "user-789", "timestamp": "2025-01-04T12:00:00Z" } ``` | Field | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------- | | `messageType` | string | Yes | Must be `"channel"` | | `id` | string | Yes | Unique message ID | | `conversationId` | string | Yes | Thread/channel ID for grouping messages | | `message` | string | Yes | Message content | | `role` | string | Yes | One of: `user`, `assistant`, `system` | | `userId` | string | No | User identifier | | `timestamp` | string | No | ISO 8601 timestamp (defaults to now) | | `metadata` | object | No | Custom metadata | ### Tool calls Tool and function invocations made by the LLM during a conversation, with full request and response data. ```json theme={"dark"} { "messageType": "tool_call", "id": "tool-123", "conversationId": "thread-456", "name": "search_knowledge_base", "toolCallRequest": { "query": "refund policy", "limit": 5 }, "toolCallResponse": { "results": [ {"title": "Refund Policy", "content": "..."} ] } } ``` | Field | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------- | | `messageType` | string | Yes | Must be `"tool_call"` | | `id` | string | Yes | Unique tool call ID | | `conversationId` | string | Yes | Conversation ID for grouping | | `name` | string | Yes | Tool/function name | | `toolCallRequest` | any | No | Request payload (can be any JSON) | | `toolCallResponse` | any | No | Response payload (can be any JSON) | | `userId` | string | No | User identifier | | `timestamp` | string | No | ISO 8601 timestamp | | `metadata` | object | No | Custom metadata | ### Emails LLM-powered conversations happening over email, with threading support via `conversationId` and optional `inReplyTo` for referencing specific parent emails. ```json theme={"dark"} { "messageType": "email", "id": "email-123", "conversationId": "conv-456", "role": "user", "subject": "Re: Order #12345 - Shipping question", "body": "Thank you for reaching out. Your order is scheduled to arrive...", "inReplyTo": "parent-email-message-id", "from": "support@example.com", "to": ["customer@example.com"] } ``` | Field | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------------------- | | `messageType` | string | Yes | Must be `"email"` | | `id` | string | Yes | Unique email ID | | `conversationId` | string | Yes | Thread/conversation ID (groups emails together) | | `role` | string | Yes | `"user"` (inbound) or `"assistant"` (outbound) | | `body` | string | Yes | Email body content | | `inReplyTo` | string | No | Email thread reference (e.g. parent email Message-ID) | | `subject` | string | No | Email subject line | | `from` | string | No | Sender email address | | `to` | array | No | Recipient email addresses | | `userId` | string | No | User identifier | | `timestamp` | string | No | ISO 8601 timestamp | The `conversationId` field groups emails into conversations, consistent with all other event types. Use `inReplyTo` optionally to reference specific parent emails within a conversation (e.g. when there are multiple email threads within the same conversation). ### Call transcripts LLM-powered conversations happening over phone or video calls, with full speaker turn transcripts. ```json theme={"dark"} { "messageType": "call", "id": "call-123", "conversationId": "call-session-456", "transcript": [ {"role": "user", "content": "Hi, I'm having trouble with my account."}, {"role": "assistant", "content": "I'd be happy to help. Can you tell me more about the issue?"}, {"role": "user", "content": "I can't log in after resetting my password."} ], "duration": 180, "callType": "phone" } ``` | Field | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------- | | `messageType` | string | Yes | Must be `"call"` | | `id` | string | Yes | Unique call ID | | `conversationId` | string | Yes | Call session ID | | `transcript` | array | Yes | Array of transcript entries (see below) | | `duration` | number | No | Call duration in seconds | | `callType` | string | No | One of: `phone`, `video`, `voice` | | `userId` | string | No | User identifier | | `timestamp` | string | No | ISO 8601 timestamp | | `metadata` | object | No | Custom metadata | **Transcript entry format:** | Field | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------- | | `role` | string | Yes | Speaker role (e.g., `user`, `assistant`, `customer`, `agent`) | | `content` | string | Yes | What was said | | `timestamp` | string | No | When this was said | Call transcripts are stored as a single record containing the full transcript. The complete transcript is searchable in the Moda dashboard, and the structured speaker turns are preserved for detailed analysis. ### Mixed batch example You can mix standard events and channel-specific events in the same request: ```json theme={"dark"} { "events": [ { "conversation_id": "conv-123", "role": "user", "message": "What is the weather?" }, { "messageType": "channel", "id": "msg-001", "conversationId": "support-thread-456", "message": "I need help with my subscription", "role": "user", "userId": "customer-789" }, { "messageType": "tool_call", "id": "tool-001", "conversationId": "support-thread-456", "name": "get_subscription", "toolCallRequest": {"user_id": "customer-789"}, "toolCallResponse": {"plan": "pro", "status": "active"} } ] } ``` Each event is validated individually. Standard events (without `messageType`) use the fields documented above, while channel-specific events are validated according to their `messageType` schema. ## Example ```bash cURL theme={"dark"} curl https://moda-ingest.modas.workers.dev/v1/ingest \ -H "Authorization: Bearer YOUR_MODA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "conversation_id": "conv-abc123", "role": "user", "message": "Hello, how are you?" }, { "conversation_id": "conv-abc123", "role": "assistant", "message": "I am doing well, thank you for asking!", "input_tokens": 8, "output_tokens": 12, "model": "gpt-4o", "provider": "openai" } ] }' ``` ```python Python theme={"dark"} import requests response = requests.post( "https://moda-ingest.modas.workers.dev/v1/ingest", headers={ "Authorization": "Bearer YOUR_MODA_API_KEY", "Content-Type": "application/json" }, json={ "events": [ { "conversation_id": "conv-abc123", "role": "user", "message": "Hello, how are you?" }, { "conversation_id": "conv-abc123", "role": "assistant", "message": "I am doing well, thank you for asking!", "input_tokens": 8, "output_tokens": 12, "model": "gpt-4o", "provider": "openai" } ] } ) print(response.json()) ``` ```javascript Node.js theme={"dark"} const response = await fetch('https://moda-ingest.modas.workers.dev/v1/ingest', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_MODA_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ events: [ { conversation_id: 'conv-abc123', role: 'user', message: 'Hello, how are you?' }, { conversation_id: 'conv-abc123', role: 'assistant', message: 'I am doing well, thank you for asking!', input_tokens: 8, output_tokens: 12, model: 'gpt-4o', provider: 'openai' } ] }) }); console.log(await response.json()); ``` ## Response ### Success response ```json theme={"dark"} { "success": true, "count": 2, "requestId": "550e8400-e29b-41d4-a716-446655440000" } ``` When the request includes channel-specific events (with `messageType`), the response includes a `details` breakdown: ```json theme={"dark"} { "success": true, "count": 4, "requestId": "550e8400-e29b-41d4-a716-446655440000", "details": { "channel": 2, "tool_call": 1, "email": 0, "call": 1, "call_transcript_messages": 0 } } ``` ### Error response ```json theme={"dark"} { "success": false, "count": 0, "message": "Invalid or missing API key", "requestId": "550e8400-e29b-41d4-a716-446655440000", "retryable": false } ``` | Field | Type | Description | | ----------- | ------- | ---------------------------------------------------------------------------------------- | | `success` | boolean | Whether the request succeeded | | `count` | number | Number of events processed | | `requestId` | string | Unique request ID for debugging | | `message` | string | Error message (on failure) | | `retryable` | boolean | Whether the error is temporary and should be retried | | `details` | object | Breakdown by channel event type (only present when channel-specific events are included) | ## Batch limits | Limit | Value | | ---------------------- | ------ | | Max events per request | 1,000 | | Max message size | 100 KB | | Max request size | 5 MB | ## Error handling | Status | Meaning | Retryable | | ------ | ------------------------------- | --------- | | 200 | Success | - | | 400 | Invalid request format | No | | 401 | Invalid or missing API key | No | | 413 | Request too large | No | | 503 | Service temporarily unavailable | Yes | For 503 errors, use exponential backoff when retrying. Start with 1 second and double each retry, up to a maximum of 30 seconds. # Moda Python SDK Source: https://docs.moda.dev/ingestion/moda-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() ``` 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: ```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!"}] ) ``` OpenRouter model names use the format `provider/model-name`. See the [OpenRouter models page](https://openrouter.ai/models) for all available models. ## 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 # Moda Node.js SDK Source: https://docs.moda.dev/ingestion/moda-sdk-node 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(); ``` `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. ## 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(); ``` **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. ### 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(), }); ``` For full setup instructions, streaming, structured output, tool use, and provider-specific examples, see the dedicated [Vercel AI SDK](/ingestion/vercel-ai-sdk) guide. ## 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!' }] }); ``` OpenRouter model names use the format `provider/model-name`. See the [OpenRouter models page](https://openrouter.ai/models) for all available models. ## 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 ``` With debug mode enabled, you should see a log message confirming that Moda detected your existing tracing setup and is sharing it. ### 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) # Ingestion Overview Source: https://docs.moda.dev/ingestion/overview Send your LLM telemetry data to Moda ## What is ingestion? Ingestion allows you to send LLM usage data to Moda from your existing applications. This is useful if you: * Already have an application making direct calls to LLM providers * Want to add analytics without changing how you call the APIs ## Ways to ingest data **Recommended for Python.** Official SDK with automatic conversation threading for OpenAI and Anthropic. **Recommended for Node.js/TypeScript.** Official SDK with automatic conversation threading. Send LLM conversation data directly to the Moda API. Supports conversations across chat, email, voice, and more. Provider-specific setup guides for OpenAI, Anthropic, OpenRouter, Azure, and Bedrock. ## How it works ``` Your App ──> LLM Provider (OpenAI, Anthropic, etc.) │ │ Moda SDK captures telemetry │ ▼ Moda Ingestion API │ ▼ Moda Dashboard ``` 1. Your app makes calls to LLM providers as normal 2. The Moda SDK captures telemetry in the background 3. Telemetry is sent to Moda's ingestion API 4. Moda validates your API key and processes the data 5. View insights and analytics in the Moda dashboard ## What data is captured? | Field | Description | | ------------------ | ----------------------------------------------------------- | | Conversation ID | Groups related messages together | | User message | What the user asked | | Assistant response | What the AI replied | | Model | Which model was used | | Provider | LLM provider name (e.g., openai, anthropic) | | Timestamp | When the interaction happened | | Token usage | Input, output, and total tokens consumed | | Reasoning tokens | Tokens used for extended thinking (e.g., Claude) | | Content blocks | Structured content including tool use, thinking, and images | | User ID | User identifier for per-user analytics | | Environment | Deployment environment (development, staging, production) | | Prompt tracking | Prompt template ID, name, and version | ## Endpoints All ingestion endpoints are hosted at `https://moda-ingest.modas.workers.dev`. | Endpoint | Use Case | | ----------------- | ------------------------------------------------------------------------------------------- | | `POST /v1/traces` | Used automatically by the Moda SDK | | `POST /v1/ingest` | Direct API integrations (conversations across chat, email, voice, and standard completions) | ## Batch Limits These limits apply across all ingestion endpoints: | Limit | Value | | ---------------------- | ------ | | Max events per request | 1,000 | | Max message size | 100 KB | | Max request size | 5 MB | ## Privacy Moda stores conversation content to provide analytics. Make sure this aligns with your privacy policy and data handling requirements. Do not send sensitive personal information (like passwords or credit card numbers) through LLM calls that are being logged. # Anthropic Source: https://docs.moda.dev/ingestion/providers/anthropic Using Moda with Anthropic Claude ## Overview Moda automatically tracks your Anthropic Claude API calls. Messages, streaming, extended thinking, tool use, and content blocks are all captured with no additional code required. ## Setup ```bash Python theme={"dark"} pip install moda-ai anthropic ``` ```bash Node.js theme={"dark"} npm install moda-ai @anthropic-ai/sdk ``` ```python Python theme={"dark"} import moda from anthropic import Anthropic moda.init("YOUR_MODA_API_KEY") client = Anthropic() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}] ) moda.flush() ``` ```typescript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import Anthropic from '@anthropic-ai/sdk'; await Moda.init('YOUR_MODA_API_KEY'); const client = new Anthropic(); const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }] }); await Moda.flush(); ``` ## Supported Features | Feature | Captured | | ----------------------------- | ----------------------------------------------------------- | | Messages | Yes | | Streaming (`messages.stream`) | Yes | | Extended thinking | Yes (captured as `thinking` content blocks) | | Tool use | Yes (captured as `tool_use` / `tool_result` content blocks) | | System prompts | Yes | | Content blocks (text, image) | Yes | | Token usage | Yes (input, output, reasoning tokens) | | Model name | Yes (request and response) | ## Extended Thinking When using extended thinking, reasoning tokens and thinking content blocks are automatically captured. ```python Python theme={"dark"} response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=16000, thinking={ "type": "enabled", "budget_tokens": 10000, }, messages=[{"role": "user", "content": "Explain quantum computing"}] ) ``` ```typescript Node.js theme={"dark"} const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 16000, thinking: { type: 'enabled', budget_tokens: 10000, }, messages: [{ role: 'user', content: 'Explain quantum computing' }] }); ``` Moda captures: * The thinking content as `thinking` content blocks * Reasoning token count for cost tracking * The final text response ## Tool Use Anthropic tool use is automatically captured with full request/response details: ```python Python theme={"dark"} response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[{ "name": "get_weather", "description": "Get the weather for a location", "input_schema": { "type": "object", "properties": { "location": {"type": "string"} } } }], messages=[{"role": "user", "content": "What's the weather in London?"}] ) ``` ```typescript Node.js theme={"dark"} const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, tools: [{ name: 'get_weather', description: 'Get the weather for a location', input_schema: { type: 'object', properties: { location: { type: 'string' } } } }], messages: [{ role: 'user', content: "What's the weather in London?" }] }); ``` ## Streaming Streaming is fully supported: ```python Python theme={"dark"} with client.messages.stream( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Count to 5"}] ) as stream: for text in stream.text_stream: print(text, end="") ``` ```typescript Node.js theme={"dark"} const stream = client.messages.stream({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'Count to 5' }] }); for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { process.stdout.write(event.delta.text); } } ``` ## Troubleshooting **Data not appearing?** * Ensure `moda.init()` is called before creating the Anthropic client * Call `moda.flush()` (Python) or `await Moda.flush()` (Node.js) before exit * Verify your API key is correct **Extended thinking not captured?** * Ensure you're using a model that supports extended thinking * Check that the `thinking` parameter is properly configured For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) or [Node.js SDK](/ingestion/moda-sdk-node) guides. # Azure OpenAI Source: https://docs.moda.dev/ingestion/providers/azure-openai Using Moda with Azure OpenAI Service ## Overview Azure OpenAI Service provides access to OpenAI models hosted on Azure infrastructure. Since the Azure OpenAI client extends the standard OpenAI client, Moda automatically tracks your Azure OpenAI calls with the same setup as standard OpenAI. ## Setup with Moda SDK ```python Python theme={"dark"} import moda from openai import AzureOpenAI moda.init("YOUR_MODA_API_KEY") client = AzureOpenAI( api_key="YOUR_AZURE_API_KEY", api_version="2024-02-01", azure_endpoint="https://your-resource.openai.azure.com", ) moda.conversation_id = "session_123" response = client.chat.completions.create( model="gpt-4o", # This is your deployment name messages=[{"role": "user", "content": "Hello!"}] ) moda.flush() ``` ```typescript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import { AzureOpenAI } from 'openai'; await Moda.init('YOUR_MODA_API_KEY'); const client = new AzureOpenAI({ apiKey: 'YOUR_AZURE_API_KEY', apiVersion: '2024-02-01', endpoint: 'https://your-resource.openai.azure.com', }); Moda.conversationId = 'session_123'; const response = await client.chat.completions.create({ model: 'gpt-4o', // This is your deployment name messages: [{ role: 'user', content: 'Hello!' }] }); await Moda.flush(); ``` The `model` parameter for Azure OpenAI is your **deployment name**, not the model name. Moda captures whatever value you pass as the model identifier. ## Azure-Specific Configuration | Parameter | Description | | ----------------------------- | ----------------------------------------- | | `azure_endpoint` / `endpoint` | Your Azure OpenAI resource URL | | `api_version` / `apiVersion` | Azure API version (e.g., `2024-02-01`) | | `api_key` / `apiKey` | Azure OpenAI API key | | `model` | Your deployment name (not the model name) | ## Supported Features | Feature | Captured | | --------------------------- | -------- | | Chat completions | Yes | | Streaming | Yes | | Function calling / tool use | Yes | | Embeddings | Yes | | Token usage | Yes | ## Troubleshooting **Model name shows deployment name instead of actual model?** * This is expected with Azure OpenAI. The `model` field will contain your deployment name. **Authentication errors?** * Verify your `api_key`, `api_version`, and `azure_endpoint` are correct * Ensure the deployment exists in your Azure OpenAI resource For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) or [Node.js SDK](/ingestion/moda-sdk-node) guides. # AWS Bedrock Source: https://docs.moda.dev/ingestion/providers/bedrock Using Moda with AWS Bedrock ## Overview AWS Bedrock provides access to foundation models from multiple providers (Anthropic Claude, Meta Llama, Mistral, etc.) through AWS infrastructure. Since the Moda SDK does not yet have automatic Bedrock support, use the [Direct API](/ingestion/direct-api) to send your Bedrock conversation data to Moda. ## Setup with Direct API Make a Bedrock call as normal, then send the conversation data to Moda via the Direct API: ```python Python theme={"dark"} import boto3 import json import requests # 1. Make the Bedrock call client = boto3.client("bedrock-runtime", region_name="us-east-1") response = client.invoke_model( modelId="anthropic.claude-3-sonnet-20240229-v1:0", body=json.dumps({ "anthropic_version": "bedrock-2023-05-31", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }) ) result = json.loads(response["body"].read()) assistant_message = result["content"][0]["text"] # 2. Send to Moda requests.post( "https://moda-ingest.modas.workers.dev/v1/ingest", headers={ "Authorization": "Bearer YOUR_MODA_API_KEY", "Content-Type": "application/json" }, json={ "events": [ { "conversation_id": "session-123", "role": "user", "message": "Hello!" }, { "conversation_id": "session-123", "role": "assistant", "message": assistant_message, "model": "anthropic.claude-3-sonnet-20240229-v1:0", "provider": "bedrock", "input_tokens": result.get("usage", {}).get("input_tokens"), "output_tokens": result.get("usage", {}).get("output_tokens") } ] } ) ``` ```typescript Node.js theme={"dark"} import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime'; const client = new BedrockRuntimeClient({ region: 'us-east-1' }); // 1. Make the Bedrock call const response = await client.send(new InvokeModelCommand({ modelId: 'anthropic.claude-3-sonnet-20240229-v1:0', body: JSON.stringify({ anthropic_version: 'bedrock-2023-05-31', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }] }) })); const result = JSON.parse(new TextDecoder().decode(response.body)); const assistantMessage = result.content[0].text; // 2. Send to Moda await fetch('https://moda-ingest.modas.workers.dev/v1/ingest', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_MODA_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ events: [ { conversation_id: 'session-123', role: 'user', message: 'Hello!', }, { conversation_id: 'session-123', role: 'assistant', message: assistantMessage, model: 'anthropic.claude-3-sonnet-20240229-v1:0', provider: 'bedrock', input_tokens: result.usage?.input_tokens, output_tokens: result.usage?.output_tokens, } ] }) }); ``` ## AWS Credentials Bedrock uses standard AWS authentication. Configure credentials via: | Method | Description | | --------------------- | ---------------------------------------------------------- | | Environment variables | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` | | AWS credentials file | `~/.aws/credentials` | | IAM roles | Recommended for production (EC2, ECS, Lambda) | | AWS SSO | For development environments | ## Supported Models Bedrock provides access to models from multiple providers. Common models include: | Provider | Model ID | | --------- | ----------------------------------------- | | Anthropic | `anthropic.claude-3-sonnet-20240229-v1:0` | | Anthropic | `anthropic.claude-3-haiku-20240307-v1:0` | | Meta | `meta.llama3-70b-instruct-v1:0` | | Mistral | `mistral.mistral-large-2402-v1:0` | See the [AWS Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) for a full list of available models. ## Supported Features | Feature | Captured | | ---------------------- | -------------------------------- | | Invoke model | Yes | | Token usage | Yes (when returned by the model) | | Model name | Yes | | Conversation threading | Yes (via `conversation_id`) | ## Troubleshooting **Data not appearing?** * Verify your Moda API key is correct * Check that your AWS credentials are valid * Ensure you're sending events to `https://moda-ingest.modas.workers.dev/v1/ingest` **Token usage missing?** * Not all Bedrock models return token usage data. Check the model's response format. For full Direct API documentation, see the [Direct API guide](/ingestion/direct-api). # Claude Agent SDK Source: https://docs.moda.dev/ingestion/providers/claude-agent-sdk Using Moda with the Claude Agent SDK ## Overview Moda tracks your Claude Agent SDK sessions, including Claude Code and custom agents built with `claude-agent-sdk`. Token usage, tool calls, agent turns, and session metadata are all captured automatically. ## Why a Separate Package? The Claude Agent SDK spawns Claude as a subprocess rather than making direct Anthropic API calls. This means standard Anthropic API instrumentation (`moda-anthropic`) does not fire. The `moda-claude-agent-sdk` package provides a dedicated instrumentor that wraps `ClaudeSDKClient` to capture agent-level spans. ## Setup ```bash theme={"dark"} pip install moda-ai moda-claude-agent-sdk claude-agent-sdk ``` The Claude Agent SDK is Python-only. Node.js is not currently supported. ## Quick Start ```python theme={"dark"} import moda import asyncio from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions # Initialize Moda (automatically instruments claude-agent-sdk) moda.init("YOUR_MODA_API_KEY") # Set conversation and user context (recommended) moda.conversation_id = "session_123" moda.user_id = "user_456" async def main(): options = ClaudeAgentOptions(model="claude-sonnet-4-20250514") async with ClaudeSDKClient(options=options) as client: await client.query("What is the capital of France?") async for msg in client.receive_response(): print(msg) asyncio.run(main()) moda.flush() ``` ## Supported Features | Feature | Captured | | ------------------- | -------------------------------------------------- | | Agent sessions | Yes | | Streaming responses | Yes | | Tool use tracking | Yes (count of tool calls across turns) | | Token usage | Yes (input, output, total, including cache tokens) | | Model name | Yes (request and response) | | Session ID | Yes | | Turn count | Yes | ## Data Captured | Attribute | Description | | ------------------------------ | ------------------------------------------ | | `gen_ai.request.model` | Requested model name | | `gen_ai.response.model` | Actual model used in response | | `gen_ai.usage.input_tokens` | Input token count (including cache tokens) | | `gen_ai.usage.output_tokens` | Output token count | | `llm.usage.total_tokens` | Total token count | | `claude_agent.num_turns` | Number of conversation turns | | `claude_agent.session_id` | Agent session identifier | | `claude_agent.tool_call_count` | Total tool calls across all turns | | `moda.conversation_id` | Conversation ID (when set) | | `moda.user_id` | User ID (when set) | ## Troubleshooting **Data not appearing?** * Make sure you installed `moda-claude-agent-sdk` separately, it is not included with `moda-ai` * Ensure `moda.init()` is called before creating the `ClaudeSDKClient` * Call `moda.flush()` before your program exits **Token counts missing?** * Token usage comes from the `ResultMessage` at the end of the stream. Make sure you consume the full async generator from `receive_response()` **No spans generated?** * Verify that `claude-agent-sdk` is installed and importable * Check that the instrumentor is active: `moda.init()` automatically enables it when the package is installed For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) guide. # Coding agent telemetry (Claude Code, Codex & Cursor) Source: https://docs.moda.dev/ingestion/providers/coding-agents Ingest OTLP logs, traces, metrics, and hook payloads from Claude Code, Codex, and Cursor into Moda ## Overview Moda ingests first-class telemetry from coding-agent runtimes. The primary signal is OTLP logs, with OTLP traces and metrics accepted on the same provider-prefixed route family. Customers can also POST JSON payloads from custom hook scripts for richer per-event signals. Together these signals power session, tool, skill, and cost analytics on top of Claude Code, Codex, and Cursor without modifying agent code or wrapping the runtime in a custom SDK. ## Endpoints | Endpoint | Purpose | | ------------------------------ | ----------------------------------------------------------- | | `POST /v1/otel/claude/logs` | Primary Claude Code runtime event path (OTLP logs). | | `POST /v1/otel/claude/traces` | Claude Code OTLP trace spans. | | `POST /v1/otel/claude/metrics` | Claude Code OTLP metrics. | | `POST /v1/otel/claude/hooks` | Claude Code hook JSON payloads from installed hook scripts. | | `POST /v1/otel/codex/logs` | Primary Codex runtime event path (OTLP logs). | | `POST /v1/otel/codex/traces` | Codex OTLP trace spans. | | `POST /v1/otel/codex/metrics` | Codex OTLP metrics. | | `POST /v1/otel/codex/hooks` | Codex hook JSON payloads from installed hook scripts. | | `POST /v1/otel/cursor/logs` | Primary Cursor runtime event path (OTLP logs). | | `POST /v1/otel/cursor/traces` | Cursor OTLP trace spans. | | `POST /v1/otel/cursor/metrics` | Cursor OTLP metrics. | | `POST /v1/otel/cursor/hooks` | Cursor hook JSON payloads from installed hook scripts. | All routes authenticate with your Moda API key via `Authorization: Bearer `. OTLP routes accept JSON (`Content-Type: application/json`) or protobuf (`Content-Type: application/x-protobuf`) bodies. Hook routes accept JSON. Metric envelopes are accepted and persisted to `events_raw` (Layer 0) as raw payloads. Normalized rows are NOT yet written to `coding_agent_events` for metrics — only logs, traces, and hook events produce normalized rows in the first release. Token-usage metrics in particular are stored raw and not yet exposed in `coding_agent_events`-backed dashboards. ## Claude Code setup ### Recommended: `moda init` Coding-agent CLI hook install (`moda hooks install` / init-time hook wiring) is **temporarily disabled** in the published CLI. Ingest still accepts OTLP from Claude Code / Codex / Cursor when you configure the exporter yourself (see Manual below). Previously installed hooks continue to work. ```bash theme={"dark"} # Manual OTEL env (recommended while hooks install is disabled): export CLAUDE_CODE_ENABLE_TELEMETRY=1 # ... see Manual section for the full exporter block ``` When hook install is re-enabled, `moda init` will again write the telemetry environment block — including endpoints and your `Authorization: Bearer` header — into `~/.claude/settings.json`. ### Manual: export the OTEL variables yourself For environments where the CLI doesn't run the agent (CI, a service unit, a container entrypoint), export the standard OpenTelemetry variables in the shell that launches Claude Code: ```bash theme={"dark"} export CLAUDE_CODE_ENABLE_TELEMETRY=1 export OTEL_METRICS_EXPORTER=otlp export OTEL_LOGS_EXPORTER=otlp export OTEL_TRACES_EXPORTER=otlp export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # Logs + metrics ingest as protobuf; the traces endpoint expects JSON. export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https:///v1/otel/claude/logs export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https:///v1/otel/claude/traces export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https:///v1/otel/claude/metrics export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $MODA_TOKEN" export OTEL_RESOURCE_ATTRIBUTES="provider=claude_code,company.id=...,project.id=...,user.email=...,repo=..." # Required — without this Claude sends only prompt_length (or prompt="") export OTEL_LOG_USER_PROMPTS=1 ``` These are the same variables `moda init` writes into `~/.claude/settings.json`. An exported value in the launching shell wins over the `settings.json` `env` block, so an existing shell with stale exports keeps using them until you start a fresh session. Claude Code redacts prompt text by default. If `OTEL_LOG_USER_PROMPTS` is not set in the shell that launches `claude`, the OTLP `prompt` attribute is empty or the literal placeholder `` while `prompt_length` still reflects the real size. Moda does not store that placeholder as message text. When `OTEL_LOG_USER_PROMPTS=1`, Moda persists the real `prompt` attribute into `coding_agent_events.prompt_text` (after secret redaction). You do not need `CODING_AGENT_CAPTURE_PROMPTS` on the ingest worker for emitter-provided prompt text; that flag is for server-wide capture or hook payloads without a Claude-side opt-in. `OTEL_RESOURCE_ATTRIBUTES` is how Moda groups events by org, project, user, and repo. See [Resource attributes Moda reads](#resource-attributes-moda-reads) below for the full list. Because Claude Code defaults to `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, the logs endpoint must accept protobuf bodies. To smoke-test the spec-mandated protobuf path directly (swap `application/x-protobuf` for `application/json` if your exporter is JSON-mode): ```bash theme={"dark"} curl -X POST https:///v1/otel/claude/logs \ -H "Authorization: Bearer $MODA_TOKEN" \ -H "Content-Type: application/x-protobuf" \ --data-binary @logs.binpb ``` ## Codex setup `moda init` or `moda hooks install --agent=codex` writes the Moda hook config plus the provider-specific OTEL env block into `~/.codex/hooks.json`. For managed environments where you configure Codex directly, add the following block to your Codex config: ```toml theme={"dark"} [otel] environment = "production" log_user_prompt = false [otel.exporter.otlp-http] endpoint = "https:///v1/otel/codex/logs" protocol = "binary" headers = { "authorization" = "Bearer MODA_TOKEN" } ``` This config should live in your user-level or managed Codex config, not in a project-local file. Project-local OTEL config can leak between repositories and is harder to audit. ## Hooks (optional) Both Claude Code and Codex support custom hook scripts that fire on session, prompt, tool, and permission events. Moda exposes dedicated JSON endpoints so those hook scripts can POST structured payloads directly, without going through OTLP. This is useful when you want richer per-event signals than the runtime emits over OTLP, for example custom approval decisions, repo metadata, or sandbox details. ```bash theme={"dark"} curl -X POST https:///v1/otel/claude/hooks \ -H "Authorization: Bearer $MODA_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "hook": "PostToolUse", "session_id": "sess_123", "tool_name": "Bash", "tool_use_id": "tool_abc", "success": true, "duration_ms": 412, "cwd": "/home/user/repo", "repo": "acme/web", "branch": "main", "timestamp": "2025-01-15T12:34:56Z" }' ``` Swap `/v1/otel/claude/hooks` for `/v1/otel/codex/hooks` or `/v1/otel/cursor/hooks` when sending from a Codex or Cursor hook script. ## Skill creation `moda skills gen` is **SDK-only**: it selects pending conversations from `conversation_logging` (Boardy / SDK ingest), runs analytics + clustering, distills candidate skills, and promotes candidates that pass the gate. `--source=coding-agent` is soft-deprecated (accepted by old CLIs, returns an empty work list). Coding-agent OTLP ingest into `coding_agent_events` continues independently; single-session `/orchestrate` can still process a coding-agent session, but the generate stage machine no longer projects those sessions. CLI hook install (`moda hooks install`) is temporarily disabled — see Claude Code setup above for manual OTEL wiring. Previously installed hooks may still post session-end signals; they are not required for SDK skill generation. ### Pulling generated skills into your project Promoted skills are served back to your coding agent with `moda skills pull`, which fetches your tenant's skills (authenticated with the same API key) and writes each into the agent's skill directory so it can use them: ```bash theme={"dark"} moda skills pull # writes approved skills to .claude/skills//SKILL.md (+ .cursor/rules/.mdc) moda skills pull --status=proposed # include not-yet-promoted candidates (default: approved) ``` `moda init` runs this automatically after detecting your coding agent, so existing skills are available immediately on setup. Re-running `pull` is idempotent: unchanged skills are skipped and updated ones are rewritten. The full SKILL.md text is stored durably server-side (in ClickHouse), so retrieval does not depend on the ephemeral skill-harness container filesystem. ## Privacy & data capture Prompt text and tool output snippets are **off by default**. Moda stores event-level metadata (tool names, durations, token counts, success/failure, error types) but does not persist user prompts or command output unless you explicitly opt in. ### Capture policy The ingest worker exposes three env flags that, together, determine whether a given prompt or output snippet is persisted. The effective rule applied to every event is: > `capture = serverCapture OR (allowEventLevelOptIn AND eventLevelOptIn)` | Env var | Default | Purpose | | --------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CODING_AGENT_CAPTURE_PROMPTS` | `false` | Server-side switch for prompt text. When `true`, every event's prompt text is persisted (after redaction + truncation). | | `CODING_AGENT_CAPTURE_OUTPUT_SNIPPETS` | `false` | Server-side switch for tool output snippets. When `true`, every event's output snippet is persisted (after redaction + truncation). | | `CODING_AGENT_ALLOW_EVENT_LEVEL_OPT_IN` | `false` | Master gate for honoring per-event `moda.capture_prompts=true` / `moda.capture_outputs=true` attributes. When `false`, those attributes are ignored. When `true`, individual events can upgrade themselves into capture even when the server-level flags are off. | All three flags accept `"true"` or `"1"` as truthy; every other value (including empty, `"yes"`, `"on"`) is treated as `false`. ### Prompt and output capture The three flags combine to give three useful deployment shapes: ```bash theme={"dark"} # Default: nothing is captured. Only event-level metadata reaches Moda. # (no env vars set — this is the recommended baseline.) # Server-on: capture everything globally, regardless of per-event attrs. export CODING_AGENT_CAPTURE_PROMPTS=true export CODING_AGENT_CAPTURE_OUTPUT_SNIPPETS=true # Event-opt-in enabled: server defaults to off, but events that carry # `moda.capture_prompts=true` or `moda.capture_outputs=true` are honored. export CODING_AGENT_ALLOW_EVENT_LEVEL_OPT_IN=true ``` When an event's text is suppressed by this policy, Moda still writes the row but sets a `prompt_text_redacted=true` (or `output_snippet_redacted=true`) marker on `attributes` so downstream consumers can distinguish "no text emitted" from "text emitted but suppressed at ingest". Secret redaction is applied to commands, error messages, and any captured output snippets before insert. Patterns such as `Authorization`, `Bearer`, `api_key`, `token`, `password`, `secret`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `MODA_TOKEN`, and `COOKIE` are stripped. ## Provider-specific notes **Claude Code: skill activation is first-class.** Claude Code emits first-class skill activation events (`claude_code.skill_activated`). Moda treats these as canonical `skill_activated` rows with `skill_name` and `skill_scope` populated, so skill usage analytics work without any extra instrumentation. **Claude child processes do not inherit OTEL env vars automatically.** If Claude Code spawns subprocesses (for example via Bash tool calls that themselves invoke `claude`), those child processes will not export telemetry unless you re-export the same OTEL environment variables in the child's shell. Plan child telemetry setup separately. **Codex: skill usage is not first-class today.** Codex does not currently expose canonical skill telemetry; skill usage is inferred from tool-call attributes or hook payloads. Moda surfaces these signals where present but does not synthesize first-class `skill_activated` rows for Codex. ## Resource attributes Moda reads The Moda normalizer recognizes the following attribute names on OTLP resource, log record, or span scopes. Set these via `OTEL_RESOURCE_ATTRIBUTES` (Claude Code) or your hook payloads (Codex) to route events to the right org, project, and user in Moda. * **Org:** `org_id`, `org.id`, `company.id`, `organization.id` * **Project:** `project_id`, `project.id`, `moda.project_id` * **User ID:** `user.id`, `enduser.id`, `moda.user_id` * **User email:** `user.email`, `enduser.email` * **Repo:** `repo` * **Branch:** `branch` `tenant_id` is always resolved from your Moda API key and cannot be overridden via body attributes. ## Troubleshooting **Events not appearing?** Query the Layer 0 raw store to see whether requests are reaching the ingest worker: ```sql theme={"dark"} SELECT timestamp, source, http_status, raw_event FROM moda.events_raw WHERE source = 'otlp' OR source = 'coding_agent_hook' ORDER BY timestamp DESC LIMIT 50; ``` * `source = 'otlp'` covers all `/v1/otel/{claude,codex,cursor}/{logs,traces,metrics}` requests. * `source = 'coding_agent_hook'` covers `/v1/otel/{claude,codex,cursor}/hooks` requests. If rows are missing entirely, check that your `Authorization` header is set and that the exporter endpoint matches the table in [Endpoints](#endpoints). If rows are present but no normalized data appears, inspect `raw_event` to confirm the payload shape and event names. For more on the underlying OTLP trace pipeline and other ingestion providers, see the [Ingestion overview](/ingestion/overview). # OpenAI Source: https://docs.moda.dev/ingestion/providers/openai Using Moda with OpenAI ## Overview Moda automatically tracks your OpenAI API calls. Chat completions, streaming, function calling, and tool use are all captured with no additional code required. ## Setup ```bash Python theme={"dark"} pip install moda-ai openai ``` ```bash Node.js theme={"dark"} npm install moda-ai openai ``` ```python Python theme={"dark"} import moda from openai import OpenAI moda.init("YOUR_MODA_API_KEY") client = OpenAI() response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello!"}] ) moda.flush() ``` ```typescript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import OpenAI from 'openai'; await Moda.init('YOUR_MODA_API_KEY'); const client = new OpenAI(); const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello!' }] }); await Moda.flush(); ``` ## Supported Features | Feature | Captured | | --------------------------- | -------------------------------- | | Chat completions | Yes | | Streaming | Yes | | Function calling / tool use | Yes (captured as content blocks) | | Embeddings | Yes | | Token usage | Yes (input, output, total) | | Model name | Yes (request and response) | ## Streaming Streaming responses are automatically tracked. The SDK captures the complete response after the stream finishes: ```python Python theme={"dark"} stream = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Count to 5"}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="") ``` ```typescript Node.js 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 || ''); } ``` ## Tool Use Tool calls and function calling are automatically captured with full input/output details: ```python Python theme={"dark"} response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What's the weather in London?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "Get the weather for a location", "parameters": { "type": "object", "properties": { "location": {"type": "string"} } } } }] ) ``` ```typescript Node.js theme={"dark"} const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: "What's the weather in London?" }], tools: [{ type: 'function', function: { name: 'get_weather', description: 'Get the weather for a location', parameters: { type: 'object', properties: { location: { type: 'string' } } } } }] }); ``` ## Troubleshooting **Data not appearing?** * Ensure `moda.init()` is called before creating the OpenAI client * Call `moda.flush()` (Python) or `await Moda.flush()` (Node.js) before exit * Verify your API key is correct **Streaming responses incomplete?** * The SDK captures the full response after the stream ends. Ensure you consume the entire stream. For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) or [Node.js SDK](/ingestion/moda-sdk-node) guides. # OpenClaw Source: https://docs.moda.dev/ingestion/providers/openclaw Using Moda with OpenClaw via OTLP diagnostics ## Overview OpenClaw can export telemetry through its `diagnostics-otel` plugin. Moda accepts that OTLP traffic at `/v1/traces` and maps OpenClaw session spans into `conversation_logging`. ## 1. Enable OpenClaw diagnostics OpenClaw does not export OTLP by default. Enable the plugin and diagnostics keys once in your OpenClaw profile: ```bash theme={"dark"} openclaw plugins enable diagnostics-otel openclaw config set diagnostics.enabled true --json openclaw config set diagnostics.otel.enabled true --json ``` ## 2. Point OpenClaw to Moda Set the OTLP endpoint and auth headers to Moda ingest: ```bash theme={"dark"} openclaw config set diagnostics.otel.endpoint '"https://moda-ingest.modas.workers.dev"' --json openclaw config set diagnostics.otel.protocol '"http/protobuf"' --json openclaw config set diagnostics.otel.serviceName '"openclaw-gateway"' --json openclaw config set diagnostics.otel.traces true --json ``` You can also use Moda SDK helpers to generate equivalent OTEL environment variables for OpenClaw subprocesses. ## Node.js helper usage ```ts theme={"dark"} import { Moda } from 'moda-ai'; await Moda.init(process.env.MODA_API_KEY!); const env = Moda.getOpenClawEnvironment({ serviceName: 'openclaw-gateway', }); // Pass `env` when spawning OpenClaw ``` ## Python helper usage ```python theme={"dark"} import moda from moda.openclaw import get_openclaw_env, run_openclaw_cli moda.init("YOUR_MODA_API_KEY") env = get_openclaw_env(service_name="openclaw-gateway") result = run_openclaw_cli( ["agent", "--local", "--session-id", "session_123", "--message", "Hello", "--json"], command_prefix=["npx", "-y", "openclaw@latest"], env={"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_KEY", **env}, ) print(result.stdout) ``` ## What gets ingested For OpenClaw spans, Moda ingestion supports: * `openclaw.model.usage` * `openclaw.message.processed` Conversation grouping uses: * `openclaw.sessionId` (preferred) * `openclaw.sessionKey` (fallback) ## Troubleshooting **No rows in `conversation_logging`?** * Verify `openclaw plugins list` shows `diagnostics-otel` as enabled. * Verify `diagnostics.enabled=true` and `diagnostics.otel.enabled=true`. * Verify endpoint is `https://moda-ingest.modas.workers.dev` (without extra path suffixes). * Verify OTLP headers include `Authorization: Bearer `. **Still seeing `count: 0` from `/v1/traces` responses?** * Your ingest worker may be running an older build that does not parse `openclaw.*` spans yet. * Deploy the ingest worker version that includes OpenClaw span extraction. # OpenRouter Source: https://docs.moda.dev/ingestion/providers/openrouter Using Moda with OpenRouter ## Overview [OpenRouter](https://openrouter.ai) provides access to multiple LLM providers through a unified, OpenAI-compatible API. Since OpenRouter uses the same interface as OpenAI, Moda automatically tracks your OpenRouter calls with no extra configuration. ## Setup with Moda SDK ```python Python theme={"dark"} import moda from openai import OpenAI moda.init("YOUR_MODA_API_KEY") openrouter = OpenAI( base_url="https://openrouter.ai/api/v1", api_key="YOUR_OPENROUTER_API_KEY", default_headers={ "HTTP-Referer": "https://your-app.com", "X-Title": "Your App Name", }, ) moda.conversation_id = "session_123" response = openrouter.chat.completions.create( model="anthropic/claude-3.5-sonnet", messages=[{"role": "user", "content": "Hello!"}] ) moda.flush() ``` ```typescript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import OpenAI from 'openai'; await Moda.init('YOUR_MODA_API_KEY'); const openrouter = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: 'YOUR_OPENROUTER_API_KEY', defaultHeaders: { 'HTTP-Referer': 'https://your-app.com', 'X-Title': 'Your App Name', }, }); Moda.conversationId = 'session_123'; const response = await openrouter.chat.completions.create({ model: 'anthropic/claude-3.5-sonnet', messages: [{ role: 'user', content: 'Hello!' }] }); await Moda.flush(); ``` OpenRouter model names use the format `provider/model-name`. See the [OpenRouter models page](https://openrouter.ai/models) for all available models. ## Direct API with Manual Tracing For cases where you want to call the OpenRouter API directly (without the OpenAI client), use the Node.js SDK's manual tracing: ```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 }) => { span.reportRequest({ model: 'anthropic/claude-3-sonnet', messages }); 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(); span.reportResponse({ model: data.model, usage: data.usage, completions: data.choices, }); return data; } ); ``` `Moda.withLLMCall()` is available in the Node.js SDK. See [Manual Tracing](/ingestion/moda-sdk-node#manual-tracing) for details. ## Supported Features | Feature | Captured | | ---------------- | ------------------------------------------- | | Chat completions | Yes | | Streaming | Yes | | Token usage | Yes | | Model name | Yes (includes provider prefix) | | Tool use | Yes (when the underlying model supports it) | ## Troubleshooting **Model name shows OpenRouter prefix?** * This is expected. OpenRouter model names include the provider prefix (e.g., `anthropic/claude-3.5-sonnet`). Moda captures the exact model name returned by OpenRouter. **Token usage missing?** * Some models on OpenRouter may not return token usage data. This depends on the underlying provider. For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) or [Node.js SDK](/ingestion/moda-sdk-node) guides. # Vapi Source: https://docs.moda.dev/ingestion/providers/vapi Using Moda with Vapi Voice AI ## Overview Moda tracks your Vapi voice AI calls by processing end-of-call report webhooks. Each call is captured with full detail, including conversation turns, tool calls, squad transfers, call analysis, and cost breakdowns. ## Setup ```bash Python theme={"dark"} pip install moda-ai ``` ```bash Node.js theme={"dark"} npm install moda-ai ``` ## Usage Point your Vapi webhook URL to your server and pass the payload to Moda: ```python FastAPI theme={"dark"} from fastapi import FastAPI, Request import moda from moda import process_vapi_end_of_call_report app = FastAPI() moda.init("YOUR_MODA_API_KEY") @app.post("/webhooks/vapi") async def vapi_webhook(request: Request): payload = await request.json() process_vapi_end_of_call_report(payload) return {"status": "ok"} ``` ```python Flask theme={"dark"} from flask import Flask, request import moda from moda import process_vapi_end_of_call_report app = Flask(__name__) moda.init("YOUR_MODA_API_KEY") @app.route("/webhooks/vapi", methods=["POST"]) def vapi_webhook(): payload = request.get_json() process_vapi_end_of_call_report(payload) return {"status": "ok"} ``` ```typescript Express theme={"dark"} import express from 'express'; import { Moda } from 'moda-ai'; const app = express(); app.use(express.json()); Moda.init('YOUR_MODA_API_KEY'); app.post('/webhooks/vapi', (req, res) => { Moda.processVapiEndOfCallReport(req.body); res.json({ status: 'ok' }); }); app.listen(3000); ``` ```typescript Next.js theme={"dark"} import { Moda } from 'moda-ai'; import { NextRequest, NextResponse } from 'next/server'; Moda.init('YOUR_MODA_API_KEY'); export async function POST(request: NextRequest) { const payload = await request.json(); Moda.processVapiEndOfCallReport(payload); return NextResponse.json({ status: 'ok' }); } ``` ## Supported Features | Feature | Captured | | ----------------------- | -------- | | Conversation turns | Yes | | Tool calls | Yes | | Squad transfers | Yes | | Call analysis / summary | Yes | | Structured data | Yes | | Cost breakdown | Yes | | Turn latency metrics | Yes | | Recording URLs | Yes | ## Configuration Use `ProcessVapiOptions` to customize the conversation and user identifiers: ```python Python theme={"dark"} process_vapi_end_of_call_report(payload, { "conversation_id": "my_session_123", "user_id": "user_456", }) ``` ```typescript Node.js theme={"dark"} Moda.processVapiEndOfCallReport(payload, { conversationId: 'my_session_123', userId: 'user_456', }); ``` If not provided, `conversationId` defaults to `call.id` and `userId` defaults to `call.customer.number`. ## What Moda Captures For each Vapi call, Moda captures a structured hierarchy of data: ``` Call |-- Turn 0 (first assistant turn) |-- Turn 1 (second assistant turn) |-- Tool Call (e.g., lookupOrder) |-- Squad Transfer (if applicable) ``` ### Call-Level Data | Field | Description | | ------------------ | ---------------------------------------------------------- | | Conversation ID | Call ID or your custom conversation ID | | User ID | Customer number or your custom user ID | | Duration | Call duration in seconds | | Total cost | Total call cost | | Ended reason | Reason the call ended | | Assistant ID | Vapi assistant identifier | | Status | Call status (e.g., "ended") | | Start / end time | Timestamps for the call | | Summary | AI-generated call summary | | Structured data | Custom structured data from Vapi | | Success evaluation | Success evaluation result | | Cost breakdown | Separate costs for LLM, speech-to-text, and text-to-speech | ### Turn-Level Data | Field | Description | | ------------------ | -------------------------------- | | User message | What the caller said | | Assistant response | What the assistant replied | | Model latency | Time for the LLM to respond (ms) | | Voice latency | Time for voice synthesis (ms) | | Total latency | End-to-end turn latency (ms) | ## Troubleshooting **No data appearing?** * Ensure `Moda.init()` is called before processing webhooks * Call `Moda.flush()` before your process exits to ensure all data is sent * Verify your Vapi webhook is configured to send `end-of-call-report` events **Call appears with no turns?** * Moda handles both the `message`-wrapped format (real Vapi webhooks) and the legacy flat format * If you see a call with no turns, check that your payload contains conversation data in `call.artifact.messages` or a `transcript` array **Transcript not captured?** * Vapi uses `[{role, message}]` format for transcripts. Moda automatically normalizes this for display. For full SDK documentation, see the [Python SDK](/ingestion/moda-sdk) or [Node.js SDK](/ingestion/moda-sdk-node) guides. # Vercel AI SDK Source: https://docs.moda.dev/ingestion/vercel-ai-sdk Using Moda with the Vercel AI SDK for automatic LLM analytics ## Overview Moda integrates with the [Vercel AI SDK](https://ai-sdk.dev) via its built-in telemetry support. The AI SDK's `experimental_telemetry` option emits telemetry data that Moda parses automatically, giving you conversation tracking, token usage, and analytics across all AI SDK providers. ## Installation ```bash theme={"dark"} npm install moda-ai ai @ai-sdk/openai ``` Install additional provider packages as needed: ```bash theme={"dark"} # Anthropic npm install @ai-sdk/anthropic # Google npm install @ai-sdk/google # Mistral npm install @ai-sdk/mistral ``` ## Quick Start ```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(), }); console.log(result.text); await Moda.flush(); ``` ## Streaming `streamText` works the same way. Telemetry is captured after the stream completes: ```typescript theme={"dark"} import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = streamText({ model: openai('gpt-4o'), prompt: 'Explain TypeScript in 3 sentences', experimental_telemetry: Moda.getVercelAITelemetry(), }); for await (const chunk of result.textStream) { process.stdout.write(chunk); } await Moda.flush(); ``` ## Structured Output `generateObject` responses are captured as JSON in the assistant message: ```typescript theme={"dark"} import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const result = await generateObject({ model: openai('gpt-4o'), schema: z.object({ name: z.string(), ingredients: z.array(z.string()), servings: z.number(), }), prompt: 'Generate a cookie recipe', experimental_telemetry: Moda.getVercelAITelemetry(), }); console.log(result.object); await Moda.flush(); ``` ## Tool Use Tool calls made by the model are captured as structured content blocks: ```typescript theme={"dark"} import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const result = await generateText({ model: openai('gpt-4o'), prompt: 'What is the weather in Paris?', tools: { getWeather: tool({ description: 'Get the weather for a location', parameters: z.object({ city: z.string() }), execute: async ({ city }) => `Sunny, 72F in ${city}`, }), }, experimental_telemetry: Moda.getVercelAITelemetry(), }); await Moda.flush(); ``` ## Conversation Threading When you set `Moda.conversationId` or `Moda.userId` before calling an AI SDK function, those values are automatically included in the telemetry metadata: ```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'); // Set conversation context Moda.conversationId = 'support_ticket_456'; Moda.userId = 'user_789'; // First turn await generateText({ model: openai('gpt-4o'), prompt: 'I need help with my order', experimental_telemetry: Moda.getVercelAITelemetry(), }); // Second turn - same conversation await generateText({ model: openai('gpt-4o'), prompt: 'Order number is #12345', experimental_telemetry: Moda.getVercelAITelemetry(), }); // Both calls are grouped under the same conversation await Moda.flush(); Moda.conversationId = null; Moda.userId = null; ``` The `getVercelAITelemetry()` helper automatically includes `moda.conversation_id` and `moda.user_id` in the telemetry metadata when they are set. ## Prompt Attribution Render managed prompts before creating the telemetry config. `Moda.getVercelAITelemetry()` includes prompt metadata in `experimental_telemetry.metadata`. ```typescript theme={"dark"} const rendered = Moda.prompt("support.triage").render({ ticket: { text: userMessage }, }); const result = await generateText({ model: openai("gpt-4o"), messages: rendered.messages, experimental_telemetry: Moda.getVercelAITelemetry(), }); ``` Moda stores `moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`, and `moda.prompt_version_id` with the conversation logs, so the dashboard and future evals can compare behavior by prompt version. ## Options Reference ```typescript theme={"dark"} Moda.getVercelAITelemetry({ recordInputs: true, // Record prompt messages (default: true) recordOutputs: true, // Record response content (default: true) functionId: 'my-chatbot', // Group telemetry by function name metadata: { // Additional custom metadata feature: 'support-chat', version: '2.0', }, }); ``` | Option | Type | Default | Description | | --------------- | ------------------------ | ------- | ----------------------------------------------- | | `recordInputs` | `boolean` | `true` | Whether to record prompt messages in telemetry | | `recordOutputs` | `boolean` | `true` | Whether to record response content in telemetry | | `functionId` | `string` | - | Identifier for grouping telemetry by function | | `metadata` | `Record` | - | Custom metadata attached to spans | ## Supported Providers Any provider supported by the Vercel AI SDK works with Moda. The model and provider are automatically captured. | Provider | Package | Example | | -------------- | ------------------------ | ----------------------------------------- | | OpenAI | `@ai-sdk/openai` | `openai('gpt-4o')` | | Anthropic | `@ai-sdk/anthropic` | `anthropic('claude-3-5-sonnet-20241022')` | | Google | `@ai-sdk/google` | `google('gemini-1.5-pro')` | | Mistral | `@ai-sdk/mistral` | `mistral('mistral-large-latest')` | | Amazon Bedrock | `@ai-sdk/amazon-bedrock` | `bedrock('anthropic.claude-3-sonnet')` | | Azure OpenAI | `@ai-sdk/azure` | `azure('gpt-4o')` | ## Troubleshooting **Data not appearing in Moda?** * Ensure `Moda.init()` is called with `await` before your first AI SDK call * Call `await Moda.flush()` before your program exits * Verify your API key is correct * Enable debug mode: `Moda.init('key', { debug: true })` **Conversation IDs not grouping?** * Make sure `Moda.conversationId` is set before calling `Moda.getVercelAITelemetry()` * The telemetry config is created at call time, so set the conversation ID first **Using with other Moda instrumentations?** * If you also use Moda's native OpenAI/Anthropic instrumentation, both will capture data. The AI SDK telemetry captures the high-level AI SDK call, while native instrumentation captures the underlying provider API call. This is safe but may result in duplicate entries. To avoid this, you can disable native instrumentation for providers used through the AI SDK. For full SDK documentation, see the [Node.js SDK](/ingestion/moda-sdk-node) guide. # Prompt Management Source: https://docs.moda.dev/prompt-management/overview Code-first prompt versioning, runtime attribution, and dashboard visibility ## Overview Moda prompt management keeps prompts in your repository and syncs immutable versions to Moda. Developers edit prompt files, the CLI detects local changes, `moda prompts sync` uploads new versions, and the dashboard shows each prompt, version, source path, labels, and runtime usage. This is designed for evals: every LLM call can carry `moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`, and `moda.prompt_version_id`, so later eval runs can compare prompt versions against production behavior. ## Workflow 1. Run `moda init` once in the repo. 2. Keep prompt files under `prompts/**/*.prompt.md` or `prompts/**/*.prompt.json`. 3. Check local changes with `moda prompts status`. 4. Upload immutable versions with `moda prompts sync`. 5. Render prompts at runtime with `Moda.prompt(...).render(...)` or `moda.prompt(...).render(...)`. 6. Promote labels with `moda prompts promote --label=prod --version=`. ## Versioning Each prompt version is content-addressed from the prompt key, content, messages, system prompt, model config, variables schema, tools, and response schema. Syncing unchanged content reuses the same version; changing a prompt creates a new version. Labels are pointers: | Label | Use | | --------- | ------------------------- | | `dev` | Active development | | `staging` | Pre-production validation | | `prod` | Production prompt | ## Runtime Attribution Node.js: ```typescript theme={"dark"} const rendered = Moda.prompt("support.triage").render({ ticket: { text: userMessage }, }); await Moda.withPrompt(rendered, async () => { await openai.chat.completions.create({ model: "gpt-4o", messages: rendered.messages, }); }); ``` Python: ```python theme={"dark"} rendered = moda.prompt("support.triage").render({ "ticket": {"text": user_message}, }) client.chat.completions.create( model="gpt-4o", messages=rendered["messages"], ) ``` The SDKs attach prompt metadata to OpenTelemetry spans. Moda ingest stores that metadata with conversation logs, making prompt usage visible in the dashboard and queryable for future evals. ## Dashboard The Prompts dashboard shows synced prompts and versions from the backend registry. Use it to inspect source paths, labels, content hashes, and usage. Treat the repository as the source of truth; use the dashboard for visibility and promotion, not copy-pasting prompt text into code. # Prompt Quickstart Source: https://docs.moda.dev/prompt-management/quickstart Sync a code-first prompt and attribute runtime calls ## 1. Initialize Moda ```bash theme={"dark"} moda init ``` `moda init` authenticates the CLI, writes SDK setup guidance, and creates a prompt manifest: ```yaml .moda/prompts.yml theme={"dark"} version: 1 prompt_paths: - "prompts/**/*.prompt.md" - "prompts/**/*.prompt.json" - "prompts/**/*.prompt.yaml" - "prompts/**/*.prompt.yml" ``` ## 2. Add a Prompt File ```md prompts/support/triage.prompt.md theme={"dark"} --- key: support.triage name: Support triage system_prompt: You route support tickets to the right team. model: gpt-4o --- Ticket: {{ticket.text}} ``` ## 3. Check Status ```bash theme={"dark"} moda prompts status ``` The command prints JSON showing new, changed, unchanged, and deleted prompt files. ## 4. Sync Versions ```bash theme={"dark"} moda prompts sync ``` Sync uploads changed prompts and writes `.moda/prompts.lock.json` with the server prompt ID, version ID, source path, and content hash. During development, you can keep the sync loop running: ```bash theme={"dark"} moda prompts sync --watch ``` ## 5. Render at Runtime ```typescript Node.js theme={"dark"} import { Moda } from "moda-ai"; import OpenAI from "openai"; await Moda.init(process.env.MODA_API_KEY!); const client = new OpenAI(); const rendered = Moda.prompt("support.triage").render({ ticket: { text: userMessage }, }); Moda.conversationId = conversationId; Moda.userId = userId; const response = await client.chat.completions.create({ model: "gpt-4o", messages: rendered.messages, }); await Moda.flush(); ``` ```python Python theme={"dark"} import moda from openai import OpenAI moda.init("YOUR_MODA_API_KEY") moda.conversation_id = conversation_id moda.user_id = user_id client = OpenAI() rendered = moda.prompt("support.triage").render({ "ticket": {"text": user_message}, }) response = client.chat.completions.create( model="gpt-4o", messages=rendered["messages"], ) moda.flush() ``` ## 6. Promote a Label ```bash theme={"dark"} moda prompts promote support.triage --label=prod --version=pver_abc123 ``` Use `dev`, `staging`, and `prod` labels to mark rollout state. Versions stay immutable; labels move. # Quickstart Source: https://docs.moda.dev/quickstart Get up and running with Moda in under 5 minutes ## Prerequisites Before you start, you need: * **A Moda API key**, get one from the [Moda dashboard](https://app.moda.dev) * **An API key from your LLM provider**, [OpenAI](https://platform.openai.com/api-keys) or [Anthropic](https://console.anthropic.com/settings/keys) * **Python 3.9+** or **Node.js 18+** ## Step 1: Install the SDK Install the Moda SDK along with your LLM provider's client library: ```bash Python (OpenAI) theme={"dark"} pip install moda-ai openai ``` ```bash Python (Anthropic) theme={"dark"} pip install moda-ai anthropic ``` ```bash Node.js (OpenAI) theme={"dark"} npm install moda-ai openai ``` ```bash Node.js (Anthropic) theme={"dark"} npm install moda-ai @anthropic-ai/sdk ``` ## Step 2: Make your first request Replace `YOUR_MODA_API_KEY` and your provider API key with your actual keys. ```python Python theme={"dark"} import moda from openai import OpenAI # Initialize Moda once at startup moda.init("YOUR_MODA_API_KEY") # Use OpenAI as normal. Moda captures the call automatically client = OpenAI(api_key="YOUR_OPENAI_KEY") response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello, how are you?"}] ) print(response.choices[0].message.content) # Flush before your process exits to ensure all data is sent moda.flush() ``` ```javascript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import OpenAI from 'openai'; // Initialize Moda once at startup Moda.init('YOUR_MODA_API_KEY'); // Use OpenAI as normal. Moda captures the call automatically const client = new OpenAI({ apiKey: 'YOUR_OPENAI_KEY' }); const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello, how are you?' }], }); console.log(response.choices[0].message.content); // Flush before your process exits to ensure all data is sent await Moda.flush(); ``` You should see the model's response printed in your terminal. Behind the scenes, Moda has captured the full request and response. ## Step 3: Sync your first prompt Prompt management is code-first. Keep prompts in files, sync versions with the CLI, and render them through the SDK so runtime calls are attributed to the exact prompt version. ```bash theme={"dark"} moda init moda prompts status moda prompts sync ``` Node.js: ```typescript theme={"dark"} const rendered = Moda.prompt("support.triage").render({ ticket: { text: userMessage }, }); await client.chat.completions.create({ model: "gpt-4o", messages: rendered.messages, }); ``` Python: ```python theme={"dark"} rendered = moda.prompt("support.triage").render({ "ticket": {"text": user_message}, }) client.chat.completions.create( model="gpt-4o", messages=rendered["messages"], ) ``` See [Prompt Management](/prompt-management/overview) for the full workflow. ## Step 4: Try a different provider Moda works the same way with Anthropic. Initialize Moda, use the provider client as normal, and Moda captures everything automatically. ```python Python theme={"dark"} import moda from anthropic import Anthropic moda.init("YOUR_MODA_API_KEY") client = Anthropic(api_key="YOUR_ANTHROPIC_KEY") response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Hello, how are you?"}] ) print(response.content[0].text) moda.flush() ``` ```javascript Node.js theme={"dark"} import { Moda } from 'moda-ai'; import Anthropic from '@anthropic-ai/sdk'; Moda.init('YOUR_MODA_API_KEY'); const client = new Anthropic({ apiKey: 'YOUR_ANTHROPIC_KEY' }); const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', maxTokens: 1024, messages: [{ role: 'user', content: 'Hello, how are you?' }], }); console.log(response.content[0].text); await Moda.flush(); ``` For provider-specific features like streaming, tool use, and extended thinking, see the [Anthropic](/ingestion/providers/anthropic) and [OpenAI](/ingestion/providers/openai) guides. ## Step 5: View your conversations After running the code above, open the [Moda dashboard](https://app.moda.dev) to see your conversations. Each LLM call is automatically logged with the full request, response, model, and token usage. ## Troubleshooting | Problem | Solution | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Data not appearing in the dashboard | Make sure `moda.init()` is called **before** creating the LLM client, and call `moda.flush()` before your process exits. | | `ModuleNotFoundError: No module named 'moda'` | Run `pip install moda-ai` (the package name is `moda-ai`, not `moda`). | | `Cannot find module 'moda-ai'` | Run `npm install moda-ai` in your project directory. | ## What is next? See all the ways to send data to Moda, including the Direct API. Detailed setup for OpenAI, Anthropic, Azure, Bedrock, and more.