Last updated 2026-05-17

Deepgram Provider

The deepgram provider implements Transcriber (speech-to-text). It does not implement Speaker, Provider, or Embedder — Deepgram is an STT-only vendor in llmrouter's scope.

Import path

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

Default endpoint

With no WithBaseURL override, the provider targets https://api.deepgram.com/v1. Pre-recorded transcription requests go to {BaseURL}/listen.

WebSocket streaming (wss://api.deepgram.com/v1/listen) is on the v0.4 roadmap; v0.3 supports pre-recorded transcription only. Setting TranscribeRequest.Stream = true currently falls back to a single final segment.

Construction

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

Authentication is via the Authorization: Token <key> header — note the Token scheme, not Bearer. The provider sets this automatically.

Models

  • nova-3 — latest production model (May 2025). Highest accuracy, lowest latency. Recommended default. 36 languages.
  • nova-2 — previous generation. Wider language coverage in some edge cases.
  • nova-2-meeting, nova-2-phonecall, nova-2-finance, nova-2-medical — domain-tuned variants of nova-2.
  • enhanced — older non-Nova model. Still supported.
  • base — legacy model. Lowest cost, lowest accuracy.

Language codes

Deepgram uses ISO-639-1 codes plus optional regional suffixes (en-US, en-GB, es-419, pt-BR, etc.). The library passes TranscribeRequest.Language through unchanged — set either the bare ISO code or the regional variant. Empty means "auto-detect."

Response format

Deepgram returns JSON with channels, alternatives, and per-word timing. The library always emits TranscriptSegments. With ResponseFormat="" or "json", you get one Final=true segment with the concatenated transcript and Words populated. With ResponseFormat="verbose_json", the library splits per Deepgram utterance into multiple segments.

Full STT example

package main
import (
"context"
"fmt"
"log"
"os"
"github.com/elloloop/llmrouter"
"github.com/elloloop/llmrouter/providers/deepgram"
)
func main() {
p, err := deepgram.New(llmrouter.WithAPIKey(os.Getenv("DEEPGRAM_API_KEY")))
if err != nil {
log.Fatal(err)
}
f, err := os.Open("call.wav")
if err != nil {
log.Fatal(err)
}
defer f.Close()
stream, err := p.Transcribe(context.Background(), llmrouter.TranscribeRequest{
Model: "nova-3",
Audio: f,
AudioFormat: "audio/wav",
Language: "en-US",
ResponseFormat: "verbose_json",
})
if err != nil {
log.Fatal(err)
}
for seg := range stream.Segments() {
fmt.Printf("[%s] (%.2f) %s\n", seg.Start, seg.Confidence, seg.Text)
for _, w := range seg.Words {
fmt.Printf(" %-20s @ %s (%.2f)\n", w.Word, w.Start, w.Confidence)
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
}

Speaker diarization

Deepgram's diarize parameter is not on the typed surface. Pass it via Raw:

raw := []byte(`{
"diarize": true,
"smart_format": true,
"punctuate": true,
"utterances": true
}`)
stream, err := p.Transcribe(ctx, llmrouter.TranscribeRequest{
Model: "nova-3",
Audio: f,
AudioFormat: "audio/wav",
Language: "en",
Raw: raw,
})
// Per-word speaker ids land on segment.Raw.
for seg := range stream.Segments() {
fmt.Println(string(seg.Raw))
}

Diarized output puts speaker on each word in the wire JSON. Read it off TranscriptSegment.Raw until the typed surface grows a SpeakerID field (tracked in the v0.4 work).

Confidence scores

Deepgram returns per-word confidence and an aggregate per-segment confidence. Both are populated:

  • TranscriptSegment.Confidence — aggregate over the segment, in [0, 1].
  • TranscriptWord.Confidence — per-word, in [0, 1].

Other features via Raw

Deepgram has a large feature surface; the typed TranscribeRequest models the common ones, and the rest travel through Raw:

  • smart_format — automatic punctuation and number formatting.
  • punctuate — add punctuation without smart formatting.
  • profanity_filter — replace profanity with asterisks.
  • redact — redact PII (pci, numbers, ssn).
  • summarize — append a summary segment.
  • detect_topics — surface topic labels.
  • keywords — boost specific terms.
  • search — return matches for query phrases.

All of these survive byte-passthrough and surface on segment.Raw.

Error handling

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

  • 401 — bad or missing API key.
  • 402 — insufficient credit.
  • 400 — unsupported model + language combination, or invalid AudioFormat.
  • 429 — concurrent-request limit hit.

Caveats

  • v0.3: pre-recorded only. WebSocket streaming lands in v0.4. The Stream flag is accepted but the provider currently treats it as a no-op on pre-recorded audio.
  • No TTS. Deepgram offers an Aura TTS endpoint; in v0.3, llmrouter only wraps the STT side. Consider Cartesia or ElevenLabs for TTS.

See also