From 872070259ef66a8d3c66d1901d397c7459e98d97 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 3 Aug 2026 05:37:05 +0800 Subject: [PATCH] fix(translator): emit `response.completed` on stream end when `finish_reason` is missing Closes: #4745 --- .../openai_openai-responses_response.go | 19 ++++++++----------- .../openai_openai-responses_response_test.go | 9 +++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index bc390f30..9d198a42 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -20,14 +20,13 @@ type oaiToResponsesStateReasoning struct { OutputIndex int } type oaiToResponsesState struct { - Seq int - ResponseID string - Created int64 - Started bool - CompletionPending bool - CompletedEmitted bool - ReasoningID string - ReasoningIndex int + Seq int + ResponseID string + Created int64 + Started bool + CompletedEmitted bool + ReasoningID string + ReasoningIndex int // aggregation buffers for response.output // Per-output message text buffers by index MsgTextBuf map[int]*strings.Builder @@ -246,7 +245,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } requestForNamespace := pickRequestJSON(originalRequestRawJSON, requestRawJSON) if bytes.Equal(rawJSON, []byte("[DONE]")) { - if st.CompletionPending && !st.CompletedEmitted { + if st.Started && !st.CompletedEmitted { st.CompletedEmitted = true return [][]byte{buildResponsesCompletedEvent(st, requestForNamespace, func() int { st.Seq++; return st.Seq })} } @@ -390,7 +389,6 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, st.TotalTokens = 0 st.ReasoningTokens = 0 st.UsageSeen = false - st.CompletionPending = false st.CompletedEmitted = false // response.created created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`) @@ -689,7 +687,6 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, st.FuncArgsDone[key] = true } } - st.CompletionPending = true } return true diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go index 9898744a..80ae4b44 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response_test.go @@ -69,6 +69,15 @@ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_ResponseCompleted outputTokens: 5, totalTokens: 18, }, + { + name: "no finish reason", + in: []string{ + `data: {"id":"resp_no_finish_reason","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":"hello"}}]}`, + `data: [DONE]`, + }, + doneInputIndex: 1, + hasUsage: false, + }, { // An OpenAI-compatible streams from a buggy server might never send usage, so response.completed should // still wait for [DONE] but omit the usage object entirely. -- 2.51.2