package llm import ( "io" "net" "net/http" "sync" "time" ) // streamIdleTimeout 是流式响应允许的最长静默。 // // 为什么需要它:三家 provider 都是 `bufio.Scanner` 死读 resp.Body,而 ctx 一路 // 传下去却**没有任何超时**(客户端也是无超时的 http.DefaultClient)。上游"连上了 // 但不吐字节"(网关半开连接、服务端卡在生成阶段)时,Read 永久阻塞——整个回合 // 就此静止,用户既等不到结果也看不出出了什么。这是第二类"卡死",且比 bash 那条 // 更容易在长任务里撞上(长任务 = 长思考 = 长静默)。 // // 90 秒是权衡后的值:推理模型的首 token 延迟与中途长思考都会静默,几十秒属正常, // 而真正卡死的连接通常连几十秒都不返回。宁可宽一点,也不要错杀慢但活的网关。 // var 仅供测试缩短。 var streamIdleTimeout = 90 * time.Second // defaultHTTPClient 是 provider 没显式给 HTTP 客户端时的默认值。 // // 不用 http.DefaultClient:① 它是进程级全局,改它会影响别人的代码,不改它就没有 // 任何超时;② 这里要的是"首字节前有限等待",而 Client.Timeout 覆盖**整个响应体**, // 设了就会把正常的长流式掐死。所以只设 ResponseHeaderTimeout + dial/TLS 超时, // 读流阶段的看门狗交给 idleGuard。 var defaultHTTPClient = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 30 * time.Second, ResponseHeaderTimeout: 90 * time.Second, ExpectContinueTimeout: 1 * time.Second, MaxIdleConnsPerHost: 8, }, } // idleGuard 是包在 SSE 响应体外面的静默看门狗:每次读到字节就重置计时, // 静默超阈值就关掉连接,从而解阻塞正在 Read 的那个 goroutine。 type idleGuard struct { body io.ReadCloser idle time.Duration mu sync.Mutex last time.Time once sync.Once stalled bool done chan struct{} } // guardStream 开始看护 body,返回可直接交给 bufio.Scanner 的 io.Reader。 // 调用方必须 defer Stop(),否则看护 goroutine 会活到进程结束。 func guardStream(body io.ReadCloser, idle time.Duration) *idleGuard { g := &idleGuard{body: body, idle: idle, last: time.Now(), done: make(chan struct{})} go g.watch() return g } func (g *idleGuard) Read(p []byte) (int, error) { n, err := g.body.Read(p) if n > 0 { g.mu.Lock() g.last = time.Now() g.mu.Unlock() } return n, err } func (g *idleGuard) Close() error { return g.body.Close() } // watch 周期检查静默时长。阈值本身可能很长(90s),tick 取 idle/4: // 既不会让正常流多出可观开销,也能在半分钟内发现"再也不会有数据"的连接。 func (g *idleGuard) watch() { tick := g.idle / 4 if tick < 20*time.Millisecond { tick = 20 * time.Millisecond // 测试里 idle 会被缩到毫秒级 } t := time.NewTicker(tick) defer t.Stop() for { select { case <-g.done: return case <-t.C: g.mu.Lock() quiet := time.Since(g.last) g.mu.Unlock() if quiet > g.idle { g.once.Do(func() { g.mu.Lock() g.stalled = true g.mu.Unlock() // 唯一的解阻塞手段:Read 卡在传输层时没有任何回调可指望。 _ = g.body.Close() }) return } } } } // Stop 结束看护(流正常读完后调用)。 func (g *idleGuard) Stop() { close(g.done) } // Err 报告这次流是不是被看门狗掐断的。给上层的意义是"能说出真实原因": // 否则调用方只会看到一句来历不明的 "read tcp: use of closed connection"。 func (g *idleGuard) Err() error { g.mu.Lock() defer g.mu.Unlock() if g.stalled { return &StalledError{Idle: g.idle} } return nil } // StalledError 是"服务端长时间不吐字节"这一类失败。可被上层 errors.As 识别。 type StalledError struct{ Idle time.Duration } func (e *StalledError) Error() string { return "provider stream stalled: no data for " + e.Idle.String() }