Merge pull request #7411 from HuntercodeT/fix/antigravity-vendor-identity-429

fix(antigravity): neutralize Claude Agent SDK identity in system prompts
This commit is contained in:
Wesley Liddick
2026-09-23 10:01:18 +08:00
committed by GitHub
3 changed files with 168 additions and 2 deletions
@@ -86,3 +86,98 @@ func TestTransformClaudeToGemini_AttributionDoesNotHideIdentity(t *testing.T) {
require.Equal(t, "You are Antigravity. Keep this identity.", got.Parts[0].Text)
require.False(t, strings.Contains(got.Parts[0].Text, "cc_version="))
}
func TestNeutralizeClaudeIdentity(t *testing.T) {
tests := []struct {
name string
text string
want string
}{
{
name: "agent sdk opener",
text: "You are a Claude agent, built on Anthropic's Claude Agent SDK. You are naming a coding session.",
want: "You are an AI agent. You are naming a coding session.",
},
{
name: "cli opener",
text: "You are Claude Code, Anthropic's official CLI for Claude. Help with engineering tasks.",
want: "You are an AI agent. Help with engineering tasks.",
},
{
name: "leading whitespace",
text: "\n You are a Claude agent, built on Anthropic's Claude Agent SDK.\nRest.",
want: "You are an AI agent.\nRest.",
},
// User instructions that merely mention the vendor must stay untouched:
// they are content, not a client identity declaration.
{
name: "mention inside instructions",
text: "Follow CLAUDE.md. Prefer claude-sonnet-4-6 for long tasks.",
want: "Follow CLAUDE.md. Prefer claude-sonnet-4-6 for long tasks.",
},
{
name: "opener not at start",
text: "Example: You are a Claude agent, built on Anthropic's Claude Agent SDK.",
want: "Example: You are a Claude agent, built on Anthropic's Claude Agent SDK.",
},
{name: "ordinary text", text: "Keep these instructions.", want: "Keep these instructions."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, neutralizeClaudeIdentity(tt.text))
})
}
}
// The attribution line and the identity sentence arrive as separate system
// blocks in real Claude Code traffic; both have to be handled.
func TestAttributionAndIdentityTogether(t *testing.T) {
attribution := "x-anthropic-billing-header: cc_version=2.1.278.0a9; cc_entrypoint=claude-vscode;"
identity := "You are a Claude agent, built on Anthropic's Claude Agent SDK."
require.Equal(t, "", stripClaudeAttribution(attribution))
require.Equal(t, "You are an AI agent.", neutralizeClaudeIdentity(identity))
}
// End-to-end through the transformer: the attribution block and the identity
// sentence arrive as separate system blocks in real Claude Code traffic. The
// same words inside user content must survive untouched, because there they are
// conversation content rather than a client identity declaration.
func TestTransformClaudeToGemini_IdentitySystemBlock(t *testing.T) {
const (
attribution = "x-anthropic-billing-header: cc_version=2.1.278.0a9; cc_entrypoint=claude-vscode;"
identity = "You are a Claude agent, built on Anthropic's Claude Agent SDK."
task = "You are naming a coding session."
userText = "Does Claude Code support claude-sonnet-4-6?"
)
system := []SystemBlock{
{Type: "text", Text: attribution},
{Type: "text", Text: identity},
{Type: "text", Text: task},
}
systemJSON, err := json.Marshal(system)
require.NoError(t, err)
userJSON, err := json.Marshal(userText)
require.NoError(t, err)
input := &ClaudeRequest{
Model: "gemini-3.8-flash-high",
System: systemJSON,
Messages: []ClaudeMessage{{Role: "user", Content: userJSON}},
}
body, err := TransformClaudeToGeminiWithOptions(input, "test-project", input.Model, TransformOptions{})
require.NoError(t, err)
var got V1InternalRequest
require.NoError(t, json.Unmarshal(body, &got))
var texts []string
for _, part := range got.Request.SystemInstruction.Parts {
if part.Text != "\n--- [SYSTEM_PROMPT_END] ---" {
texts = append(texts, part.Text)
}
}
require.Equal(t, []string{"You are an AI agent.", task}, texts)
for _, text := range texts {
require.NotContains(t, text, "Claude")
require.NotContains(t, text, "Anthropic")
}
require.Equal(t, userText, got.Request.Contents[0].Parts[0].Text)
}
@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"math/rand"
"regexp"
"strconv"
"strings"
"sync"
@@ -297,6 +298,31 @@ func filterOpenCodePrompt(text string) string {
// from Antigravity system text. This is prompt metadata, not an HTTP header; it
// can trigger RESOURCE_EXHAUSTED on the Google upstream. Keep this scoped to the
// Antigravity transformer: native Anthropic OAuth may require the attribution.
// claudeIdentityOpeners are the Claude Agent SDK identity sentences that some
// Anthropic clients put at the start of a system block. Stripping the
// x-anthropic-billing-header line alone is not enough: a request whose system
// text still opens with this sentence is still rejected by the Google upstream
// with 429 RESOURCE_EXHAUSTED (see docs/ANTIGRAVITY_ATTRIBUTION_429.md).
//
// The patterns are anchored at the start of a system block on purpose, so user
// instructions that merely mention Claude or Anthropic are left untouched.
var claudeIdentityOpeners = []*regexp.Regexp{
regexp.MustCompile(`(?i)^[ \t\r\n]*You are a Claude agent, built on Anthropic'?s Claude Agent SDK\.?`),
regexp.MustCompile(`(?i)^[ \t\r\n]*You are Claude Code, Anthropic'?s official CLI for Claude\.?`),
}
// neutralizeClaudeIdentity rewrites a leading vendor identity sentence into a
// vendor-neutral one. It keeps the rest of the block, including any user
// instructions that follow.
func neutralizeClaudeIdentity(text string) string {
for _, re := range claudeIdentityOpeners {
if loc := re.FindStringIndex(text); loc != nil {
return "You are an AI agent." + text[loc[1]:]
}
}
return text
}
func stripClaudeAttribution(text string) string {
trimmed := strings.TrimLeft(text, " \t\r\n")
if !strings.HasPrefix(trimmed, "x-anthropic-billing-header:") {
@@ -325,7 +351,7 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans
// 尝试解析为字符串
var sysStr string
if err := json.Unmarshal(system, &sysStr); err == nil {
sysStr = stripClaudeAttribution(sysStr)
sysStr = neutralizeClaudeIdentity(stripClaudeAttribution(sysStr))
if strings.TrimSpace(sysStr) != "" {
if strings.Contains(sysStr, "You are Antigravity") {
userHasAntigravityIdentity = true
@@ -341,7 +367,7 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans
var sysBlocks []SystemBlock
if err := json.Unmarshal(system, &sysBlocks); err == nil {
for _, block := range sysBlocks {
block.Text = stripClaudeAttribution(block.Text)
block.Text = neutralizeClaudeIdentity(stripClaudeAttribution(block.Text))
if block.Type == "text" && strings.TrimSpace(block.Text) != "" {
if strings.Contains(block.Text, "You are Antigravity") {
userHasAntigravityIdentity = true
+45
View File
@@ -59,3 +59,48 @@ Claude Code 提供 [`CLAUDE_CODE_ATTRIBUTION_HEADER`](https://code.claude.com/do
回归测试覆盖两种 system 格式、换行与空白、保留后续指令、保留普通文本和用户 / 工具内容,以及不修改原请求。已有的无冒号 billing-header 文本保留测试继续适用。
修复后应使用真实客户端新建会话验证完整响应,并确认已有冷却已到期。如果仍返回 429,继续检查原始上游错误、实际额度、并发和模型映射;不要用无限重试或反复清空冷却替代定位。
## 补充:仅移除 attribution 行仍不足(2026-09-20)
在另一套部署上(**Sub2API 0.2.7,已包含上一节的 `stripClaudeAttribution` 修复**,
Claude Code 2.1.278 / claude-vscode 入口,映射模型 `gemini-3.8-flash-high`),
同一类 429 再次出现在 Claude Code 的**会话标题生成**请求上。
这类请求很小:约 4 KB、system 3201 字符、0 个工具、1 条消息。
也就是说,下面的“原始请求回放”本身就是在**已经移除 attribution 行**的代码上跑的。
对照实验在一个只绑定单个账号的隔离分组里进行,每次请求之前都等模型冷却到期,
避免上一次失败造成的冷却把后续结果污染成 503:
| 对照项 | 结果 |
| --- | --- |
| 原始请求回放 | 429(8/8) |
| 仅把 system 换成普通提示词,其余不变 | 200(8/8) |
| 回放前先由客户端侧移除 attribution 块(与网关行为叠加,确认无额外影响) | **429(3/3)** |
| 在上一行基础上再中性化身份开场白 | **200(3/3)** |
| 仅把 system 里的 `Claude` 替换为中性词 | 200(2/2) |
| 仅把 system 里的 `Anthropic` 移除 | 200(2/2) |
| 去掉 `output_config` / 改小 `max_tokens` / 改非流式 | 429(各 1/1,无影响) |
该请求的 system 是三个文本块:
1. 仅 attribution 元数据(本文上一节的修复会整块丢弃);
2. `You are a Claude agent, built on Anthropic's Claude Agent SDK.`;
3. 其余任务说明,不含厂商名。
也就是说,触发拒绝的不只是 attribution 元数据,**第 2 块的身份声明同样会触发**。
Claude Code 的主对话提示词以 `You are an interactive CLI tool` 开头,已被
`filterOpenCodePrompt` 处理,所以只有 Agent SDK 这类提示词漏网。
### 本次修复
在 attribution 处理之后,追加一次**开头锚定**的身份句中性化:
- 命中 `You are a Claude agent, built on Anthropic's Claude Agent SDK.` 与
`You are Claude Code, Anthropic's official CLI for Claude.` 两种开场白,替换为
`You are an AI agent.`,块内其余内容原样保留。
- 只在 system 文本块**开头**匹配:正文中间提到 Claude/Anthropic 的用户指令不受影响,
`claude-sonnet-4-6` 这类模型名也不会被改写。
- 不修改用户消息、工具定义、模型映射与认证;仅作用于 Antigravity 转换器。
同样地,这些对照只支持“这段文本在本次请求中触发拒绝”的判断,不能推断 Google 的内部策略。