返回 DeepSeek-Reasonix
auxiliary_recovery.go
根目录 / internal / provider / auxiliary_recovery.go
1 package provider
2
3 import (
4 "context"
5 "errors"
6 "time"
7 )
8
9 // StreamAuxiliary applies the finite policy to search and summarization. It
10 // buffers attempts so failed partial summaries never leak into their caller.
11 func StreamAuxiliary(ctx context.Context, p Provider, req Request) (<-chan Chunk, error) {
12 ctx = WithManagedRecovery(WithIndependentRequestAttemptCounter(ctx))
13 out := make(chan Chunk)
14 var aggregate Usage
15 go func() {
16 defer close(out)
17 send := func(c Chunk) bool {
18 select {
19 case out <- c:
20 return true
21 case <-ctx.Done():
22 return false
23 }
24 }
25 for attempt := range 4 {
26 attemptCtx, cancel := context.WithCancel(ctx)
27 ch, err := Stream(attemptCtx, p, req)
28 var latest *Usage
29 var chunks []Chunk
30 complete := false
31 bytes := 0
32 if err == nil {
33 loop:
34 for {
35 select {
36 case <-ctx.Done():
37 cancel()
38 return
39 case c, ok := <-ch:
40 if !ok {
41 break loop
42 }
43 if c.Type == ChunkError {
44 err = c.Err
45 if err == nil {
46 err = errors.New("auxiliary provider error")
47 }
48 break loop
49 }
50 bytes += len(c.Text)
51 if bytes > 16*1024*1024 {
52 err = errors.New("auxiliary response exceeds local limit")
53 break loop
54 }
55 if c.Type == ChunkUsage {
56 latest = c.Usage
57 continue
58 }
59 complete = complete || c.Type == ChunkDone
60 chunks = append(chunks, c)
61 }
62 }
63 if err == nil && !complete {
64 err = StreamInterrupt(errors.New("auxiliary response ended before terminal event"), "unexpected_eof")
65 }
66 }
67 cancel()
68 if latest == nil {
69 aggregate.Unknown = true
70 }
71 if latest != nil {
72 aggregate.PromptTokens += latest.PromptTokens
73 aggregate.CompletionTokens += latest.CompletionTokens
74 aggregate.TotalTokens += latest.TotalTokens
75 aggregate.CacheHitTokens += latest.CacheHitTokens
76 aggregate.CacheMissTokens += latest.CacheMissTokens
77 aggregate.ReasoningTokens += latest.ReasoningTokens
78 aggregate.CacheWriteTokens += latest.CacheWriteTokens
79 aggregate.CacheWriteBilledTokens += latest.CacheWriteBilledTokens
80 aggregate.FinishReason = latest.FinishReason
81 aggregate.Estimated = aggregate.Estimated || latest.Estimated
82 aggregate.Unknown = aggregate.Unknown || latest.Unknown
83 }
84 aggregate.RequestCount = RequestAttemptCount(ctx)
85 if aggregate.RequestCount == 0 {
86 aggregate.RequestCount = attempt + 1
87 }
88 if err == nil {
89 send(Chunk{Type: ChunkUsage, Usage: &aggregate})
90 for _, c := range chunks {
91 if !send(c) {
92 return
93 }
94 }
95 return
96 }
97 f := ClassifyRecovery(err)
98 if !f.Retryable || attempt == 3 {
99 send(Chunk{Type: ChunkUsage, Usage: &aggregate})
100 send(Chunk{Type: ChunkError, Err: err})
101 return
102 }
103 delay := time.Duration(1<<attempt) * 2 * time.Second
104 delay = max(delay, f.RetryAfter)
105 if !auxiliarySleep(ctx, delay) {
106 return
107 }
108 }
109 }()
110 return out, nil
111 }
112
113 type recoverySleeperKey struct{}
114
115 // WithRecoverySleeper supplies the owner clock without changing the retry policy.
116 func WithRecoverySleeper(ctx context.Context, sleep func(context.Context, time.Duration) bool) context.Context {
117 return context.WithValue(ctx, recoverySleeperKey{}, sleep)
118 }
119 func auxiliarySleep(ctx context.Context, d time.Duration) bool {
120 if sleep, ok := ctx.Value(recoverySleeperKey{}).(func(context.Context, time.Duration) bool); ok {
121 return sleep(ctx, d)
122 }
123 timer := time.NewTimer(d)
124 defer timer.Stop()
125 select {
126 case <-ctx.Done():
127 return false
128 case <-timer.C:
129 return true
130 }
131 }
132
132 lines GO