fix: 非流式路径在上游终态事件output为空时从delta事件重建响应内容
上游API近期更新后,response.completed终态SSE事件的output字段可能为空, 实际内容仅通过response.output_text.delta等增量事件下发。流式路径不受影响, 但chat_completions非流式路径和responses OAuth非流式路径只依赖终态事件的 output,导致返回空响应。 新增BufferedResponseAccumulator累积器,在SSE扫描过程中收集delta事件内容 (文本、function_call、reasoning),当终态output为空时补充重建。 同时修复handleChatBufferedStreamingResponse遗漏response.done事件类型的问题。
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
|
||||
@@ -3901,6 +3902,16 @@ func (s *OpenAIGatewayService) handleOAuthSSEToJSON(resp *http.Response, c *gin.
|
||||
if parsedUsage, parsed := extractOpenAIUsageFromJSONBytes(finalResponse); parsed {
|
||||
*usage = parsedUsage
|
||||
}
|
||||
// When the terminal event has an empty output array, reconstruct
|
||||
// output from accumulated delta events so the client gets full content.
|
||||
// gjson Array() returns empty slice for null, missing, or empty arrays.
|
||||
if len(gjson.GetBytes(finalResponse, "output").Array()) == 0 {
|
||||
if outputJSON, reconstructed := reconstructResponseOutputFromSSE(bodyText); reconstructed {
|
||||
if patched, err := sjson.SetRawBytes(finalResponse, "output", outputJSON); err == nil {
|
||||
finalResponse = patched
|
||||
}
|
||||
}
|
||||
}
|
||||
body = finalResponse
|
||||
if originalModel != mappedModel {
|
||||
body = s.replaceModelInResponseBody(body, mappedModel, originalModel)
|
||||
@@ -4002,6 +4013,34 @@ func extractCodexFinalResponse(body string) ([]byte, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// reconstructResponseOutputFromSSE scans raw SSE body text for delta events and
|
||||
// returns a JSON-encoded output array reconstructed from accumulated deltas.
|
||||
// Returns (nil, false) if no content was found in deltas.
|
||||
func reconstructResponseOutputFromSSE(bodyText string) ([]byte, bool) {
|
||||
acc := apicompat.NewBufferedResponseAccumulator()
|
||||
lines := strings.Split(bodyText, "\n")
|
||||
for _, line := range lines {
|
||||
data, ok := extractOpenAISSEDataLine(line)
|
||||
if !ok || data == "" || data == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
var event apicompat.ResponsesStreamEvent
|
||||
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
acc.ProcessEvent(&event)
|
||||
}
|
||||
if !acc.HasContent() {
|
||||
return nil, false
|
||||
}
|
||||
output := acc.BuildOutput()
|
||||
outputJSON, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return outputJSON, true
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) parseSSEUsageFromBody(body string) *OpenAIUsage {
|
||||
usage := &OpenAIUsage{}
|
||||
lines := strings.Split(body, "\n")
|
||||
|
||||
Reference in New Issue
Block a user