You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(learnings/voice-providers): document Vapi-native voices + SSML rejection on provider:vapi
The voice catalog (Clara, Elliot, Nico, Emma, Neil, Sagar, Kai, Godfrey,
Naina, Sid, Layla, Gustavo) wraps Cartesia / ElevenLabs / other backends
behind a single 'provider: vapi' alias. Verified empirically that
voice.enableSsmlParsing returns HTTP 400 'should not exist' on this
provider — the field is only valid on provider: 11labs voices.
Adds: new column in the quick-lookup table, a dedicated Vapi-native
voices section with the field cheat-sheet, an explanation of when SSML
break tags do/don't render based on the wrapped backend, and a four-step
smoke-test protocol for confirming SSML behavior on Vapi-aliased voices.
Worth noting for future readers: Vapi's voice-formatting pipeline
preserves <break>, <spell>, and <<...>> through angle-bracket stripping,
so even when the wrapped backend ignores SSML, tags don't leak as
literal spoken text — worst case is silent no-op.
| SSML parsing |`voice.enableSsmlParsing: true`| (parsed natively, no flag) | varies — see provider docs |
17
-
| Pronunciation dictionary |`voice.pronunciationDictionaryLocators[]` (array of `{pronunciationDictionaryId, versionId}`) |`voice.pronunciationDictId` (single string id; not in Vapi docs but accepted as a Cartesia passthrough) | — |
18
-
| Volume control | — |`voice.generationConfig.volume` (0.5–2.0) | — |
| SSML parsing |`voice.enableSsmlParsing: true`| (parsed natively, no flag) |**`enableSsmlParsing` REJECTED** — see notes below |varies — see provider docs |
17
+
| Pronunciation dictionary |`voice.pronunciationDictionaryLocators[]` (array of `{pronunciationDictionaryId, versionId}`) |`voice.pronunciationDictId` (single string id; not in Vapi docs but accepted as a Cartesia passthrough) | — (treat as unsupported until confirmed) | — |
Vapi's first-party voice catalog wraps various TTS backends (Cartesia, ElevenLabs, others) behind a single `provider: vapi` alias. Which backend each named voice resolves to is **not publicly documented** — you can sometimes confirm by force-pulling an assistant that uses one (the pull engine occasionally resolves `vapi/<voiceId>` to the canonical provider name on the way back) or by inspecting the per-voice characteristics page in the Vapi voice docs.
76
+
77
+
```yaml
78
+
voice:
79
+
provider: vapi
80
+
voiceId: <voiceName> # one of the Vapi catalog: Clara, Elliot, Nico, Emma, Neil, Sagar, Kai, Godfrey, Naina, Sid, Layla, Gustavo (grows over time)
81
+
speed: 1.05 # top-level, 0.7–1.2 range observed
82
+
chunkPlan:
83
+
formatPlan:
84
+
numberToDigitsCutoff: 10000 # numbers below this are spoken naturally; above are spelled digit-by-digit
85
+
fallbackPlan:
86
+
voices:
87
+
- provider: 11labs
88
+
model: eleven_turbo_v2_5
89
+
voiceId: <fallback-voice-id>
90
+
```
91
+
92
+
**Forbidden at top level for Vapi-native voices (will 400):**
93
+
94
+
- `voice.enableSsmlParsing` — API returns `400 Bad Request` with `"voice.property enableSsmlParsing should not exist"`. The field is **only valid on `provider: 11labs` voices**. Vapi-native voices do not expose an explicit SSML-parsing opt-in flag, even if the underlying backend (e.g. Cartesia sonic-3) parses SSML natively.
95
+
- `voice.stability`, `voice.similarityBoost` — those are 11labs fields and will 400 here too.
96
+
- `voice.generationConfig.*` — Cartesia path. Even if a Vapi-native voice wraps Cartesia internally, the `generationConfig` block is not exposed through the `provider: vapi` alias.
97
+
98
+
**Does SSML actually work without the flag?** Depends on the wrapped backend:
99
+
100
+
- If the Vapi voice wraps **Cartesia sonic-3**, SSML break tags work natively — Cartesia parses SSML from the text stream without an opt-in. Pause durations should approximate the declared values.
101
+
- If the Vapi voice wraps **11labs Turbo v2.5**, SSML break tags would require `enableSsmlParsing: true` — but the flag is rejected on the `provider: vapi` alias, so SSML effectively does not render through this path. To use SSML on an 11labs-backed voice, declare it explicitly as `provider: 11labs` with the matching `voiceId`.
102
+
- **Vapi's voice-formatting pipeline preserves `<break>`, `<spell>`, and `<<...>>` patterns through angle-bracket stripping** (the `removeAngleBracketContent` step has these as hardcoded exceptions — see `assistants/voice-formatting-plan` in the Vapi docs). So even when the wrapped backend doesn't render the tags, they don't leak as literal text either. Worst-case behavior is silent no-op, NOT audible regression like "the assistant says 'break time 350 milliseconds' out loud."
103
+
104
+
**Smoke-test protocol** when adding SSML pacing to a `provider: vapi` voice (or migrating from `provider: 11labs` to `provider: vapi`):
105
+
106
+
1. Push the new voice config to a non-prod sandbox assistant.
107
+
2. Place one call that elicits a 3+ item enumeration (e.g. "what features does X have?").
108
+
3. Listen for whether `<break time="..."/>` tags between list items render as audible pauses or get silently absorbed.
109
+
4. If absorbed and pauses are needed for naturalness, either (a) switch the primary voice to `provider: 11labs` explicitly (with `enableSsmlParsing: true`), (b) use Cartesia natively, or (c) rely on commas + sentence punctuation as the prosody mechanism (Cartesia sonic-3 renders commas as ~150–200ms pauses, which covers most sentence-internal pacing without SSML).
110
+
111
+
**Cross-reference:** the comma-as-micro-pause convention works on most TTS providers and is the recommended fallback for sentence-internal pacing when SSML support is unreliable. The SSML-only-for-lists convention works on Cartesia-wrapped Vapi voices but should be smoke-tested before relying on it for a production fleet.
0 commit comments