Merge pull request #7162 from HuntercodeT/fix/antigravity-gemini-sse-separator

fix(antigravity): gemini 透传 SSE 事件之间不再多写一个空行(Antigravity CLI invalid stream chunk)
This commit is contained in:
Wesley Liddick
2026-09-15 16:17:25 +08:00
committed by GitHub
2 changed files with 51 additions and 0 deletions
@@ -1387,6 +1387,49 @@ func TestHandleClaudeStreamingResponse_NormalComplete(t *testing.T) {
// TestHandleGeminiStreamingResponse_ThoughtsTokenCount
// 验证:Gemini 流式转发时 thoughtsTokenCount 被计入 OutputTokens
// 回归:上游事件之间的空分隔行不能再透传,否则下游看到的是 "data: ...\n\n\n"。
// google-genai Go SDK 按 "\n\n" 切事件,多出的 "\n" 会让第二个事件的前缀变成
// "\ndata" 而报 invalid stream chunk(Antigravity CLI 每条流都在第二个事件中断)。
func TestHandleGeminiStreamingResponse_EventSeparatorIsExactlyOneBlankLine(t *testing.T) {
gin.SetMode(gin.TestMode)
svc := newAntigravityTestService(&config.Config{
Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize},
})
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
pr, pw := io.Pipe()
resp := &http.Response{StatusCode: http.StatusOK, Body: pr, Header: http.Header{}}
first := `{"candidates":[{"content":{"role":"model","parts":[{"text":"Hello"}]}}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":3}}`
second := `{"candidates":[{"content":{"role":"model","parts":[{"thoughtSignature":"sig","text":""}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":3,"thoughtsTokenCount":5}}`
go func() {
defer func() { _ = pw.Close() }()
// 上游原样:每个 data 行后跟一个空分隔行(第二个事件用 CRLF,覆盖两种换行)
fmt.Fprintf(pw, "data: %s\n\n", first)
fmt.Fprintf(pw, "data: %s\r\n\r\n", second)
}()
result, err := svc.handleGeminiStreamingResponse(c, resp, time.Now())
_ = pr.Close()
require.NoError(t, err)
require.NotNil(t, result)
body := rec.Body.String()
require.Equal(t, "data: "+first+"\n\ndata: "+second+"\n\n", body)
require.NotContains(t, body, "\n\n\n", "events must be separated by exactly one blank line")
// 模拟 google-genai 的切帧方式:按 "\n\n" 切,每个非空 token 都必须以 "data" 为前缀
for _, token := range strings.Split(strings.TrimSuffix(body, "\n\n"), "\n\n") {
prefix, _, _ := strings.Cut(token, ":")
require.Equal(t, "data", prefix, "token %q would be rejected by a \\n\\n-delimited SSE parser", token)
}
}
func TestHandleGeminiStreamingResponse_ThoughtsTokenCount(t *testing.T) {
gin.SetMode(gin.TestMode)
svc := newAntigravityTestService(&config.Config{
@@ -288,6 +288,14 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
continue
}
// 上游每个 data 事件后面跟一个空行作为事件分隔。上面已经把 data 行写成
// "data: ...\n\n",若再把这个空行透传出去,事件之间就会变成 "\n\n\n"。
// google-genai 的 Go SDK(Antigravity CLI 在用)按 "\n\n" 切事件,多出的
// "\n" 会粘到下一个事件开头,前缀变成 "\ndata" 而被判成 invalid stream chunk。
if trimmed == "" {
continue
}
cw.Fprintf("%s\n", line)
case <-intervalCh: