Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- feat: opt-in HTTP/2 PING keepalives on the Bedrock provider via a configurable interval (0 = off) [@jeremym-tanium](https://github.com/jeremym-tanium)
9 changes: 9 additions & 0 deletions core/providers/bedrock/bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ func NewBedrockProvider(config *schemas.ProviderConfig, logger schemas.Logger) (
transport.TLSClientConfig = tlsConfig
}

// When HTTP/2 is enforced and a ping interval is configured, send client-initiated
// PING keepalives so an idle streaming connection isn't closed by an intermediary
// (surfaces as "unexpected EOF"). Left off by default; opt in via the interval.
if config.NetworkConfig.EnforceHTTP2 && config.NetworkConfig.HTTP2PingIntervalInSeconds > 0 {
transport.HTTP2 = &http.HTTP2Config{
SendPingTimeout: time.Duration(config.NetworkConfig.HTTP2PingIntervalInSeconds) * time.Second,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

client := &http.Client{Transport: transport, Timeout: requestTimeout}
streamingClient := providerUtils.BuildStreamingHTTPClient(client)

Expand Down
5 changes: 5 additions & 0 deletions core/schemas/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type NetworkConfig struct {
KeepAliveTimeoutInSeconds int `json:"keep_alive_timeout_in_seconds,omitempty"` // Idle keep-alive for pooled connections; set below the upstream server's keep-alive to avoid reusing connections it has already closed. Default: 30s
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` // Max TCP connections per provider host (default: 5000)
EnforceHTTP2 bool `json:"enforce_http2,omitempty"` // Force HTTP/2 on provider connections (relevant for net/http-based providers like Bedrock)
HTTP2PingIntervalInSeconds int `json:"http2_ping_interval_in_seconds,omitempty"` // Seconds of stream idle before an HTTP/2 keepalive PING (0 = disabled; only when enforce_http2)
BetaHeaderOverrides map[string]bool `json:"beta_header_overrides,omitempty"` // Override default beta header support per provider (keys are prefixes like "redact-thinking-")
AllowPrivateNetwork bool `json:"allow_private_network,omitempty"` // Allow connections to RFC 1918 private IPs (for k8s pods, LAN deployments). Link-local (169.254.x.x) is always blocked.
}
Expand All @@ -90,6 +91,7 @@ func (nc *NetworkConfig) UnmarshalJSON(data []byte) error {
KeepAliveTimeoutInSeconds int `json:"keep_alive_timeout_in_seconds,omitempty"`
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
EnforceHTTP2 bool `json:"enforce_http2,omitempty"`
HTTP2PingIntervalInSeconds int `json:"http2_ping_interval_in_seconds,omitempty"`
BetaHeaderOverrides map[string]bool `json:"beta_header_overrides,omitempty"`
AllowPrivateNetwork bool `json:"allow_private_network,omitempty"`
}
Expand All @@ -110,6 +112,7 @@ func (nc *NetworkConfig) UnmarshalJSON(data []byte) error {
nc.KeepAliveTimeoutInSeconds = alias.KeepAliveTimeoutInSeconds
nc.MaxConnsPerHost = alias.MaxConnsPerHost
nc.EnforceHTTP2 = alias.EnforceHTTP2
nc.HTTP2PingIntervalInSeconds = alias.HTTP2PingIntervalInSeconds
nc.BetaHeaderOverrides = alias.BetaHeaderOverrides
nc.AllowPrivateNetwork = alias.AllowPrivateNetwork

Expand Down Expand Up @@ -183,6 +186,7 @@ func (nc NetworkConfig) MarshalJSON() ([]byte, error) {
KeepAliveTimeoutInSeconds int `json:"keep_alive_timeout_in_seconds,omitempty"`
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
EnforceHTTP2 bool `json:"enforce_http2,omitempty"`
HTTP2PingIntervalInSeconds int `json:"http2_ping_interval_in_seconds,omitempty"`
BetaHeaderOverrides map[string]bool `json:"beta_header_overrides,omitempty"`
AllowPrivateNetwork bool `json:"allow_private_network,omitempty"`
}
Expand All @@ -200,6 +204,7 @@ func (nc NetworkConfig) MarshalJSON() ([]byte, error) {
KeepAliveTimeoutInSeconds: nc.KeepAliveTimeoutInSeconds,
MaxConnsPerHost: nc.MaxConnsPerHost,
EnforceHTTP2: nc.EnforceHTTP2,
HTTP2PingIntervalInSeconds: nc.HTTP2PingIntervalInSeconds,
BetaHeaderOverrides: nc.BetaHeaderOverrides,
AllowPrivateNetwork: nc.AllowPrivateNetwork,
}
Expand Down
25 changes: 25 additions & 0 deletions core/schemas/serialization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,31 @@ func TestNetworkConfig_StreamIdleTimeoutRoundTrip(t *testing.T) {
assert.Contains(t, string(data), `"stream_idle_timeout_in_seconds":120`)
}

func TestNetworkConfig_HTTP2PingInterval(t *testing.T) {
nc := NetworkConfig{EnforceHTTP2: true, HTTP2PingIntervalInSeconds: 45}
data, err := json.Marshal(nc)
require.NoError(t, err)
var decoded NetworkConfig
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, 45, decoded.HTTP2PingIntervalInSeconds, "http2_ping_interval_in_seconds should round-trip")
assert.Contains(t, string(data), `"http2_ping_interval_in_seconds":45`)

// enforce_http2 set + interval unset -> left at zero (pings are opt-in, off by default)
cfgDefault := &ProviderConfig{NetworkConfig: NetworkConfig{EnforceHTTP2: true}}
cfgDefault.CheckAndSetDefaults()
assert.Equal(t, 0, cfgDefault.NetworkConfig.HTTP2PingIntervalInSeconds)

// explicit interval is preserved
cfgExplicit := &ProviderConfig{NetworkConfig: NetworkConfig{EnforceHTTP2: true, HTTP2PingIntervalInSeconds: 5}}
cfgExplicit.CheckAndSetDefaults()
assert.Equal(t, 5, cfgExplicit.NetworkConfig.HTTP2PingIntervalInSeconds)

// enforce_http2 off -> left at zero (no ping keepalive)
cfgOff := &ProviderConfig{}
cfgOff.CheckAndSetDefaults()
assert.Equal(t, 0, cfgOff.NetworkConfig.HTTP2PingIntervalInSeconds)
}

// TestNormalizeResponsesToolType verifies that versioned/provider-specific tool type
// strings are normalized to their canonical ResponsesToolType values.
func TestNormalizeResponsesToolType(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions helm-charts/bifrost/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5725,6 +5725,11 @@
"type": "boolean",
"description": "Force HTTP/2 on provider connections (relevant for Bedrock and other net/http-based providers)"
},
"http2_ping_interval_in_seconds": {
"type": "integer",
"minimum": 0,
"description": "Seconds of stream idle before a client-initiated HTTP/2 keepalive PING (0 = disabled; only when enforce_http2)."
},
"beta_header_overrides": {
"type": "object",
"additionalProperties": {
Expand Down
10 changes: 10 additions & 0 deletions transports/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3698,6 +3698,11 @@
"type": "boolean",
"description": "Force HTTP/2 on provider connections (relevant for Bedrock and other net/http-based providers)"
},
"http2_ping_interval_in_seconds": {
"type": "integer",
"minimum": 0,
"description": "Seconds of stream idle before a client-initiated HTTP/2 keepalive PING (0 = disabled; only when enforce_http2)."
},
"insecure_skip_verify": {
"type": "boolean",
"description": "Disable TLS certificate verification for provider connections. This bypasses server certificate validation and should be used only as a last resort when a trusted CA chain cannot be configured. Prefer ca_cert_pem for self-signed or private CA deployments."
Expand Down Expand Up @@ -3773,6 +3778,11 @@
"type": "boolean",
"description": "Force HTTP/2 on provider connections (relevant for Bedrock and other net/http-based providers)"
},
"http2_ping_interval_in_seconds": {
"type": "integer",
"minimum": 0,
"description": "Seconds of stream idle before a client-initiated HTTP/2 keepalive PING (0 = disabled; only when enforce_http2)."
},
"insecure_skip_verify": {
"type": "boolean",
"description": "Disable TLS certificate verification for provider connections. This bypasses server certificate validation and should be used only as a last resort when a trusted CA chain cannot be configured. Prefer ca_cert_pem for self-signed or private CA deployments."
Expand Down
1 change: 1 addition & 0 deletions ui/lib/schemas/providerForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const NetworkConfigSchema = z
keep_alive_timeout_in_seconds: z.number().int().min(1).max(3600).optional(),
max_conns_per_host: z.number().int().min(1).max(10000).optional(),
enforce_http2: z.boolean().optional(),
http2_ping_interval_in_seconds: z.number().int().min(0).optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the provider-form bound consistent with the shared network schema.

This accepts any non-negative integer, while ui/lib/types/schemas.ts caps the same setting at 3600 seconds. Values above 3600 can therefore pass this schema but fail shared validation later. Add the same .max(3600, ...) constraint here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/lib/schemas/providerForm.ts` at line 46, Update the
http2_ping_interval_in_seconds field in the provider form schema to include the
shared 3600-second maximum, preserving its integer, non-negative, and optional
constraints and using the same validation message as the shared network schema.

})
.refine((v) => v.retry_backoff_initial <= v.retry_backoff_max, {
message: "Initial backoff must be <= max backoff",
Expand Down
1 change: 1 addition & 0 deletions ui/lib/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export interface NetworkConfig {
keep_alive_timeout_in_seconds?: number;
max_conns_per_host?: number;
enforce_http2?: boolean;
http2_ping_interval_in_seconds?: number;
beta_header_overrides?: Record<string, boolean>;
allow_private_network?: boolean;
}
Expand Down
12 changes: 12 additions & 0 deletions ui/lib/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ export const networkConfigSchema = z
.max(10000, "Max connections must be at most 10000")
.optional(),
enforce_http2: z.boolean().optional(),
http2_ping_interval_in_seconds: z
.number()
.int("HTTP/2 ping interval must be a whole number of seconds")
.min(0, "HTTP/2 ping interval must be at least 0 seconds")
.max(3600, "HTTP/2 ping interval must be at most 3600 seconds i.e. 60 minutes")
.optional(),
allow_private_network: z.boolean().optional(),
})
.refine((d) => d.retry_backoff_initial <= d.retry_backoff_max, {
Expand Down Expand Up @@ -489,6 +495,12 @@ export const networkFormConfigSchema = z
.max(10000, "Max connections must be at most 10000")
.optional(),
enforce_http2: z.boolean().optional(),
http2_ping_interval_in_seconds: z.coerce
.number("HTTP/2 ping interval must be a number")
.int("HTTP/2 ping interval must be a whole number of seconds")
.min(0, "HTTP/2 ping interval must be at least 0 seconds")
.max(3600, "HTTP/2 ping interval must be at most 3600 seconds i.e. 60 minutes")
.optional(),
allow_private_network: z.boolean().optional(),
})
.refine((d) => d.retry_backoff_initial <= d.retry_backoff_max, {
Expand Down