fix(antigravity): neutralize Claude Agent SDK identity in system prompts

Stripping the x-anthropic-billing-header line is not enough. Claude Code's
Agent SDK prompts (session title generation, and other SDK-driven calls) open
a separate system block with

    You are a Claude agent, built on Anthropic's Claude Agent SDK.

and the Google upstream still rejects those requests with 429 RESOURCE_EXHAUSTED.

Measured on Sub2API 0.2.7, which already contains the attribution fix, using an isolated single-account group, waiting for the per-model cooldown
to expire before every request so that a previous failure could not turn later
results into 503:

  original request replayed                      429 (8/8)
  same request, generic system prompt            200 (8/8)
  attribution also removed client-side           429 (3/3)
  attribution removed + identity neutralized     200 (3/3)
  only 'Claude' replaced in system               200 (2/2)
  only 'Anthropic' removed from system           200 (2/2)
  output_config / max_tokens / non-stream        no effect

The rewrite is anchored at the start of a system block, so user instructions that
merely mention Claude or Anthropic, and model ids such as claude-sonnet-4-6, are
left untouched. Scoped to the Antigravity transformer, as with the existing
attribution handling.
This commit is contained in:
HuntercodeT
2026-09-20 16:34:26 +08:00
parent acdf3c54da
commit dd292e3b00
3 changed files with 124 additions and 2 deletions
@@ -86,3 +86,54 @@ 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))
}
@@ -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