Merge pull request #3218 from wucm667/fix/antigravity-system-role-message

fix(antigravity): 处理 messages 中 role:system 消息,修复 Claude Code 接入 400
This commit is contained in:
Wesley Liddick
2026-06-16 11:12:08 +08:00
committed by GitHub
2 changed files with 125 additions and 4 deletions
@@ -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 验证
@@ -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",