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 }