Skip to content

Meter TTS usage on characters sent to the provider - #10

Merged
shubhamthink41 merged 2 commits into
feat/stt-usage-meteringfrom
feat/tts-usage-metering
Jul 16, 2026
Merged

Meter TTS usage on characters sent to the provider#10
shubhamthink41 merged 2 commits into
feat/stt-usage-meteringfrom
feat/tts-usage-metering

Conversation

@abhinav-t41

@abhinav-t41 abhinav-t41 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

TTS providers charge for the characters you send them, not the characters the user ends up hearing. Pipecat forwards each sentence to the TTS service as soon as the LLM produces it, which is usually a few sentences ahead of what's actually playing. So when a user interrupts, the provider has already received text it will never speak, and we still pay for it. Counting characters from the transcript will always come in under the real bill.

We also can't lean on Pipecat's built-in usage metric. The websocket Deepgram service never fires it, and the base class throws away its accumulated text on interruption, which is the one case we care about most.

The fix is to count it ourselves inside run_tts, since that's where text actually goes out to the provider. It's the same trick the STT byte meter already uses for audio.

Changes

  • TtsUsageMeterMixin counts len(text) in run_tts on both AdkDeepgramTTSService and AdkElevenLabsTTSService — the exact prepared text sent to the provider.
  • One usage.tts trace event per session at teardown, next to usage.stt: {provider, model, voice, sent_characters}. Usage facts only; cost is computed later from the pricing catalog.
  • Rewrote docs/cost-metering-design.md §6: the earlier plan (flip enable_usage_metrics=True, read TTSUsageMetricsData in MetricsSink) doesn't work — the Deepgram websocket service never emits that metric, and interrupted turns are dropped by the base class. Also documents verified billing semantics: sent = billed for Deepgram; for ElevenLabs (credits deducted on successful generation, context closed on interruption) sent_characters is a tight upper bound.
  • Tests in server/tests/test_tts_usage_meter.py, mirroring the STT meter tests.

Verification

  • uv run pytest: 4 new tests pass; the 2 test_agent_config failures also fail on the base branch (pre-existing).
  • uv run ruff check .: the 4 reported errors pre-exist on the base branch; this change adds none.
  • Calibration to do after merge: run one deliberately-interrupted session per provider and reconcile sent_characters against the Deepgram console per-request character counts and the ElevenLabs subscription character counter (design doc §6.4).

Stacked on #9 (feat/stt-usage-metering) since it extends the same runtime file and teardown block.

abhinav-t41 and others added 2 commits July 16, 2026 10:43
TTS providers bill on characters sent over the wire, not characters the
user hears: Pipecat pushes each sentence to the provider as the LLM
streams it, so on interruption Deepgram has already received (and bills
for) text whose audio is discarded by Clear. Pipecat's built-in
TTSUsageMetricsData metric cannot measure this — the Deepgram websocket
service never emits it, and the base-class fallback drops accumulated
text on interruption, which is exactly where billing diverges from the
transcript.

Add TtsUsageMeterMixin counting characters at the run_tts seam — the
exact prepared text sent to the provider — on both instrumented TTS
services, and emit a usage.tts trace event (provider, model, voice,
sent_characters) at session teardown next to usage.stt.

Also rewrite docs/cost-metering-design.md §6 with the verified billing
semantics: sent = billed for Deepgram; ElevenLabs deducts credits on
successful generation so sent_characters is a tight upper bound there;
per-provider differences belong in the pricing catalog, not the meter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shubhamthink41
shubhamthink41 merged commit b5961bd into feat/stt-usage-metering Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants