mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-19 15:37:44 +08:00
Merge pull request #362 from CJackHwang/codex/fix-issue-based-on-feedback
fix(sse): batch tiny stream chunks before emitting
This commit is contained in:
@@ -4,12 +4,15 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
parsedLineBufferSize = 128
|
parsedLineBufferSize = 128
|
||||||
scannerBufferSize = 64 * 1024
|
scannerBufferSize = 64 * 1024
|
||||||
maxScannerLineSize = 2 * 1024 * 1024
|
maxScannerLineSize = 2 * 1024 * 1024
|
||||||
|
minFlushChars = 160
|
||||||
|
maxFlushWait = 80 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartParsedLinePump scans an upstream DeepSeek SSE body and emits normalized
|
// StartParsedLinePump scans an upstream DeepSeek SSE body and emits normalized
|
||||||
@@ -20,21 +23,123 @@ func StartParsedLinePump(ctx context.Context, body io.Reader, thinkingEnabled bo
|
|||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(out)
|
defer close(out)
|
||||||
scanner := bufio.NewScanner(body)
|
type scanItem struct {
|
||||||
scanner.Buffer(make([]byte, 0, scannerBufferSize), maxScannerLineSize)
|
line []byte
|
||||||
|
err error
|
||||||
|
eof bool
|
||||||
|
}
|
||||||
|
lineCh := make(chan scanItem, 1)
|
||||||
|
stopScanner := make(chan struct{})
|
||||||
|
defer close(stopScanner)
|
||||||
|
go func() {
|
||||||
|
sendScanItem := func(item scanItem) bool {
|
||||||
|
select {
|
||||||
|
case lineCh <- item:
|
||||||
|
return true
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false
|
||||||
|
case <-stopScanner:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer close(lineCh)
|
||||||
|
scanner := bufio.NewScanner(body)
|
||||||
|
scanner.Buffer(make([]byte, 0, scannerBufferSize), maxScannerLineSize)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := append([]byte{}, scanner.Bytes()...)
|
||||||
|
if !sendScanItem(scanItem{line: line}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = sendScanItem(scanItem{err: scanner.Err(), eof: true})
|
||||||
|
}()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(maxFlushWait)
|
||||||
|
defer ticker.Stop()
|
||||||
currentType := initialType
|
currentType := initialType
|
||||||
for scanner.Scan() {
|
var pending *LineResult
|
||||||
line := append([]byte{}, scanner.Bytes()...)
|
pendingChars := 0
|
||||||
result := ParseDeepSeekContentLine(line, thinkingEnabled, currentType)
|
|
||||||
currentType = result.NextType
|
sendResult := func(r LineResult) bool {
|
||||||
|
select {
|
||||||
|
case out <- r:
|
||||||
|
return true
|
||||||
|
case <-ctx.Done():
|
||||||
|
done <- ctx.Err()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushPending := func() bool {
|
||||||
|
if pending == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !sendResult(*pending) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pending = nil
|
||||||
|
pendingChars = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
select {
|
select {
|
||||||
case out <- result:
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
done <- ctx.Err()
|
done <- ctx.Err()
|
||||||
return
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if !flushPending() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case item, ok := <-lineCh:
|
||||||
|
if !ok || item.eof {
|
||||||
|
if !flushPending() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done <- item.err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line := item.line
|
||||||
|
result := ParseDeepSeekContentLine(line, thinkingEnabled, currentType)
|
||||||
|
currentType = result.NextType
|
||||||
|
|
||||||
|
canAccumulate := result.Parsed && !result.Stop && result.ErrorMessage == "" && !result.ContentFilter && result.ResponseMessageID == 0
|
||||||
|
if canAccumulate {
|
||||||
|
lineChars := 0
|
||||||
|
for _, p := range result.Parts {
|
||||||
|
lineChars += len(p.Text)
|
||||||
|
}
|
||||||
|
for _, p := range result.ToolDetectionThinkingParts {
|
||||||
|
lineChars += len(p.Text)
|
||||||
|
}
|
||||||
|
if lineChars > 0 {
|
||||||
|
if pending == nil {
|
||||||
|
cp := result
|
||||||
|
pending = &cp
|
||||||
|
} else {
|
||||||
|
pending.Parts = append(pending.Parts, result.Parts...)
|
||||||
|
pending.ToolDetectionThinkingParts = append(pending.ToolDetectionThinkingParts, result.ToolDetectionThinkingParts...)
|
||||||
|
pending.NextType = result.NextType
|
||||||
|
}
|
||||||
|
pendingChars += lineChars
|
||||||
|
if pendingChars < minFlushChars {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !flushPending() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flushPending() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !sendResult(result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
done <- scanner.Err()
|
|
||||||
}()
|
}()
|
||||||
return out, done
|
return out, done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func TestStartParsedLinePumpMultipleLines(t *testing.T) {
|
|||||||
if err := <-done; err != nil {
|
if err := <-done; err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if len(collected) < 3 {
|
if len(collected) < 2 {
|
||||||
t.Fatalf("expected at least 3 results, got %d", len(collected))
|
t.Fatalf("expected at least 2 results, got %d", len(collected))
|
||||||
}
|
}
|
||||||
// First should be thinking
|
// First should be thinking
|
||||||
if collected[0].Parts[0].Type != "thinking" {
|
if collected[0].Parts[0].Type != "thinking" {
|
||||||
@@ -175,3 +175,31 @@ func TestStartParsedLinePumpThinkingDisabled(t *testing.T) {
|
|||||||
t.Fatalf("expected at least 1 part, got %d", len(parts))
|
t.Fatalf("expected at least 1 part, got %d", len(parts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStartParsedLinePumpAccumulatesSmallChunks(t *testing.T) {
|
||||||
|
body := strings.NewReader(
|
||||||
|
"data: {\"p\":\"response/content\",\"v\":\"h\"}\n" +
|
||||||
|
"data: {\"p\":\"response/content\",\"v\":\"i\"}\n" +
|
||||||
|
"data: [DONE]\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
||||||
|
|
||||||
|
collected := make([]LineResult, 0)
|
||||||
|
for r := range results {
|
||||||
|
collected = append(collected, r)
|
||||||
|
}
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(collected) != 2 {
|
||||||
|
t.Fatalf("expected 2 results (accumulated content + done), got %d", len(collected))
|
||||||
|
}
|
||||||
|
if len(collected[0].Parts) != 2 {
|
||||||
|
t.Fatalf("expected 2 accumulated parts, got %d", len(collected[0].Parts))
|
||||||
|
}
|
||||||
|
if !collected[1].Stop {
|
||||||
|
t.Fatal("expected second result to stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user