Repository navigation
Conversation
GenerateSpeechAsync used the SDK defaults: 100s per attempt and 3 retries.
In production some /v1/audio/speech requests never return, so a failed call
blocked the caller for about 7 minutes ("Retry failed after 4 tries").
AudioSynthesisProvider now waits at most 30s per attempt and keeps 3 retries.
Both can be set per model under Audio.Synthesis (TimeoutSeconds, MaxRetries).
ProviderHelper.GetClient takes the timeout and retry count as optional
arguments; other callers keep the SDK defaults.
Verified against a local server that never answers: 4 attempts about 30s
apart, failing after 125.7s (was ~400s); a hang followed by a good response
recovers on the retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Say mode (Twilio:SayGeneratedSpeech, off by default)
- Generated speech is saved as a text file and spoken with <Say voice=SayVoice>
instead of pre-generating an mp3 with the audio synthesizer: the opening line
(no longer synthesized before dialing), replies, hang-up, transfer, voicemail
and tool-call indications.
- Prerecorded prompts (twilio/{name}.mp3) are spoken from Twilio:PromptTexts
so the whole call uses one voice; prompts without text keep playing.
- TwilioService.SaveSpeechAsync / AppendSpeech are the single places that
write and play speech. With the switch off, behaviour is unchanged.
ConversationRelay (Twilio:ConversationRelay, off by default)
- Outbound calls for enabled agents (AgentIds, empty = all) are answered with
<Connect><ConversationRelay>: Twilio transcribes and speaks, and exchanges
text with TwilioConversationRelayMiddleware at /twilio/relay/{agent}/{conv}.
- ConversationRelaySession runs one agent turn at a time (later words are
merged into the next turn), speaks tool-call indications, re-prompts on
caller silence, hangs up after MaxIdlePrompts or AiResponseTimeoutSeconds,
and ends with handoffData hangup/transfer.
- twilio/relay/action dials the CSR on transfer, hangs up otherwise, and
reconnects a failed session up to MaxReconnects times.
- The WebSocket handshake is checked against X-Twilio-Signature.
- The caller-message handling used by the webhook queue moved to
TwilioConversationProcessor so both flows share it.
Tested with in-process harnesses: 24 TwiML checks for Say mode and 28 checks
driving the relay WebSocket (turns, merging, DTMF, indications, silence,
timeouts, signature, TwiML, action callback).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Summary by QodoAdd Twilio Say and ConversationRelay with bounded OpenAI TTS requests
AI Description
Diagram
High-Level Assessment
Files changed (20)
|
… them With Twilio:ConversationRelay:StreamReplies on (off by default), a relay turn asks BotSharp to stream (use_stream_message, and language English when unset) and forwards each OnReceiveLlmStreamMessage piece to Twilio as a text token with last=false, so Twilio starts speaking with the first words instead of after the whole reply. - Each model response is closed with last=true when the next one starts, before a tool-call indication is spoken, and at the end of the turn. - The final reply is not spoken again; only the part not streamed is (text set by a tool, or a reply rewritten after the model wrote it). - When the caller cuts in, the rest of the streamed reply is dropped; when the turn times out, nothing streamed afterwards reaches the call. - TwilioConversationProcessor takes AgentTurnCallbacks (indication, stream start, stream text); the webhook queue passes only the indication, so that flow does not stream. Needs a chat provider that streams (OpenAI, Azure OpenAI, Anthropic, Google); non-English conversations get the whole reply as before. Tested with in-process harnesses: relay harness 42/42 (13 new: pieces and close, first words before the model finishes, tool-call boundary, remainder, rewritten reply, tool-set reply, interrupt, timeout, and the processor's state and event wiring against stubbed BotSharp services); Say harness passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Outbound Twilio calls synthesize speech with OpenAI
/v1/audio/speechbefore dialing and on every turn. In production some of those requests never return.GenerateSpeechAsyncused the SDK defaults (100s per attempt, 3 retries), so one stuck request blocked the call for about 7 minutes (Retry failed after 4 tries), and when it was the opening line the call was never placed.Changes
1. OpenAI text-to-speech timeout (
bf039def7)AudioSynthesisProviderwaits at most 30s per attempt and keeps 3 retries.Audio.Synthesis.TimeoutSeconds/MaxRetriesinLlmModelSetting.ProviderHelper.GetClienttakes the timeout and retry count as optional arguments; other callers keep the SDK defaults.2. Twilio
<Say>mode (Twilio:SayGeneratedSpeech, off by default)<Say voice="Twilio:SayVoice">instead of a pre-generated mp3: opening line, replies, hang-up, transfer, voicemail, tool-call indications.twilio/{name}.mp3) can be spoken fromTwilio:PromptTextsso the whole call uses one voice; prompts without text keep playing.TwilioService.SaveSpeechAsync/AppendSpeechare now the single places that write and play speech. With the switch off, behaviour is unchanged.3. Twilio ConversationRelay (
Twilio:ConversationRelay, off by default;AgentIdslimits it to listed agents)<Connect><ConversationRelay>: Twilio transcribes and speaks, and exchanges text with the newTwilioConversationRelayMiddlewareat/twilio/relay/{agentId}/{conversationId}.ConversationRelaySessionhandles one call:#-terminated) and speaks tool-call indications;MaxIdlePrompts, or when the agent doesn't answer withinAiResponseTimeoutSeconds;handoffDatahanguportransfer.twilio/relay/actiondials the CSR number on transfer, hangs up otherwise, and reconnects a failed session up toMaxReconnectstimes.X-Twilio-Signature(ValidateSignature, on by default).TwilioMessageQueueServicemoved toTwilioConversationProcessorso both flows share it.TwilioStreamMiddleware:app.UseMiddleware<TwilioConversationRelayMiddleware>();. Before enabling it, the account must accept Twilio's AI/ML features addendum.Testing
<Say>mode: 24 TwiML checks throughTwilioServicepass: each speech type, escaping, prompt replacement, config binding, unchanged behaviour with the switch off.🤖 Generated with Claude Code