Skip to content

Commit 079ac0e

Browse files
localai-botmudler
andauthored
fix(realtime): raise WebRTC data-channel max-message-size + keep sendLoop alive (#10407)
* fix(realtime): raise WebRTC data-channel max-message-size for large events Browsers advertise a conservative SCTP max-message-size in their SDP offer (Chrome uses 256 KiB). pion enforces the remote's advertised value on send, so a single realtime event larger than it cannot be sent over the "oai-events" data channel: SendText fails, the event is dropped, and the turn silently yields no response. Some turns legitimately produce a >256 KiB JSON event — notably tool calls with sizeable schemas or results. Browsers advertise the value conservatively but their SCTP stacks reassemble much larger messages, so raise the max-message-size honored for our own server-generated events by rewriting the attribute in the offer before SetRemoteDescription. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(realtime): keep the WebRTC sendLoop alive when one event send fails A failed SendText on the oai-events data channel exited the sender goroutine, so a single dropped event (e.g. one over the negotiated SCTP max-message-size) tore down the session and silently dropped every subsequent event. Log and skip the offending event instead and keep draining; a genuinely dead transport is still handled by the closed / connection-state path. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 2e734bf commit 079ac0e

4 files changed

Lines changed: 76 additions & 5 deletions

File tree

core/http/endpoints/openai/realtime_transport_webrtc.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,22 @@ func (t *WebRTCTransport) sendLoop() {
113113
return
114114
}
115115
if err := t.dc.SendText(string(data)); err != nil {
116-
xlog.Error("data channel send failed", "error", err)
117-
return
116+
// Drop just this event and keep the loop alive: a single
117+
// failed send (e.g. an event over the negotiated SCTP
118+
// max-message-size) must not tear down the session and
119+
// silently drop every subsequent event. A genuinely dead
120+
// transport is handled by the <-t.closed case.
121+
xlog.Error("data channel send failed, dropping event", "error", err)
122+
continue
118123
}
119124
case <-t.closed:
120125
// Drain any remaining queued events before exiting
121126
for {
122127
select {
123128
case data := <-t.outEvents:
124129
if err := t.dc.SendText(string(data)); err != nil {
125-
return
130+
xlog.Error("data channel send failed while draining, dropping event", "error", err)
131+
continue
126132
}
127133
default:
128134
return

core/http/endpoints/openai/realtime_webrtc.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,13 @@ func RealtimeCalls(application *application.Application) echo.HandlerFunc {
128128
handleIncomingAudioTrack(track, transport)
129129
})
130130

131-
// Set the remote SDP (client's offer)
131+
// Set the remote SDP (client's offer). Raise the data-channel
132+
// max-message-size the browser advertised so pion permits the larger
133+
// realtime events some turns produce (e.g. tool calls), which would
134+
// otherwise be dropped on send. See realtime_webrtc_sctp.go.
132135
if err := pc.SetRemoteDescription(webrtc.SessionDescription{
133136
Type: webrtc.SDPTypeOffer,
134-
SDP: req.SDP,
137+
SDP: raiseDataChannelMaxMessageSize(req.SDP),
135138
}); err != nil {
136139
transport.Close()
137140
xlog.Error("failed to set remote description", "error", err)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package openai
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
)
7+
8+
// realtimeDataChannelMaxMessageSize is the SCTP max-message-size LocalAI honors
9+
// for the "oai-events" data channel, in bytes.
10+
//
11+
// Browsers advertise a conservative max-message-size in their SDP offer (Chrome
12+
// uses 262144 = 256 KiB). pion enforces the remote's advertised value on send,
13+
// so a single realtime event larger than it cannot be sent: the SendText fails,
14+
// the event is dropped, and the turn silently yields no response. Some turns
15+
// legitimately produce a single JSON event above 256 KiB (notably tool calls
16+
// with sizeable schemas or results). Browsers advertise this value
17+
// conservatively but their SCTP stacks reassemble much larger messages, so we
18+
// raise the value honored for our own server-generated events.
19+
const realtimeDataChannelMaxMessageSize = 16 * 1024 * 1024 // 16 MiB
20+
21+
var maxMessageSizeAttrRe = regexp.MustCompile(`a=max-message-size:\d+`)
22+
23+
// raiseDataChannelMaxMessageSize rewrites the SCTP max-message-size attribute in
24+
// an SDP offer to realtimeDataChannelMaxMessageSize so pion permits larger
25+
// outbound realtime events. Offers that don't carry the attribute are returned
26+
// unchanged.
27+
func raiseDataChannelMaxMessageSize(sdp string) string {
28+
return maxMessageSizeAttrRe.ReplaceAllString(sdp, fmt.Sprintf("a=max-message-size:%d", realtimeDataChannelMaxMessageSize))
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package openai
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
)
10+
11+
var _ = Describe("raiseDataChannelMaxMessageSize", func() {
12+
It("raises a max-message-size the browser advertised", func() {
13+
offer := "v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=max-message-size:262144\r\n"
14+
out := raiseDataChannelMaxMessageSize(offer)
15+
Expect(out).To(ContainSubstring(fmt.Sprintf("a=max-message-size:%d", realtimeDataChannelMaxMessageSize)))
16+
Expect(out).NotTo(ContainSubstring("a=max-message-size:262144"))
17+
})
18+
19+
It("leaves an offer without the attribute unchanged", func() {
20+
offer := "v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n"
21+
Expect(raiseDataChannelMaxMessageSize(offer)).To(Equal(offer))
22+
})
23+
24+
It("rewrites every occurrence", func() {
25+
offer := "a=max-message-size:1024\r\na=max-message-size:262144\r\n"
26+
out := raiseDataChannelMaxMessageSize(offer)
27+
Expect(strings.Count(out, fmt.Sprintf("a=max-message-size:%d", realtimeDataChannelMaxMessageSize))).To(Equal(2))
28+
})
29+
30+
It("raises above the 256 KiB browsers advertise", func() {
31+
Expect(realtimeDataChannelMaxMessageSize).To(BeNumerically(">", 262144))
32+
})
33+
})

0 commit comments

Comments
 (0)