mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 的内部策略。
|
||||
|
||||
Reference in New Issue
Block a user