Metadata-Version: 2.4
Name: superbryn-pipecat-observer
Version: 0.11.1
Summary: Track and evaluate Pipecat agent sessions with automatic metrics, transcripts, and usage analytics
Author-email: superbryndev <support@superbryndev.com>
License: MIT
Project-URL: Homepage, https://github.com/superbryndev/superbryn-pipecat-observer
Project-URL: Documentation, https://docs.superbryn.com/monitor/integrations#pipecat-sdk
Project-URL: Repository, https://github.com/superbryndev/superbryn-pipecat-observer
Project-URL: Issues, https://github.com/superbryndev/superbryn-pipecat-observer/issues
Keywords: pipecat,agents,analytics,metrics,evaluation,voice-ai,observability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Communications :: Telephony
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pipecat-ai>=1.4.0
Requires-Dist: aiohttp>=3.9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

# SuperBryn Pipecat Observer

[![PyPI version](https://img.shields.io/badge/pypi-v0.11.0-orange)](https://pypi.org/project/superbryn-pipecat-observer/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Pipecat](https://img.shields.io/badge/pipecat-compatible-purple.svg)](https://github.com/pipecat-ai/pipecat)

Drop-in observer for [Pipecat](https://github.com/pipecat-ai/pipecat) voice AI agents. Captures transcript, audio, usage, and latency, then ships everything to SuperBryn at end of session — on any transport (Daily, Twilio, Plivo, Vobiz, WebRTC, WebSocket, …).

## Features

- **Drop-in integration** — wrap your pipeline with one call, every call shows up in SuperBryn.
- **Transport-agnostic** — same code works on Daily, Twilio, Plivo, Vobiz, WebRTC, and FastAPI WebSocket. No carrier credentials needed.
- **Precise transcripts** — speaker turns with timestamps, confidence scores, and per-turn bot latency.
- **Usage metrics** — LLM tokens (in / out), STT seconds, TTS characters, captured automatically from Pipecat's `MetricsFrame`.
- **Latency tracking** — average and p95 response time from the caller's end of speech to the bot's first audio.
- **Provider auto-detection** — LLM / STT / TTS provider, model, and voice ID inferred from the service modules in your pipeline.
- **Tool-call capture** — both real Pipecat tools (`FunctionCallInProgressFrame` / `FunctionCallResultFrame`) and Anthropic-style `<tool_use>` prompt tools.
- **UAT / simulation mode** — `simulate_and_create_task` suppresses billing for non-production runs.
- **Session log capture** — buffer INFO+ logs (`logging` and loguru) from the call window and attach them to the payload.
- **Custom metadata** — attach any key/value bag to the call record; mutable mid-session.
- **Turn-detection latency** — end-to-end turn-completion timing and confidence, the hidden part of response latency.
- **Per-service timings** — real TTFB / TTFA (with silence padding split out) / processing time, broken down by service.
- **Pipeline diagnostics** — error frames, interruptions, VAD segments, DTMF, transport lifecycle, frame histogram.
- **Fail-open** — if anything in telemetry fails, your pipeline keeps running. Telemetry never crashes a call.
- **Frame-version tolerant** — frames are matched by class name, so a Pipecat upgrade doesn't hard-break the observer.

## Prerequisites

- Python **3.11+**
- An active Pipecat pipeline (`pipecat-ai >= 0.0.50` recommended for the 1.3+ observer lifecycle)
- `aiohttp >= 3.9.0` (installed as a transitive dependency)
- A SuperBryn API key from [https://app.superbryn.com/api-keys](https://app.superbryn.com/api-keys)

## Install

```bash
pip install superbryn-pipecat-observer
```

## Quick Start

1. Set your API key in the agent process environment:

   ```bash
   export SUPERBRYN_API_KEY="sb_..."
   ```

2. Wrap your pipeline with the observer:

   ```python
   from pipecat.pipeline.runner import PipelineRunner
   from superbryn_pipecat_observer import SuperbrynObserver

   observer = SuperbrynObserver(
       agent_id="your-agent-id",
       agent_name="support-bot",
   )

   task = observer.monitor_and_create_task(
       pipeline,
       transport=transport,
   )

   await PipelineRunner().run(task)
   ```

3. Run a call. The session shows up in SuperBryn within seconds of the call ending.

That's it. The same code works on every transport — no carrier credentials, no per-carrier wiring.

For UAT / simulation calls that shouldn't be billed or analyzed, use `simulate_and_create_task` instead — same signature.

> **Note:** earlier releases exposed these as `observe_and_create_task` /
> `track_and_create_task`. Those names still work but emit a
> `DeprecationWarning` and will be removed in `0.8.0`.

## What Gets Tracked

### Transcript
- Speaker turns (`user` / `agent`)
- Per-turn `start_time_ms` / `end_time_ms` (relative to call start)
- Per-turn `confidence` (when the STT provider reports it)
- Per-bot-turn `latency_ms` (caller's end of speech → bot audio start)
- One agent turn per stretch of bot audio; its text is what was played (TTS text), so a reply the caller cut off keeps only the words heard and carries `interrupted: true`
- Pipelines that play no TTS text fall back to the LLM's streamed text
- Empty turns are filtered out before send

### Usage Metrics
Captured from Pipecat's `MetricsFrame`. Pipecat sends them only with `enable_usage_metrics=True` (and service timings with `enable_metrics=True`); the observer switches both on, including on your own `PipelineParams`, unless you set one to `False`:
- **LLM** — input tokens, output tokens, model, provider
- **TTS** — character count, model, provider, voice ID
- **STT** — seconds of audio processed, model, provider (with a VAD-derived fallback when the provider doesn't report)

### Latency
- Per-turn response delay (caller's end of speech → bot audio start)
- Aggregated **average** and **p95** across the call

### Session Metadata
- Agent ID, agent name, transport label (e.g. `daily`, `twilio`, `vobiz`)
- LLM / STT / TTS provider, model, voice ID
- Pipeline / SDK version tag
- Optional `from_number`, `to_number`, `recording_url`
- `mode` (`observe` or `track`)
- Any `custom_metadata` you pass in (mutable mid-session via `observer.set_custom_metadata({...})`)
- Call end reason (`completed` / `cancelled`)

### Tool Calls
- Real Pipecat tools — paired from `FunctionCallInProgressFrame` + `FunctionCallResultFrame` by `tool_call_id`
- Anthropic-style prompt tool calls — `<tool_use>{...}</tool_use>` blocks extracted from each LLM response and lifted onto `call.tool_calls` (XML is stripped from the visible transcript)
- One record per tool call, however many processors the frames pass through
- Each record carries `function_name`, `arguments`, `result`, `timestamp_ms`, and `tool_call_id`

### Session Logs (opt-in)
- INFO+ records from the agent process during the call window, from both `logging` and loguru (which Pipecat uses)
- Bounded by `max_log_records` (default 1000) so a chatty agent can't blow up the payload
- Disable with `capture_logs=False`

### Extended Telemetry (on by default)

Everything below ships as **additive** sections on the same payload — all previously
existing fields are unchanged. Pass `extended_capture=False` for the legacy shape.
Frames are matched by class name, so an older or newer Pipecat produces fewer
fields rather than breaking.

| Section | What's inside |
|---------|---------------|
| `call.turn_detection` | Turn-completion predictions (`TurnMetricsData`): avg/max end-to-end processing ms, prediction count, confidence, incomplete predictions |
| `call.latency` (new keys) | Real per-service timings: `avg/p95_ttfb_ms`, `avg_ttfa_ms` + `avg_tts_leading_silence_ms`, `avg_processing_ms`, `avg_text_aggregation_ms`, `avg_turn_detection_ms`, `by_service` breakdown |
| `call.usage` (new keys) | Full token breakdown: total, cache read/creation, reasoning, input/output audio tokens |
| `call.speech_stats` | Talk seconds per side, turn counts, talk ratio, silence, longest gap, response delays |
| `call.interruptions` | Interruption count, interruptions while the bot was speaking, user idle timeouts |
| `call.errors` | `ErrorFrame` / `FatalErrorFrame` — message, `fatal`, processor, exception type |
| `call.connection` | Transport lifecycle timeline (client connected, bot connected, output ready, disconnect) |
| `call.sip` | `{attributes, dtmf}` — DTMF keypresses with timestamps and direction |
| `call.vad` | Speech segments, total speech seconds, average segment length, VAD `start_secs`/`stop_secs` |
| `call.pipeline` | Services seen, service metadata (`ttfs_p99_ms`, `is_realtime_service`), frame-type histogram, LLM reasoning counts |
| `call.environment` | Observer / Pipecat / Python versions |

```python
observer = SuperbrynObserver(
    agent_id=AGENT_ID,
    api_key=os.getenv("SUPERBRYN_API_KEY"),
    extended_capture=False,  # legacy payload shape only
)
```

> **Note:** unlike the LiveKit SDK, a Pipecat observer has no access to
> transport-level WebRTC statistics, so there is no jitter / RTT / packet-loss
> data. `call.connection` captures the transport lifecycle events that *are*
> observable. LLM reasoning is reported as a count and character total only —
> the reasoning text itself is never transmitted.

## Prompt Sync

Keeps SuperBryn in step with the prompt your agent runs. Pass the prompt template (placeholders unfilled) to the observer:

```python
observer = SuperbrynObserver(
    agent_name="my-pipecat-bot",
    prompt=SYSTEM_PROMPT,     # the template your agent runs
    prompt_version="v13",     # optional: your label for this version
)
```

- Every call carries `metadata.prompt_ref = {"hash": "sha256:...", "version": "v13"}`.
- The template's text is pushed to SuperBryn once per process per version (`POST /public-api/v1/prompts`), with the tools found in the pipeline's LLM context.
- When you deploy a changed prompt, the agent's settings in SuperBryn show the change and offer to make it a new version. A prompt saved but never deployed never shows up, because no call names it.
- Prompt sync never holds a call back: if the push fails, the call is still sent, and it is matched once the prompt text arrives.
- The API key needs the `obs:write` scope. Keys generated in Monitor > Configure have it; with an older key the observer logs `SUPERBRYN_PROMPT_SYNC_FORBIDDEN` once and keeps sending calls.
- Without `prompt=`, the system prompt found in the pipeline's LLM context is sent as `metadata.system_prompt`. SuperBryn reads it as rendered text (it can differ per call), so a change shows up after a few calls instead of one.
- Simulation runs (`simulate_*`) never sync: they test drafts, not the live prompt. Set `sync_prompt=False` to turn it off.

Any stack can compute the hash itself: `sha256` over the template's UTF-8 bytes, written `sha256:<hex>` (`superbryn_pipecat_observer.prompt_ref(template, version)`).

## Agent Config Sync (opt-in)

Push your agent's configuration to SuperBryn as a reviewable draft. Requires an **agent-scoped** API key; nothing syncs unless you call it explicitly:

```python
from superbryn_pipecat_observer import sync_config

sync_config(
    pipeline,
    api_key="sk_agent_...",           # or SUPERBRYN_API_KEY
    identity={"name": "Support Agent", "type": "inbound", "agent_modality": "voice"},
    behavior={"prompt": open("prompt.txt").read()},
    policy_guardrails=open("guardrails.md").read(),
)
```

The helper walks the pipeline's processors — recursing into nested pipelines, `ParallelPipeline` branches, and `ServiceSwitcher` / `LLMSwitcher` members, and unwrapping custom wrapper classes — to fill the `llm` / `stt` / `tts` / `voice` blocks automatically. For switchers, the primary (initially active) service fills the block and the next member is reported in its `fallback` sub-block. It also reads the LLM context to fill `behavior.prompt` (the system message) and `tools` (the advertised function schemas) — explicit `behavior=` / `tools=` overrides always win. Extraction reads a fixed allow-list of configuration attributes (including private fields like `_settings`, `_model`, `_voice_id` where Pipecat services store their settings); credential attributes such as provider API keys are never part of that list and are never read or transmitted. The manifest lands as a pending draft in the SuperBryn dashboard for review; it never changes the live agent directly. Use `async_sync_config(...)` inside a running event loop, or `build_manifest_from_pipeline(...)` + `sync_manifest(...)` to inspect/modify the manifest before pushing.

Override sections accept exactly the fields of the canonical manifest schema (unknown keys raise `ValueError` locally — the endpoint rejects them anyway):

| Keyword | Fields |
|---|---|
| `identity` | `name`, `type` (`inbound`/`outbound`), `agent_modality` (`voice`/`chat`), `description`, `pain_point`, `gender`, `age`, `dob` |
| `behavior` | `prompt`, `flow` |
| `tools` | list of `{name, description, schema, server: {type, url}}` |
| `language` | `primary_language`, `additional_languages: [{code, priority}]` |
| `telephony` | `phone_number`, `ivr_config: {enabled, number}` |

Plus top-level strings/ints: `policy_guardrails`, `additional_details`, `concurrency_calls`. Sections the pipeline can't expose (identity, telephony, guardrails, ...) are supplied through these explicit overrides.

Docs: https://docs.superbryn.com/advanced/agent-sync

## Testing data (eval mode)

For SuperBryn test calls only, `attach_test_data` sends your agent's own record of the call to SuperBryn: `call.started` when the pipeline starts, and `call.ended` when it finishes, with the transcript, tool calls and diagnostics. Every other call sends nothing, and audio is never sent. It is independent of `SuperbrynObserver`; use either or both.

```python
from pipecat.pipeline.task import PipelineParams, PipelineTask
from superbryn_pipecat_observer import attach_test_data


async def bot(runner_args):
    ...
    task = PipelineTask(
        pipeline,
        params=PipelineParams(enable_metrics=True, enable_usage_metrics=True),
    )
    attach_test_data(task, runner_args)  # before the task runs
    await runner.run(task)
```

| Variable | Meaning |
|---|---|
| `SUPERBRYN_TEST_API_KEY` | Org API key with the `test_data:write` scope; falls back to `SUPERBRYN_API_KEY`. Without a key nothing is sent. |
| `SUPERBRYN_AGENT_ID` | Your SuperBryn agent ID (a UUID). Without it nothing is sent. |
| `SUPERBRYN_BASE_URL` | Defaults to `https://api.superbryn.com`. |

Keyword arguments override these (`api_key=`, `agent_id=`, `base_url=`), and `superbryn_call_id=`, `from_number=`, `to_number=`, `call_id=` and `call_data=` (a parsed telephony handshake) pass values your bot already knows.

A call is a SuperBryn test call when, in this order:

1. `superbryn_call_id=` is passed;
2. `runner_args.body` has `superbryn_call_id` (webhook, custom cURL and Pipecat Cloud starts);
3. a Daily dial-in SIP header ends in `x-superbryn-call-id`;
4. a telephony WebSocket carries a `superbryn_call_id` custom parameter (Twilio stream parameters, Exotel custom parameters), read from `call_data=`, else `runner_args.call_data`, else the handshake your bot parsed itself with `parse_telephony_websocket`;
5. otherwise, the caller or dialled number is one of SuperBryn's test numbers.

A value that still holds a template placeholder such as `{From}` is ignored, with a warning. A call with neither a test ID nor phone numbers logs one `INFO` line and sends nothing.

`call.ended` carries:

- `transcript`: user turns timed by VAD (or, in realtime pipelines without VAD, by the service's own turn frames) with their final transcripts; what the user says while a user mute strategy is active is left out, as the agent never hears it. Agent turns run from bot started to bot stopped speaking, with the text the output transport played and whether the user interrupted; the LLM text stands in only when the pipeline never reports played text, and never for an interrupted turn.
- `tool_calls`: name, arguments, result, start and end, and the agent turn each belongs to.
- `diagnostics.turn_latencies`: per reply, `turnLatency` (user stopped speaking to bot started), `endpointingLatency` (user stopped speaking to the turn's release, including the stop strategy's wait), `transcriberLatency`, `modelLatency` and `voiceLatency`.
- `diagnostics.configuration`: STT, LLM and TTS provider and model, the TTS voice, turn detection, and the LLM's tools.
- `diagnostics.usage`: LLM input, cached input and output tokens, and TTS characters.
- `diagnostics.errors`: every error frame by pipeline step (`stt`, `llm`, `tts`, or `telephony` for a transport), whether that step worked again later, and a fatal one never recovered. A tool handler that raised is `is_error` on its tool call instead.
- `diagnostics.configuration.interruptions`: from the user turn start strategies: enabled, `min_words` (the min-words strategy) and `min_duration_ms` (VAD's start time when the VAD strategy is used).
- Caller turns carry the STT's `confidence` when the provider's result has one (Deepgram, Google, Gladia, AWS Transcribe, Soniox), and `usage.stt_audio_ms` is the STT usage Pipecat reports, or on Pipecat 1.4 (which reports none) the audio delivered to a streaming STT while it isn't muted; a segmented STT gets none.
- `diagnostics.llm_requests`: every LLM response, with when it started, its tokens and what became of it: `spoken`, `cut_off` (the caller cut in), `not_spoken` (generated, never played), `stopped` (nothing came back) or `tool_call` (only a tool call).

Latency and usage need `enable_metrics=True` and `enable_usage_metrics=True` in `PipelineParams`; a warning is logged when either is off. Attaching after the task started logs a warning and times the call from the first frame seen. When the pipeline finishes, sending `call.ended` gets at most 12 seconds; a call without a test ID that ends before the SuperBryn-number check finishes sends nothing and adds no wait. Log lines start with `SUPERBRYN_TEST_DATA`; a `401` or `403` means the key lacks `test_data:write`.

## How It Works

1. **Pipeline observation.** The observer is registered via `PipelineTask(observers=[...])` — it runs **alongside** your pipeline, not inside it.
2. **Frame inspection.** `on_push_frame` watches every frame flowing between processors and aggregates by class name, so a Pipecat upgrade doesn't break it.
3. **Auto-detection.** The observer inspects each service's module path (`pipecat.services.<provider>.<role>`) to tag LLM / STT / TTS provider, model, and voice ID automatically.
4. **End-of-session delivery.** When the call ends (`EndFrame` / `CancelFrame` / transport disconnect), the SDK builds a normalized call payload and POSTs it to SuperBryn.

## Webhook Payload

```json
{
  "event": "call.completed",
  "sdk_version": "@superbryn/pipecat-observer@0.11.0",
  "call": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "started_at": "2026-06-15T12:00:00.000+00:00",
    "ended_at": "2026-06-15T12:05:30.000+00:00",
    "duration_seconds": 330.0,
    "call_end_reason": "completed",
    "from_number": "+15551234567",
    "to_number": "+15557654321",
    "transcript": {
      "turns": [
        {
          "speaker": "user",
          "text": "Hello, how are you?",
          "start_time_ms": 5000,
          "end_time_ms": 5000,
          "confidence": 0.98
        },
        {
          "speaker": "agent",
          "text": "I'm doing great, thanks for asking!",
          "start_time_ms": 7000,
          "end_time_ms": 11000,
          "latency_ms": 2000
        }
      ]
    },
    "metadata": {
      "agent_id": "support-bot",
      "agent_name": "support-bot",
      "transport": "daily",
      "llm_provider": "openai",
      "llm_model": "gpt-4o-mini",
      "stt_provider": "deepgram",
      "stt_model": "nova-3",
      "tts_provider": "cartesia",
      "tts_model": "sonic-english",
      "tts_voice_id": "...",
      "pipeline_version": "@superbryn/pipecat-observer@0.11.0",
      "mode": "observe"
    },
    "usage": {
      "llm_input_tokens": 1250,
      "llm_output_tokens": 850,
      "stt_duration_seconds": 45.2,
      "tts_characters": 1200,
      "llm_total_tokens": 2100,
      "llm_cache_read_input_tokens": 800,
      "llm_reasoning_tokens": 64,
      "llm_input_audio_tokens": 0
    },
    "latency": {
      "avg_ms": 750.5,
      "p95_ms": 1240.0,
      "avg_ttfb_ms": 335.0,
      "p95_ttfb_ms": 480.0,
      "avg_ttfa_ms": 260.0,
      "avg_tts_leading_silence_ms": 70.0,
      "avg_processing_ms": 1100.0,
      "avg_text_aggregation_ms": 50.0,
      "avg_turn_detection_ms": 310.0,
      "by_service": {
        "AnthropicLLM": { "avg_ttfb_ms": 480.0, "avg_processing_ms": 1100.0 },
        "CartesiaTTS": { "avg_ttfb_ms": 190.0, "avg_ttfa_ms": 260.0 }
      }
    },
    "tool_calls": [
      {
        "tool_call_id": "call_abc123",
        "function_name": "get_cart",
        "arguments": { "user_id": "u_42" },
        "result": { "items": [] },
        "timestamp_ms": 8200
      }
    ],
    "turn_detection": {
      "avg_e2e_processing_ms": 310.0,
      "max_e2e_processing_ms": 540.0,
      "prediction_count": 12,
      "incomplete_predictions": 3,
      "avg_probability": 0.91,
      "events": [
        { "timestamp_ms": 5200, "is_complete": true, "probability": 0.94, "e2e_processing_time_ms": 302.0 }
      ]
    },
    "speech_stats": {
      "user_talk_seconds": 41.2, "agent_talk_seconds": 88.6,
      "user_turn_count": 12, "agent_turn_count": 13,
      "avg_user_turn_seconds": 3.4, "avg_agent_turn_seconds": 6.8,
      "agent_talk_ratio": 0.683, "silence_seconds": 20.2, "longest_silence_ms": 4100,
      "avg_response_delay_ms": 910.4, "max_response_delay_ms": 2210.0
    },
    "interruptions": { "interruption_count": 2, "bot_turns_interrupted": 2, "user_idle_timeouts": 0 },
    "errors": [
      { "timestamp_ms": 84200, "message": "deepgram websocket closed: 1011",
        "fatal": false, "processor": "DeepgramSTTService", "exception_type": "ConnectionClosed" }
    ],
    "connection": [
      { "type": "ClientConnectedFrame", "timestamp_ms": 120 },
      { "type": "OutputTransportReadyFrame", "timestamp_ms": 180 }
    ],
    "sip": { "attributes": {}, "dtmf": [ { "timestamp_ms": 32000, "digit": "1", "direction": "input" } ] },
    "vad": {
      "segment_count": 12, "total_speech_seconds": 41.2, "avg_segment_ms": 3433.0,
      "params": { "start_secs": 0.2, "stop_secs": 0.8 },
      "segments": [ { "start_ms": 5000, "end_ms": 7500, "duration_ms": 2500 } ]
    },
    "pipeline": {
      "services_seen": ["AnthropicLLMService", "CartesiaTTSService", "DeepgramSTTService"],
      "service_metadata": {
        "deepgram": { "ttfs_p99_ms": 420.0 },
        "anthropic": { "is_realtime_service": false }
      },
      "frame_counts": { "InterruptionFrame": 2, "TranscriptionFrame": 12 },
      "llm_thought_count": 0, "llm_thought_chars": 0
    },
    "environment": {
      "observer_version": "0.8.0", "pipecat_version": "1.7.0", "python_version": "3.12.12"
    }
  }
}
```

`tool_calls` is included only when the LLM invoked one or more tools.

## Troubleshooting

### Enable debug logs

```python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("superbryn_pipecat_observer").setLevel(logging.DEBUG)
```

### Log messages to look for

| Log prefix | Meaning |
|---|---|
| `SUPERBRYN_PIPECAT_CALL_STARTED` | Observer initialized for this session |
| `SUPERBRYN_PIPECAT_SENDING` | Webhook about to be sent |
| `SUPERBRYN_PIPECAT_PAYLOAD` | Full outbound payload (debug aid) |
| `SUPERBRYN_PIPECAT_SENT` | Webhook delivered successfully (HTTP 200) |
| `SUPERBRYN_PIPECAT_SKIPPED` | No API key configured — observer no-ops |
| `SUPERBRYN_PIPECAT_AUTH_FAILED` | Invalid or revoked API key (401/403) |
| `SUPERBRYN_PIPECAT_PAYLOAD_TOO_LARGE` | JSON exceeded the 20 MB backend cap |
| `SUPERBRYN_PIPECAT_HTTP_*` | Non-2xx response from the SuperBryn server |
| `SUPERBRYN_PIPECAT_ERROR` | Network / exception during delivery |
| `SUPERBRYN_PIPECAT_MISSING_AIOHTTP` | `aiohttp` not installed in the runtime |
| `SUPERBRYN_PIPECAT_SESSION_DISCARDED` | Session aborted — no webhook sent |

### Common errors

| Error | Cause | Fix |
|---|---|---|
| `SUPERBRYN_API_KEY not configured` | Missing API key | Set `SUPERBRYN_API_KEY` in your environment |
| `SUPERBRYN_PIPECAT_AUTH_FAILED (401)` | Invalid API key | Verify the key in SuperBryn → API keys |
| `SUPERBRYN_PIPECAT_AUTH_FAILED (403)` | Expired / disabled key | Generate a new API key |
| `SUPERBRYN_PIPECAT_MISSING_AIOHTTP` | `aiohttp` missing | `pip install aiohttp>=3.9.0` |
| `SUPERBRYN_PIPECAT_PAYLOAD_TOO_LARGE` | Transcript / logs too large | Trim `custom_metadata` or pass `capture_logs=False` |

### Missing usage metrics

Pipecat only emits `MetricsFrame`s when usage metrics are enabled. `monitor_and_create_task` / `simulate_and_create_task` / `register_task_handlers` switch it on for you, also on your own `PipelineParams`, unless you set `enable_usage_metrics=False` (a `SUPERBRYN_PIPECAT_METRICS_OFF` warning is logged). If you attach the observer to your own `PipelineTask` without `register_task_handlers`, pass `enable_usage_metrics=True`:

```python
PipelineTask(
    pipeline,
    observers=[observer],
    params=PipelineParams(enable_usage_metrics=True),
)
```

Without it you'll still get the transcript, latency, and provider detection — but `llm_input_tokens`, `llm_output_tokens`, and `tts_characters` will be `0`.

## Links

- [Pipecat documentation](https://docs.pipecat.ai/)
- [GitHub repository](https://github.com/superbryndev/superbryn-pipecat-observer)
- [Issue tracker](https://github.com/superbryndev/superbryn-pipecat-observer/issues)
- [Get an API key](https://app.superbryn.com/api-keys)

## Support

- Email: support@superbryn.com
- GitHub issues: [Report a bug](https://github.com/superbryndev/superbryn-pipecat-observer/issues)

## License

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

---

Made with ❤️ by [SuperBryn](https://www.superbryn.com)
