{"record":{"id":"a76a2414bd7694a3","repo":"siyuan-note/siyuan","slug":"response-stream-ended-before-a-terminal-event","errorCode":null,"errorMessage":"response stream ended before a terminal event","messagePattern":"response stream ended before a terminal event","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/openai_completion.go","lineNumber":403,"sourceCode":"\t\treturn stream.chat.Recv()\n\t}\n\tif stream.responses == nil {\n\t\treturn openai.ChatCompletionStreamResponse{}, io.EOF\n\t}\n\tif len(stream.pending) > 0 {\n\t\tresponse := stream.pending[0]\n\t\tstream.pending = stream.pending[1:]\n\t\treturn response, nil\n\t}\n\tif stream.responsesDone {\n\t\treturn openai.ChatCompletionStreamResponse{}, io.EOF\n\t}\n\n\tfor {\n\t\tevent, err := stream.responses.Recv()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn openai.ChatCompletionStreamResponse{}, errors.New(\"response stream ended before a terminal event\")\n\t\t\t}\n\t\t\treturn openai.ChatCompletionStreamResponse{}, err\n\t\t}\n\t\tresponse := openai.ChatCompletionStreamResponse{Object: \"chat.completion.chunk\"}\n\t\tswitch event.Type {\n\t\tcase openai.ResponseStreamEventOutputTextDelta:\n\t\t\tstream.responseContent.WriteString(event.Delta)\n\t\t\tresponse.Choices = []openai.ChatCompletionStreamChoice{{\n\t\t\t\tIndex: 0,\n\t\t\t\tDelta: openai.ChatCompletionStreamChoiceDelta{Content: event.Delta},\n\t\t\t}}\n\t\t\treturn response, nil\n\t\tcase openai.ResponseStreamEventRefusalDelta:\n\t\t\tstream.responseContent.WriteString(event.Delta)\n\t\t\tresponse.Choices = []openai.ChatCompletionStreamChoice{{\n\t\t\t\tIndex: 0,\n\t\t\t\tDelta: openai.ChatCompletionStreamChoiceDelta{Content: event.Delta, Refusal: event.Delta},\n\t\t\t}}","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/util/openai_completion.go#L385-L421","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the request — transient connection cuts are the leading cause.","If reproducible, fix the transport: disable SSE buffering and raise read/idle timeouts on every proxy and gateway in the path.","Test the same request against the official OpenAI endpoint to see whether the provider itself omits the terminal event; upgrade it if so.","Fall back to non-streaming CreateOpenAICompletion for requests that keep failing."],"exampleFix":"// before: single attempt, fail hard\nstream, err := util.CreateOpenAICompletionStream(ctx, client, protocol, req, input)\n\n// after: bounded retry around the whole streamed request\nfor attempt := 0; attempt < 3; attempt++ {\n\tstream, err = util.CreateOpenAICompletionStream(ctx, client, protocol, req, input)\n\tif err == nil {\n\t\tif err = consume(stream); err != nil && strings.Contains(err.Error(), \"terminal event\") && attempt < 2 {\n\t\t\tcontinue // premature EOF, retry\n\t\t}\n\t}\n\tbreak\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := stream.Recv()\nif err != nil {\n\tif strings.Contains(err.Error(), \"ended before a terminal event\") {\n\t\tstream.Close()\n\t\t// retry the whole streamed request with backoff, or fall back to:\n\t\treturn util.CreateOpenAICompletion(ctx, client, protocol, req, input) // non-streaming\n\t}\n\treturn nil, err\n}","preventionTips":["Keep SSE connections free of buffering proxies","Set generous read timeouts for long generations","Monitor premature-termination rates per provider","Keep a non-streaming fallback path configured"],"tags":["streaming","sse","openai","responses-api","network","eof"],"backgroundTag":"sse-stream-truncated","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}