diff --git a/backend/internal/handler/gateway_helper.go b/backend/internal/handler/gateway_helper.go index 09e6c09baf8..7c263550e57 100644 --- a/backend/internal/handler/gateway_helper.go +++ b/backend/internal/handler/gateway_helper.go @@ -131,8 +131,9 @@ const ( type SSEPingFormat string const ( - // SSEPingFormatClaude is the Claude/Anthropic SSE ping format - SSEPingFormatClaude SSEPingFormat = "data: {\"type\": \"ping\"}\n\n" + // SSEPingFormatClaude uses SSE comment format: keeps HTTP connection alive without + // being yielded by Anthropic SDK, so client idle watchdogs work correctly. + SSEPingFormatClaude SSEPingFormat = ": keepalive\n\n" // SSEPingFormatNone indicates no ping should be sent (e.g., OpenAI has no ping spec) SSEPingFormatNone SSEPingFormat = "" // SSEPingFormatComment is an SSE comment ping for OpenAI/Codex CLI clients diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index a76e59fbea9..2e81c638a28 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -4053,9 +4053,8 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context if time.Since(lastDataAt) < keepaliveInterval { continue } - // SSE ping 事件:Anthropic 原生格式,客户端会正确处理, - // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开 - if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") { + // SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。 + if !cw.Fprintf(": keepalive\n\n") { logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity claude), continuing to drain upstream for billing") continue } @@ -4457,9 +4456,8 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp if time.Since(lastDataAt) < keepaliveInterval { continue } - // SSE ping 事件:Anthropic 原生格式,客户端会正确处理, - // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开 - if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") { + // SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。 + if !cw.Fprintf(": keepalive\n\n") { logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity upstream), continuing to drain upstream for billing") continue } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index c65e828a0dc..c122306b062 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -6939,9 +6939,9 @@ func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http if time.Since(lastDataAt) < keepaliveInterval { continue } - // SSE ping 事件:Anthropic 原生格式,客户端会正确处理, - // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开 - if _, werr := fmt.Fprint(w, "event: ping\ndata: {\"type\": \"ping\"}\n\n"); werr != nil { + // SSE 注释格式 keepalive:在 HTTP 层保持连接活跃(防止 Cloudflare Tunnel 等代理因空闲断开), + // 但不会被 Anthropic SDK yield 给消费者,不干扰客户端(如 Claude Code)的 idle watchdog。 + if _, werr := fmt.Fprint(w, ": keepalive\n\n"); werr != nil { clientDisconnected = true logger.LegacyPrintf("service.gateway", "Client disconnected during keepalive ping, continuing to drain upstream for billing") continue diff --git a/backend/internal/service/openai_gateway_messages.go b/backend/internal/service/openai_gateway_messages.go index a72b9bbf4bd..3a0add5596f 100644 --- a/backend/internal/service/openai_gateway_messages.go +++ b/backend/internal/service/openai_gateway_messages.go @@ -552,8 +552,8 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( if time.Since(lastDataAt) < keepaliveInterval { continue } - // Send Anthropic-format ping event - if _, err := fmt.Fprint(c.Writer, "event: ping\ndata: {\"type\":\"ping\"}\n\n"); err != nil { + // SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。 + if _, err := fmt.Fprint(c.Writer, ": keepalive\n\n"); err != nil { // Client disconnected logger.L().Info("openai messages stream: client disconnected during keepalive", zap.String("request_id", requestID),