mattermost / mattermost/mattermost-plugin-agents

OpenAI Compatible streaming: SSE comment lines are silently skipped, so they never reset bifrost's per-chunk watchdog

Open
#985 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Configure an OpenAI Compatible service pointing at a backend that:
    • Takes noticeably longer than Streaming Timeout Seconds to produce its first real content chunk, and
    • Sends periodic : keepalive\n\n SSE comment lines while it waits (a correct, spec-compliant way to keep an intermediate reverse-proxy's idle-connection timeout from firing)
  2. Mention the bot with a prompt that takes long enough to exceed Streaming Timeout Seconds
  3. 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.chunk delta frames instead of SSE comments to survive Streaming 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-agents with github.com/maximhq/bifrost/core v1.7.6
  • Service type: OpenAI Compatible, custom self-hosted backend

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.