Files
IanShaw027 ec9e733606 feat(grok): stream idle 换号、team+model 冷却与 RT 刷新打散
- Grok 流默认启用上游读空闲超时,首包前返回可 failover 错误并短冷却
- 同 team_id 账号在 429/free-usage 后对同模型共享进程内冷却,粘连与选号均过滤
- TokenRefreshService 已覆盖 Grok;NeedsRefresh 增加按账号稳定 jitter 防 stampede
2026-08-07 13:56:38 +08:00

39 lines
1.6 KiB
Go

package service
import (
"fmt"
"strings"
"time"
)
// Default Grok stream idle when gateway.stream_data_interval_timeout is 0.
// Long enough for slow thinking models, short enough to release hung sockets.
const defaultGrokStreamIdleTimeout = 180 * time.Second
// Shorter cool after a Grok stream-idle failure so the account can re-enter soon
// but is not immediately re-picked in a tight failover loop.
const grokStreamIdleCooldown = 2 * time.Minute
// resolveGrokStreamIdleTimeout returns the effective upstream-read idle timeout
// for Grok streams. Prefers the global gateway setting when positive; otherwise
// applies a Grok-only default so hung SSE bodies still fail over.
func resolveGrokStreamIdleTimeout(cfgStreamIntervalSec int) time.Duration {
if cfgStreamIntervalSec > 0 {
return time.Duration(cfgStreamIntervalSec) * time.Second
}
return defaultGrokStreamIdleTimeout
}
// grokStreamIdleFailoverError builds a pre-commit/handler-visible failover so
// the gateway can switch OAuth accounts after a hung Grok upstream stream.
func grokStreamIdleFailoverError(account *Account, idle time.Duration) *UpstreamFailoverError {
msg := fmt.Sprintf("Grok stream idle timeout after %s with no upstream data", idle.Round(time.Second))
return &UpstreamFailoverError{
StatusCode: 502,
ResponseBody: []byte(`{"error":{"code":"empty_upstream","message":"` + strings.ReplaceAll(msg, `"`, `'`) + `"}}`),
SafeToFailoverAfterWrite: true,
// Allow pool-mode retries; normal OAuth switches account via handler.
RetryableOnSameAccount: account != nil && account.IsPoolMode(),
}
}