mattermost / mattermost/mattermost-plugin-agents
OpenAI Compatible streaming: SSE comment lines are silently skipped, so they never reset bifrost's per-chunk watchdog
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 250
- Forks
- 103
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 24
Description
Description
For the OpenAI Compatible service type, when the upstream backend is slow to produce the first token (e.g. a self-hosted model that needs to cold-load, or a queued job behind a slow job runner), the backend needs to keep the streaming HTTP connection alive across intermediate reverse proxies (nginx/Traefik/etc.) that would otherwise drop an idle connection. The standard way to do this in an SSE stream is periodic comment lines (: keepalive\n\n), which are valid per the SSE spec and are meant to be invisible to any conforming client.
However, bifrost's SSE reader explicitly discards comment lines before they ever reach the chunk-processing loop:
https://github.com/maximhq/bifrost/blob/core/v1.7.6/core/providers/utils/sse.go
func (r *defaultSSEDataReader) ReadDataLine() ([]byte, error) {
for {
line, ok := r.nextLine()
if !ok {
break
}
// Skip empty lines and comments
if len(line) == 0 || line[0] == ':' {
continue
}
...
And in bifrost/bifrost.go, streamChat's per-chunk watchdog (backing the "Streaming Timeout Seconds" service setting) is only pinged inside the loop that consumes already-parsed chunks from that reader:
for chunk := range streamChan {
// Ping watchdog
select {
case watchdog <- struct{}{}:
default:
}
...
Since comment-only SSE lines never produce an item on streamChan, they never reach this ping — meaning a backend that keeps the raw TCP connection alive with valid SSE comments will still have the request killed by the watchdog once Streaming Timeout Seconds of real silence elapses, even though the connection itself never dropped. The user just sees the generic "Sorry! An error occurred while accessing the LLM. See server logs for details." with no indication that the actual cause was this watchdog, not a connection failure.
Steps to reproduce
- Configure an
OpenAI Compatibleservice pointing at a backend that:- Takes noticeably longer than
Streaming Timeout Secondsto produce its first real content chunk, and - Sends periodic
: keepalive\n\nSSE comment lines while it waits (a correct, spec-compliant way to keep an intermediate reverse-proxy's idle-connection timeout from firing)
- Takes noticeably longer than
- Mention the bot with a prompt that takes long enough to exceed
Streaming Timeout Seconds - Observe the request fails with "An error occurred while accessing the LLM" even though the TCP connection to the backend was never actually dropped
Expected behavior
Either:
- The per-chunk streaming watchdog should be reset on any received bytes from the underlying HTTP response body (not only on parsed data chunks), so a spec-compliant SSE comment-based keep-alive is honored, or
- If that's intentionally not supported, it would help to document that self-hosted/custom backends need to send real (content-less)
chat.completion.chunkdelta frames instead of SSE comments to surviveStreaming Timeout Seconds— and/or surface a more specific error message when the failure is this internal watchdog rather than an actual upstream/connection error.
Workaround found
Sending real empty delta chunks instead of SSE comments for keep-alive (i.e. data: {"choices":[{"index":0,"delta":{},"finish_reason":null}]}\n\n on an interval) works around this, since those do flow through the chunk-processing loop and reset the watchdog — but this isn't obvious, and cost significant debugging time to trace into bifrost's source, since the plugin's own error message gives no indication of where the timeout is actually enforced.
Environment
mattermost-plugin-agentswithgithub.com/maximhq/bifrost/core v1.7.6- Service type:
OpenAI Compatible, custom self-hosted backend
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with core/providers/utils/sse.go and trace defaultSSEDataReader.ReadDataLine, then inspect bifrost/bifrost.go where streamChat consumes parsed chunks and pings the watchdog. Verify how raw response bytes and SSE comment lines flow through these paths. Done means valid SSE keepalives no longer cause an active stream to hit the watchdog, or the limitation and resulting failure are explicitly surfaced.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai, api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100