mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-22 06:40:21 +08:00
Merge pull request #3218 from wucm667/fix/antigravity-system-role-message
fix(antigravity): 处理 messages 中 role:system 消息,修复 Claude Code 接入 400
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user