refactor(config): replace nonstream-keepalive with nonstream-keepalive-interval

- Updated `SDKConfig` to use `nonstream-keepalive-interval` (seconds) instead of the boolean `nonstream-keepalive`.
- Refactored handlers and logic to incorporate the new interval-based configuration.
- Updated config diff, tests, and example YAML to reflect the changes.
This commit is contained in:
Luis Pater
2026-01-13 03:14:38 +08:00
parent b1b379ea18
commit 43652d044c
5 changed files with 35 additions and 21 deletions

View File

@@ -77,8 +77,8 @@ routing:
# When true, enable authentication for the WebSocket API (/v1/ws). # When true, enable authentication for the WebSocket API (/v1/ws).
ws-auth: false ws-auth: false
# When true, emit blank lines every 5s for non-streaming responses to prevent idle timeouts. # When > 0, emit blank lines every N seconds for non-streaming responses to prevent idle timeouts.
nonstream-keepalive: false nonstream-keepalive-interval: 0
# Streaming behavior (SSE keep-alives + safe bootstrap retries). # Streaming behavior (SSE keep-alives + safe bootstrap retries).
# streaming: # streaming:

View File

@@ -26,8 +26,9 @@ type SDKConfig struct {
// Streaming configures server-side streaming behavior (keep-alives and safe bootstrap retries). // Streaming configures server-side streaming behavior (keep-alives and safe bootstrap retries).
Streaming StreamingConfig `yaml:"streaming" json:"streaming"` Streaming StreamingConfig `yaml:"streaming" json:"streaming"`
// NonStreamKeepAlive enables emitting blank lines every 5 seconds for non-streaming responses. // NonStreamKeepAliveInterval controls how often blank lines are emitted for non-streaming responses.
NonStreamKeepAlive bool `yaml:"nonstream-keepalive" json:"nonstream-keepalive"` // <= 0 disables keep-alives. Value is in seconds.
NonStreamKeepAliveInterval int `yaml:"nonstream-keepalive-interval,omitempty" json:"nonstream-keepalive-interval,omitempty"`
} }
// StreamingConfig holds server streaming behavior configuration. // StreamingConfig holds server streaming behavior configuration.

View File

@@ -54,8 +54,8 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
if oldCfg.ForceModelPrefix != newCfg.ForceModelPrefix { if oldCfg.ForceModelPrefix != newCfg.ForceModelPrefix {
changes = append(changes, fmt.Sprintf("force-model-prefix: %t -> %t", oldCfg.ForceModelPrefix, newCfg.ForceModelPrefix)) changes = append(changes, fmt.Sprintf("force-model-prefix: %t -> %t", oldCfg.ForceModelPrefix, newCfg.ForceModelPrefix))
} }
if oldCfg.NonStreamKeepAlive != newCfg.NonStreamKeepAlive { if oldCfg.NonStreamKeepAliveInterval != newCfg.NonStreamKeepAliveInterval {
changes = append(changes, fmt.Sprintf("nonstream-keepalive: %t -> %t", oldCfg.NonStreamKeepAlive, newCfg.NonStreamKeepAlive)) changes = append(changes, fmt.Sprintf("nonstream-keepalive-interval: %d -> %d", oldCfg.NonStreamKeepAliveInterval, newCfg.NonStreamKeepAliveInterval))
} }
// Quota-exceeded behavior // Quota-exceeded behavior

View File

@@ -235,7 +235,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
ProxyURL: "http://old-proxy", ProxyURL: "http://old-proxy",
APIKeys: []string{"key-1"}, APIKeys: []string{"key-1"},
ForceModelPrefix: false, ForceModelPrefix: false,
NonStreamKeepAlive: false, NonStreamKeepAliveInterval: 0,
}, },
} }
newCfg := &config.Config{ newCfg := &config.Config{
@@ -272,7 +272,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
ProxyURL: "http://new-proxy", ProxyURL: "http://new-proxy",
APIKeys: []string{" key-1 ", "key-2"}, APIKeys: []string{" key-1 ", "key-2"},
ForceModelPrefix: true, ForceModelPrefix: true,
NonStreamKeepAlive: true, NonStreamKeepAliveInterval: 5,
}, },
} }
@@ -287,7 +287,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
expectContains(t, details, "proxy-url: http://old-proxy -> http://new-proxy") expectContains(t, details, "proxy-url: http://old-proxy -> http://new-proxy")
expectContains(t, details, "ws-auth: false -> true") expectContains(t, details, "ws-auth: false -> true")
expectContains(t, details, "force-model-prefix: false -> true") expectContains(t, details, "force-model-prefix: false -> true")
expectContains(t, details, "nonstream-keepalive: false -> true") expectContains(t, details, "nonstream-keepalive-interval: 0 -> 5")
expectContains(t, details, "quota-exceeded.switch-project: false -> true") expectContains(t, details, "quota-exceeded.switch-project: false -> true")
expectContains(t, details, "quota-exceeded.switch-preview-model: false -> true") expectContains(t, details, "quota-exceeded.switch-preview-model: false -> true")
expectContains(t, details, "api-keys count: 1 -> 2") expectContains(t, details, "api-keys count: 1 -> 2")

View File

@@ -49,7 +49,6 @@ const idempotencyKeyMetadataKey = "idempotency_key"
const ( const (
defaultStreamingKeepAliveSeconds = 0 defaultStreamingKeepAliveSeconds = 0
defaultStreamingBootstrapRetries = 0 defaultStreamingBootstrapRetries = 0
nonStreamingKeepAliveInterval = 5 * time.Second
) )
// BuildErrorResponseBody builds an OpenAI-compatible JSON error response body. // BuildErrorResponseBody builds an OpenAI-compatible JSON error response body.
@@ -115,6 +114,19 @@ func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration {
return time.Duration(seconds) * time.Second return time.Duration(seconds) * time.Second
} }
// NonStreamingKeepAliveInterval returns the keep-alive interval for non-streaming responses.
// Returning 0 disables keep-alives (default when unset).
func NonStreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration {
seconds := 0
if cfg != nil {
seconds = cfg.NonStreamKeepAliveInterval
}
if seconds <= 0 {
return 0
}
return time.Duration(seconds) * time.Second
}
// StreamingBootstrapRetries returns how many times a streaming request may be retried before any bytes are sent. // StreamingBootstrapRetries returns how many times a streaming request may be retried before any bytes are sent.
func StreamingBootstrapRetries(cfg *config.SDKConfig) int { func StreamingBootstrapRetries(cfg *config.SDKConfig) int {
retries := defaultStreamingBootstrapRetries retries := defaultStreamingBootstrapRetries
@@ -298,10 +310,11 @@ func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *
// StartNonStreamingKeepAlive emits blank lines every 5 seconds while waiting for a non-streaming response. // StartNonStreamingKeepAlive emits blank lines every 5 seconds while waiting for a non-streaming response.
// It returns a stop function that must be called before writing the final response. // It returns a stop function that must be called before writing the final response.
func (h *BaseAPIHandler) StartNonStreamingKeepAlive(c *gin.Context, ctx context.Context) func() { func (h *BaseAPIHandler) StartNonStreamingKeepAlive(c *gin.Context, ctx context.Context) func() {
if h == nil || h.Cfg == nil || !h.Cfg.NonStreamKeepAlive { if h == nil || c == nil {
return func() {} return func() {}
} }
if c == nil { interval := NonStreamingKeepAliveInterval(h.Cfg)
if interval <= 0 {
return func() {} return func() {}
} }
flusher, ok := c.Writer.(http.Flusher) flusher, ok := c.Writer.(http.Flusher)
@@ -318,7 +331,7 @@ func (h *BaseAPIHandler) StartNonStreamingKeepAlive(c *gin.Context, ctx context.
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
ticker := time.NewTicker(nonStreamingKeepAliveInterval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {