sub2api/backend/internal/pkg/windsurf/cold_threshold.go
win de048fad25 chore(wip): save Windsurf/Antigravity/ops customizations before upstream merge
WIP commit保存以下定制工作以便后续合并 upstream v0.1.124-125:
- Windsurf: tier access service, NLU extractor, cold threshold, Google login
- Antigravity: client/oauth 调整
- Ops: log stream handler/broadcaster/middleware, OpsLogStreamView
- Frontend: WindsurfLoginModal Google, GoogleIcon, AccountsView, sidebar/router/i18n
2026-05-09 00:41:19 +08:00

109 lines
3.4 KiB
Go

package windsurf
import (
"os"
"strconv"
"strings"
"sync"
"time"
)
// ColdThresholdConfig parameterises the adaptive cold-stall timeout. Values
// can be overridden via env vars (see DefaultColdThresholdConfig).
type ColdThresholdConfig struct {
// Base is the timeout for an empty / tiny prompt (e.g. "ping").
Base time.Duration
// PerKChar adds this much time for every 1000 characters of input.
PerKChar time.Duration
// Max caps the total threshold regardless of input size. Callers may
// further clamp via the runtime maxWait argument.
Max time.Duration
}
var (
defaultColdCfg ColdThresholdConfig
defaultColdCfgOnce sync.Once
)
// DefaultColdThresholdConfig returns the active config. The first call
// resolves env-var overrides:
//
// WINDSURF_COLD_BASE_SECONDS (default 30)
// WINDSURF_COLD_PER_KCHAR_SEC (default 5)
// WINDSURF_COLD_MAX_SECONDS (default 90)
//
// Defaults match dwgx/WindsurfAPI's empirical "long inputs up to 90s" rule
// while preserving the prior 30s base for backward compatibility.
func DefaultColdThresholdConfig() ColdThresholdConfig {
defaultColdCfgOnce.Do(func() {
defaultColdCfg = ColdThresholdConfig{
Base: envSeconds("WINDSURF_COLD_BASE_SECONDS", 30),
PerKChar: envSeconds("WINDSURF_COLD_PER_KCHAR_SEC", 5),
Max: envSeconds("WINDSURF_COLD_MAX_SECONDS", 90),
}
if defaultColdCfg.Base <= 0 {
defaultColdCfg.Base = 30 * time.Second
}
if defaultColdCfg.Max <= 0 {
defaultColdCfg.Max = 90 * time.Second
}
if defaultColdCfg.Max < defaultColdCfg.Base {
defaultColdCfg.Max = defaultColdCfg.Base
}
})
return defaultColdCfg
}
// AdaptiveColdThreshold returns the cold-stall timeout for a given prompt
// size, applying the active ColdThresholdConfig and an absolute upstream
// cap (typically the StreamCascadeChat maxWait constant).
//
// The returned threshold is the minimum of:
//
// Base + PerKChar * (inputChars / 1000)
// Config.Max
// upstreamCap (when > 0)
//
// inputChars < 0 is treated as 0. The result is always >= Base unless
// upstreamCap < Base, in which case upstreamCap wins.
func AdaptiveColdThreshold(inputChars int, upstreamCap time.Duration) time.Duration {
cfg := DefaultColdThresholdConfig()
return ComputeColdThreshold(cfg, inputChars, upstreamCap)
}
// maxInputCharsForOverflowGuard caps inputChars before multiplication to keep
// the resulting time.Duration (int64 ns) from wrapping. 2^31 chars (~2GB)
// is already absurd for an LLM prompt; anything beyond is a bug or DoS attempt.
const maxInputCharsForOverflowGuard = 1<<31 - 1
// ComputeColdThreshold is the pure form used by tests and callers that want
// to inject a custom config without touching the singleton.
func ComputeColdThreshold(cfg ColdThresholdConfig, inputChars int, upstreamCap time.Duration) time.Duration {
if inputChars < 0 {
inputChars = 0
}
if inputChars > maxInputCharsForOverflowGuard {
inputChars = maxInputCharsForOverflowGuard
}
scaled := cfg.Base + time.Duration(inputChars/1000)*cfg.PerKChar
if cfg.Max > 0 && scaled > cfg.Max {
scaled = cfg.Max
}
if upstreamCap > 0 && scaled > upstreamCap {
scaled = upstreamCap
}
return scaled
}
func envSeconds(key string, defaultSec int) time.Duration {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return time.Duration(defaultSec) * time.Second
}
v, err := strconv.Atoi(raw)
if err != nil || v <= 0 {
return time.Duration(defaultSec) * time.Second
}
return time.Duration(v) * time.Second
}