Voice AI
All recipes
PythonTypeScriptIntermediate

Custom Modalities

Bring your own LLM and a custom TTS voice.

Recipe prompt

Paste into Cursor, Claude Code, v0, or your coding agent
Use with your coding agent
You are implementing the "Custom Modalities" recipe in this project.

Read the recipe markdown first:
https://raw.githubusercontent.com/AgoraIO-Conversational-AI/recipe-agent-custom-llm-tts/main/README.md

Use the source repository for cross-reference:
https://github.com/AgoraIO-Conversational-AI/recipe-agent-custom-llm-tts

Build this recipe into the user's app using the markdown as the implementation guide. Inspect related source files through the repository links when the recipe points to them. Ask before installing new dependencies.

Recipe

Rendered from the configured recipe markdown.

Raw

Agora Conversational AI — Custom LLM-TTS Recipe (Python)

The custom-llm-tts recipe in the Agora Conversational AI recipes family. Your endpoint returns audio directly — playing both the LLM and TTS roles — so Agora plays it over RTC with no separate TTS step. STT (Deepgram) still transcribes the user's speech for your endpoint.

This repo ships a zero-key mock that emits a sine-wave tone, so you can run the full STT → custom audio endpoint → RTC pipeline immediately, then replace the mock.

Prerequisites

Run it

# 1. Install web deps + create the Python venv
bun run setup

# 2. Add Agora credentials (CLI), or edit server/.env.local by hand
agora login
agora project use <your-project>
agora project env write server/.env.local

# 3. Expose the backend publicly (Agora cloud calls the mounted /audio endpoint)
ngrok http 8000

# 4. Add the tunnel URL (note the /audio path) to server/.env.local
#    CUSTOM_LLM_URL=https://<your-tunnel>.ngrok-free.dev/audio/chat/completions

# 5. Run all three services
bun run dev

Open http://localhost:3000Start Conversation → speak. You'll hear the mock tone as the agent's reply.

Architecture

Browser (localhost:3000)
  │  fetch /api/*
  ▼
Next.js  ──rewrite──▶  Agent backend  (server/, localhost:8000)
                          │  CustomLLM(output_modalities=["audio"])
                          ▼
                       Agora ConvoAI Cloud
                          │  POST <CUSTOM_LLM_URL>   (Authorization: Bearer)
                          ▼
                       Custom audio endpoint  (mounted at /audio in server/, :8000)
                          │  returns transcript + PCM audio (SSE)
                          ▲  public via ngrok tunnel
                       (no TTS — audio plays straight to RTC)

See ARCHITECTURE.md.

Project structure

recipe-agent-custom-llm-tts/
├── server/   # Single backend (:8000) — token/agent endpoints + mounted /audio endpoint
│   ├── src/{server.py, agent.py, llm.py}   # llm.py: POST /audio/chat/completions, no agora deps
│   ├── scripts/run_fake_server.py
│   └── tests/{conftest.py, test_llm_mount.py}
├── web/      # Shared Next.js frontend (:3000)
└── package.json

Environment variables

Backend env file: `server/.env.example`.

VariableRequiredDefaultNotes
AGORA_APP_IDAgora Console → Project → App ID
AGORA_APP_CERTIFICATEAgora Console → Project → App Certificate (server only)
CUSTOM_LLM_URLPublic URL of the mounted /audio endpoint, ending in /audio/chat/completions. Agora cloud calls it; cannot be localhost.
CUSTOM_LLM_API_KEYany-key-hereForwarded by Agora cloud as Authorization: Bearer. Required by the CustomLLM vendor.
CUSTOM_LLM_MODELaudio-mockModel name passed to your endpoint
AGENT_GREETINGbuilt-inOpening line (supported in audio mode via the messages protocol)
PORT8000Backend port (serves the token/agent endpoints and /audio)
AGENT_BACKEND_URL (web deploy)Required in a deployed web app when proxying to the backend

Commands

bun run setup            # install web deps + create the server/ venv
bun run dev              # run backend (:8000) + web (:3000)

bun run doctor           # prerequisite check (no creds needed)
bun run doctor:local     # + .env.local + credentials + CUSTOM_LLM_URL checks

bun run verify           # web-only gate (no Agora creds needed)
bun run verify:local     # full local gate: backend compile + smoke tests + web build
bun run clean            # remove venvs and build artifacts

Replacing the mock

Replace generate_tone() in `server/src/llm.py` with your real audio source. Keep the SSE contract (transcript chunk + base64 PCM16/16kHz chunks + [DONE]) — the transcript is required for agent context. Keep llm.py free of agora-agents (a test enforces this). See `server/README.md`.

Troubleshooting

ProblemFix
Agent joins but no audio / garbled audioCUSTOM_LLM_URL must be public and end in /audio/chat/completions; audio must be PCM16/16kHz/mono.
Agent doesn't remember contextYour endpoint must include audio.transcript in the first chunk.
doctor:local warns about localhostReplace the local URL with your public tunnel URL.
Local calls fail under a global proxy (Clash, etc.)Route 127.0.0.1/localhost/RFC-1918 DIRECT in your proxy (don't disable it).
Missing server/venv during verifyRun bun run setup.

License

MIT