Last updated 2026-05-17

ElevenLabs Provider

The elevenlabs provider implements both Speaker (text-to-speech) and Transcriber (speech-to-text via the Scribe family). It does not implement Provider — ElevenLabs has no chat endpoint. Pair it with one of the chat providers (OpenAI, Anthropic, or any other) for end-to-end voice pipelines.

Import path

import (
"github.com/elloloop/llmrouter"
"github.com/elloloop/llmrouter/providers/elevenlabs"
)

Default endpoint

With no WithBaseURL override, the provider targets https://api.elevenlabs.io/v1. TTS requests go to {BaseURL}/text-to-speech/{voice_id} (or /text-to-speech/{voice_id}/stream when SpeechRequest.Stream is true). STT requests go to {BaseURL}/speech-to-text.

Construction

p, err := elevenlabs.New(
llmrouter.WithAPIKey(os.Getenv("ELEVENLABS_API_KEY")),
)
if err != nil {
log.Fatal(err)
}

Authentication is via the xi-api-key header — the provider sets it automatically from the API key. There is no Bearer token form for ElevenLabs.

Text-to-speech

Models

  • eleven_turbo_v2_5 — lowest latency, recommended for conversational and real-time use cases. Multilingual (32 languages).
  • eleven_multilingual_v2 — highest quality, 29 languages. Higher latency than turbo.
  • eleven_flash_v2_5 — ultra-low latency (~75 ms), English-only.
  • eleven_monolingual_v1 — legacy, English-only.

Voice IDs

ElevenLabs voices are referenced by opaque IDs (often 20-character alphanumeric strings) — set SpeechRequest.Voice to the ID, not the display name. Find IDs in your account at elevenlabs.io/app/voice-library or via the ElevenLabs /v1/voices endpoint. A handful of public voice IDs (subject to change):

Voice ID Notes
Rachel21m00Tcm4TlvDq8ikWAMdefault female
AdampNInz6obpgDQGcFmaJgBdefault male
BellaEXAVITQu4vr4xnSDxMaLsoft female
AntoniErXwobaYiN019PkySvjVnarrator male

Output format mapping

ElevenLabs uses compound format codes ({codec}_{sample_rate}_{bitrate}). The library maps the canonical Format field as follows:

Library Format ElevenLabs output_format
mp3 (default)mp3_44100_128
pcmpcm_22050
ulawulaw_8000

For non-default sample rates or bitrates, set Format="" and pass output_format on Raw.

Streaming TTS example

package main
import (
"context"
"log"
"os"
"github.com/elloloop/llmrouter"
"github.com/elloloop/llmrouter/providers/elevenlabs"
)
func main() {
p, err := elevenlabs.New(llmrouter.WithAPIKey(os.Getenv("ELEVENLABS_API_KEY")))
if err != nil {
log.Fatal(err)
}
stream, err := p.Speak(context.Background(), llmrouter.SpeechRequest{
Model: "eleven_turbo_v2_5",
Input: "Welcome to the future of voice synthesis.",
Voice: "21m00Tcm4TlvDq8ikWAM", // Rachel
Format: "mp3",
Stream: true,
})
if err != nil {
log.Fatal(err)
}
out, _ := os.Create("welcome.mp3")
defer out.Close()
for chunk := range stream.Chunks() {
out.Write(chunk.Data)
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
}

Voice stability and style

ElevenLabs' voice_settings object (stability, similarity_boost, style, use_speaker_boost) is not on the typed surface. Pass it via Raw:

raw := []byte(`{
"voice_settings": {
"stability": 0.65,
"similarity_boost": 0.85,
"style": 0.2,
"use_speaker_boost": true
}
}`)
stream, err := p.Speak(ctx, llmrouter.SpeechRequest{
Model: "eleven_multilingual_v2",
Input: "Hello world.",
Voice: "21m00Tcm4TlvDq8ikWAM",
Raw: raw,
})

Speech-to-text (Scribe)

Models

  • scribe_v1 — current production model. 99 languages, word-level timing, speaker diarization, and (optionally) profanity filtering.

Language detection

Scribe auto-detects language by default. Set TranscribeRequest.Language to an ISO-639-1 code to skip detection and improve accuracy on short clips.

STT example

f, err := os.Open("interview.m4a")
if err != nil {
log.Fatal(err)
}
defer f.Close()
stream, err := p.Transcribe(ctx, llmrouter.TranscribeRequest{
Model: "scribe_v1",
Audio: f,
AudioFormat: "audio/m4a",
Filename: "interview.m4a",
Language: "en",
ResponseFormat: "verbose_json",
})
if err != nil {
log.Fatal(err)
}
for seg := range stream.Segments() {
fmt.Printf("[%s] %s\n", seg.Start, seg.Text)
for _, w := range seg.Words {
fmt.Printf(" %s @ %s\n", w.Word, w.Start)
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}

Speaker diarization

Diarization is enabled via Scribe's diarize field, which is not on the typed surface. Pass it via Raw:

raw := []byte(`{"diarize": true, "num_speakers": 2}`)
stream, err := p.Transcribe(ctx, llmrouter.TranscribeRequest{
Model: "scribe_v1",
Audio: f,
AudioFormat: "audio/m4a",
Raw: raw,
})
// Each segment's Raw json contains "speaker_id" when diarize is on.
for seg := range stream.Segments() {
fmt.Printf("%s\n", string(seg.Raw))
}

Error handling

Non-2xx responses surface as *llmrouter.ErrUpstream with Provider == "elevenlabs". Common cases:

  • 401 — bad or missing API key.
  • 402 — quota exhausted (insufficient characters).
  • 422 — invalid voice ID or model id.
  • 429 — concurrent-request limit hit.

Caveats

  • No chat. ElevenLabs does not offer text generation. Calling CompletionStream on this provider is a compile error — the type doesn't have the method.
  • Streaming TTS uses HTTP chunked transfer, not SSE. The library wraps the stream identically to other providers, so consumer code is unchanged.
  • STT uses multipart upload. The Audio io.Reader is consumed once; for retries, the caller must rewind.

See also