Files
sub2api/backend/internal/service/gemini_sse_comment_compat.go
T
HuntercodeT f79b8bf96c fix(antigravity): Gemini 原生流对 go-genai / python-genai 客户端不发 SSE 注释心跳
Antigravity 账号的 Gemini 原生流式路径在上游空闲超过 gateway.stream_keepalive_interval
(默认 10s)时会向下游写 ":\n\n" 作为心跳。SSE 规范允许注释行,但 Google 官方 SDK 里
go-genai 与 python-genai 都不会忽略它:

- go-genai(Antigravity CLI 在用)api_client.go iterateResponseStream 对任何非 data: 前缀的
  事件直接返回 "iterateResponseStream: invalid stream chunk: :",整个流中断;
- python-genai _api_client.py 把非 data: 行当作错误 JSON 拼接后 json.loads,同样抛错。

Google 官方 API 从不下发注释行,这两个 SDK 从未处理过这种输入。js-genai 用规范的
event-stream 解析器,不受影响。

现在按下游 User-Agent / X-Goog-Api-Client 识别这两类 SDK(形如
google-genai-sdk/1.71.0 gl-go/go1.28 …、google-genai-sdk/x gl-python/3.12),命中时该路径
不再发注释心跳;其他客户端(curl、js-genai、Gemini CLI)行为不变。

生产复现:Antigravity CLI 1.2.5 经网关(sub2api 0.2.5)跑长思考请求,上游首个 chunk 超过 10s
时客户端报 "agent executor error: … iterateResponseStream: invalid stream chunk: :"。
2026-09-17 19:41:55 +08:00

36 lines
1.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"strings"
"github.com/gin-gonic/gin"
)
// geminiClientRejectsSSEComments 判断下游客户端是否是「无法容忍 SSE 注释行」的 Google GenAI SDK。
//
// Gemini 原生流里我们用 ":\n\n" 作为空闲心跳(防止代理/Cloudflare Tunnel 断开),但官方 SDK 里
// 有两个实现不按 SSE 规范忽略注释行:
// - go-genai(Antigravity CLI 在用):api_client.go iterateResponseStream 对任何非 "data:" 前缀的
// 事件直接报 "invalid stream chunk: :",整个流中断;
// - python-genai:_api_client.py 把非 "data:" 行当作错误 JSON 拼接后 json.loads,同样抛错。
//
// js-genai 用的是规范的 event-stream 解析器,能正确忽略注释。Google 官方 API 从不下发注释行,
// 所以这两个 SDK 从未处理过这种输入。识别依据是 SDK 固定携带的 User-Agent / X-Goog-Api-Client,
// 形如 "google-genai-sdk/1.71.0 gl-go/go1.28 ..." 或 "google-genai-sdk/1.x gl-python/3.12"。
func geminiClientRejectsSSEComments(clientHint string) bool {
hint := strings.ToLower(strings.TrimSpace(clientHint))
if !strings.Contains(hint, "google-genai-sdk/") {
return false
}
return strings.Contains(hint, "gl-go/") || strings.Contains(hint, "gl-python/")
}
// downstreamRejectsSSEComments 从请求头识别下游客户端(同时看 User-Agent 与 X-Goog-Api-Client)。
func downstreamRejectsSSEComments(c *gin.Context) bool {
if c == nil || c.Request == nil {
return false
}
return geminiClientRejectsSSEComments(c.GetHeader("User-Agent")) ||
geminiClientRejectsSSEComments(c.GetHeader("X-Goog-Api-Client"))
}