From 65559ac58993c5eb42eb14d9f889ec76f2f44c8e Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 11 Jun 2026 18:37:00 +0800 Subject: [PATCH] fix(antigravity): merge system role messages --- .../pkg/antigravity/request_transformer.go | 20 +++- .../antigravity/request_transformer_test.go | 109 ++++++++++++++++++ 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index ac3610d0ab..5fe2de8406 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -107,13 +107,19 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map allowDummyThought := strings.HasPrefix(targetModel, "gemini-") // 1. 构建 contents - contents, strippedThinking, err := buildContents(claudeReq.Messages, toolIDToName, isThinkingEnabled, allowDummyThought) + contents, messageSystemParts, strippedThinking, err := buildContents(claudeReq.Messages, toolIDToName, isThinkingEnabled, allowDummyThought) if err != nil { return nil, fmt.Errorf("build contents: %w", err) } // 2. 构建 systemInstruction(使用 targetModel 而非原始请求模型,确保身份注入基于最终模型) systemInstruction := buildSystemInstruction(claudeReq.System, targetModel, opts, claudeReq.Tools) + if len(messageSystemParts) > 0 { + if systemInstruction == nil { + systemInstruction = &GeminiContent{Role: "user"} + } + systemInstruction.Parts = append(systemInstruction.Parts, messageSystemParts...) + } // 3. 构建 generationConfig reqForConfig := claudeReq @@ -357,8 +363,9 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans } // buildContents 构建 contents -func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isThinkingEnabled, allowDummyThought bool) ([]GeminiContent, bool, error) { +func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isThinkingEnabled, allowDummyThought bool) ([]GeminiContent, []GeminiPart, bool, error) { var contents []GeminiContent + var systemParts []GeminiPart strippedThinking := false for i, msg := range messages { @@ -369,12 +376,17 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT parts, strippedThisMsg, err := buildParts(msg.Content, toolIDToName, allowDummyThought) if err != nil { - return nil, false, fmt.Errorf("build parts for message %d: %w", i, err) + return nil, nil, false, fmt.Errorf("build parts for message %d: %w", i, err) } if strippedThisMsg { strippedThinking = true } + if role == "system" { + systemParts = append(systemParts, parts...) + continue + } + // 只有 Gemini 模型支持 dummy thinking block workaround // 只对最后一条 assistant 消息添加(Pre-fill 场景) // 历史 assistant 消息不能添加没有 signature 的 dummy thinking block @@ -406,7 +418,7 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT }) } - return contents, strippedThinking, nil + return contents, systemParts, strippedThinking, nil } // DummyThoughtSignature 用于跳过 Gemini 3 thought_signature 验证 diff --git a/backend/internal/pkg/antigravity/request_transformer_test.go b/backend/internal/pkg/antigravity/request_transformer_test.go index 6fae5b7c56..ea95cc7ea6 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -424,6 +424,115 @@ func TestTransformClaudeToGeminiWithOptions_PreservesBillingHeaderSystemBlock(t } } +func TestTransformClaudeToGeminiWithOptions_MessageRoles(t *testing.T) { + transform := func(t *testing.T, claudeReq *ClaudeRequest) V1InternalRequest { + t.Helper() + + body, err := TransformClaudeToGeminiWithOptions(claudeReq, "project-1", "gemini-2.5-flash", DefaultTransformOptions()) + require.NoError(t, err) + + var req V1InternalRequest + require.NoError(t, json.Unmarshal(body, &req)) + return req + } + + systemText := func(content *GeminiContent) string { + if content == nil { + return "" + } + var texts []string + for _, part := range content.Parts { + texts = append(texts, part.Text) + } + return strings.Join(texts, "\n") + } + + t.Run("message system role moves to system instruction", func(t *testing.T) { + req := transform(t, &ClaudeRequest{ + Model: "claude-3-5-sonnet-latest", + Messages: []ClaudeMessage{ + { + Role: "system", + Content: json.RawMessage(`[{"type":"text","text":"skills context"}]`), + }, + { + Role: "user", + Content: json.RawMessage(`"hello"`), + }, + }, + }) + + require.Len(t, req.Request.Contents, 1) + require.Equal(t, "user", req.Request.Contents[0].Role) + require.Contains(t, systemText(req.Request.SystemInstruction), "skills context") + for _, content := range req.Request.Contents { + require.NotEqual(t, "system", content.Role) + } + }) + + t.Run("assistant role still maps to model", func(t *testing.T) { + req := transform(t, &ClaudeRequest{ + Model: "claude-3-5-sonnet-latest", + Messages: []ClaudeMessage{ + { + Role: "assistant", + Content: json.RawMessage(`"hello from assistant"`), + }, + }, + }) + + require.Len(t, req.Request.Contents, 1) + require.Equal(t, "model", req.Request.Contents[0].Role) + require.Equal(t, "hello from assistant", req.Request.Contents[0].Parts[0].Text) + }) + + t.Run("top level and message system instructions are merged", func(t *testing.T) { + req := transform(t, &ClaudeRequest{ + Model: "claude-3-5-sonnet-latest", + System: json.RawMessage(`"top level system"`), + Messages: []ClaudeMessage{ + { + Role: "system", + Content: json.RawMessage(`"message system"`), + }, + { + Role: "user", + Content: json.RawMessage(`"hello"`), + }, + }, + }) + + mergedSystem := systemText(req.Request.SystemInstruction) + require.Contains(t, mergedSystem, "top level system") + require.Contains(t, mergedSystem, "message system") + require.Less(t, strings.Index(mergedSystem, "top level system"), strings.Index(mergedSystem, "message system")) + require.Len(t, req.Request.Contents, 1) + require.Equal(t, "user", req.Request.Contents[0].Role) + }) + + t.Run("ordinary user assistant conversation is unchanged", func(t *testing.T) { + req := transform(t, &ClaudeRequest{ + Model: "claude-3-5-sonnet-latest", + Messages: []ClaudeMessage{ + { + Role: "user", + Content: json.RawMessage(`"question"`), + }, + { + Role: "assistant", + Content: json.RawMessage(`"answer"`), + }, + }, + }) + + require.Len(t, req.Request.Contents, 2) + require.Equal(t, "user", req.Request.Contents[0].Role) + require.Equal(t, "question", req.Request.Contents[0].Parts[0].Text) + require.Equal(t, "model", req.Request.Contents[1].Role) + require.Equal(t, "answer", req.Request.Contents[1].Parts[0].Text) + }) +} + func TestTransformClaudeToGeminiWithOptions_PreservesWebSearchAlongsideFunctions(t *testing.T) { claudeReq := &ClaudeRequest{ Model: "claude-3-5-sonnet-latest",