siyuan-note/siyuan · error

response stream ended before a terminal event

Error message

response stream ended before a terminal event

What it means

OpenAICompletionStream adapts a Responses-API SSE stream into Chat Completions chunks. A compliant stream must terminate with a terminal event (response.completed, response.incomplete, or a failure event) that carries the final state. If the underlying stream returns io.EOF before any terminal event, the accumulated deltas have no confirmed final state, so Recv reports this error instead of pretending the response ended normally.

Source

Thrown at kernel/util/openai_completion.go:403

		return stream.chat.Recv()
	}
	if stream.responses == nil {
		return openai.ChatCompletionStreamResponse{}, io.EOF
	}
	if len(stream.pending) > 0 {
		response := stream.pending[0]
		stream.pending = stream.pending[1:]
		return response, nil
	}
	if stream.responsesDone {
		return openai.ChatCompletionStreamResponse{}, io.EOF
	}

	for {
		event, err := stream.responses.Recv()
		if err != nil {
			if errors.Is(err, io.EOF) {
				return openai.ChatCompletionStreamResponse{}, errors.New("response stream ended before a terminal event")
			}
			return openai.ChatCompletionStreamResponse{}, err
		}
		response := openai.ChatCompletionStreamResponse{Object: "chat.completion.chunk"}
		switch event.Type {
		case openai.ResponseStreamEventOutputTextDelta:
			stream.responseContent.WriteString(event.Delta)
			response.Choices = []openai.ChatCompletionStreamChoice{{
				Index: 0,
				Delta: openai.ChatCompletionStreamChoiceDelta{Content: event.Delta},
			}}
			return response, nil
		case openai.ResponseStreamEventRefusalDelta:
			stream.responseContent.WriteString(event.Delta)
			response.Choices = []openai.ChatCompletionStreamChoice{{
				Index: 0,
				Delta: openai.ChatCompletionStreamChoiceDelta{Content: event.Delta, Refusal: event.Delta},
			}}

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Retry the request — transient connection cuts are the leading cause.
  2. If reproducible, fix the transport: disable SSE buffering and raise read/idle timeouts on every proxy and gateway in the path.
  3. Test the same request against the official OpenAI endpoint to see whether the provider itself omits the terminal event; upgrade it if so.
  4. Fall back to non-streaming CreateOpenAICompletion for requests that keep failing.

Example fix

// before: single attempt, fail hard
stream, err := util.CreateOpenAICompletionStream(ctx, client, protocol, req, input)

// after: bounded retry around the whole streamed request
for attempt := 0; attempt < 3; attempt++ {
	stream, err = util.CreateOpenAICompletionStream(ctx, client, protocol, req, input)
	if err == nil {
		if err = consume(stream); err != nil && strings.Contains(err.Error(), "terminal event") && attempt < 2 {
			continue // premature EOF, retry
		}
	}
	break
}
Defensive patterns

Strategy: retry

Try / catch

resp, err := stream.Recv()
if err != nil {
	if strings.Contains(err.Error(), "ended before a terminal event") {
		stream.Close()
		// retry the whole streamed request with backoff, or fall back to:
		return util.CreateOpenAICompletion(ctx, client, protocol, req, input) // non-streaming
	}
	return nil, err
}

Prevention

When it happens

Trigger: Streaming a Responses-protocol completion (protocol "openai-responses") when the connection drops or a proxy closes the SSE channel early; gateway idle/read timeouts cutting long generations; providers that end the event stream without ever emitting response.completed; flaky mobile or VPN networks.

Common situations: Reverse proxies (nginx, Cloudflare) buffering or timing out SSE; self-hosted gateways with short idle timeouts; very long tool-call generations exceeding intermediate hop limits; unstable networks on mobile clients.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/a76a2414bd7694a3. Report an issue: GitHub.